Virtual Shelly Pro 3EM (Home Assistant–backed)
Overview
- Emulates a Shelly Pro 3EM device. It polls power/voltage/current/PF values from Home Assistant (HA) and exposes Shelly-like RPC over HTTP, WebSocket, and UDP, with mDNS discovery and simple energy integration/persistence.
- Useful for gateways/apps that expect a Shelly 3‑phase power meter on the LAN (e.g., Shelly app, b2500 integrations, custom dashboards).
Features
- HTTP API (FastAPI + Uvicorn) with:
- GET
/shelly: Gen2 device info (id, app, ver, model, gen, mac, sn, auth flags). - GET
/ui: Minimal web interface showing current values, recent client IPs, and endpoint metrics (auto-refreshes every 5s). - POST
/rpc: JSON‑RPC 2.0 envelope for Shelly.* and EM*/EMData* methods. - GET
/rpc?method=...and GET/rpc/{method}: returns the method result directly (no envelope), matching Shelly GET semantics. - GET
/healthzand GET/: simple health and info.
- GET
- WebSockets:
- WS
/rpc: JSON‑RPC over WebSocket (common for Shelly Gen2). - Fan‑out WS servers on TCP ports 6010–6022 echoing RPC responses (configurable).
- On
/rpcconnect: sendsNotifyFullStatus; then periodicNotifyStatusbroadcasts (throttled byWS_NOTIFY_INTERVAL).
- WS
- UDP RPC (Shelly‑style, compatible with tomquist/b2500‑meter):
- Supports
EM.GetStatus(3‑phase) andEM1.GetStatus(single total) with identical rounding/decimal behavior. - Listens on configurable UDP ports (defaults include 1010 and 2220).
- Supports
- Modbus TCP server (pymodbus):
- Exposes key Shelly Pro 3EM measurements (voltage, current, power, PF, energy totals) as 32-bit floats.
- Read-only input/holding registers share the same data; writing
1to holding register4200resets the virtual energy counters. - Defaults to TCP port 502 with unit ID 1 (configurable via env vars).
- mDNS service advertisements (
_http._tcpand_shelly._tcp) for discovery. - Energy counters integrated from power over time and persisted hourly at
/data/state.json(via Docker volume). - LightGBM forecasts replace phase and total power in Shelly responses. Raw readings are retained for energy integration, while the dashboard reports validation MAPE and model status.
- Simple
/metricsendpoint with Prometheus‑style counters for HTTP/WS/UDP events.
Quick Start (Docker Compose)
- Adjust
docker-compose.yamlenvironment to match your HA URL, token, and entity IDs. - Ensure your host can use host networking (recommended for UDP + mDNS).
- Bring it up:
docker compose up -d
- Verify:
curl http://<device-ip>/shellycurl "http://<device-ip>/rpc/EM.GetStatus?id=0"curl http://<device-ip>/healthz
Manual Run (no Docker)
- Python 3.11+
- LightGBM requires the OpenMP runtime (
libgomp.so.1on Linux; e.g. installlibgomp1on Debian/Ubuntu). pip install -r requirements.txtuvicorn app:app --host 0.0.0.0 --port 80
Configuration (env vars)
- Home Assistant
HA_BASE_URL: e.g.http://homeassistant:8123orhttp://192.168.x.x:8123HA_TOKEN: HA Long‑Lived Access Token (read‑only)POLL_INTERVAL: seconds between polls (default 2.0)INPUT_SOURCE: source for phase values. Supported:home_assistant(default) orshelly_webapiHA_SMOOTHING_ENABLE: average each sensor reading over the last 3 values (defaulttrue; set tofalseto disable)- Entity IDs (override as needed):
A_POWER,B_POWER,C_POWER,A_VOLT,B_VOLT,C_VOLT,A_CURR,B_CURR,C_CURR,A_PF,B_PF,C_PF - Shelly upstream source (used when
INPUT_SOURCE=shelly_webapi)SHELLY_BASE_URL: base URL of one upstream Shelly (for examplehttp://192.168.1.120)SHELLY_BASE_URLS: comma-separated list of Shelly base URLs to aggregate (takes precedence overSHELLY_BASE_URL)SHELLY_TIMEOUT: HTTP timeout in seconds for Shelly requests (default3.0)SHELLY_API_MODE: API mode for all configured Shelly devices (auto,gen2_rpc,gen1_emeter; defaultauto)SHELLY_API_MODES: optional comma-separated API modes per configured URL (same order asSHELLY_BASE_URLS; overridesSHELLY_API_MODEper device)
- Device identity
DEVICE_ID: Device identifier reported in RPC/mDNS (defaultshellypro3em-virtual-001)APP_ID: Shelly application identifier (defaultshellypro3em)MODEL: Hardware model string (defaultSHPRO-3EM)FIRMWARE: Firmware version reported asver(default1.0.0-virt)FW_ID: Firmware build identifier reported asfw_id(defaults to the same asFIRMWARE)MAC,SN,MANUFACTURERGENERATION: Shelly generation advertised via HTTP and mDNS TXT records (default2)
- HTTP/WebSocket
HTTP_PORT: container listens on this port and advertises it via mDNS (default80).WS_PORT_START,WS_PORT_END: WS fan‑out TCP range (default 6010–6022)WS_NOTIFY_INTERVAL: throttle seconds for WS/rpcNotifyStatus(default2.0).WS_NOTIFY_EPS: coalescing threshold in watts; only broadcast when change ≥ EPS (default0.1).CORS_ENABLE: enable CORS middleware (true|false, defaultfalse).CORS_ORIGINS: comma‑separated allowed origins (default*).- Request‑side scaling (divide power among active client IPs minus one, so multiple batteries share the load without oscillating)
REQUEST_SIDE_SCALING_ENABLE:true|false(defaulttrue)REQUEST_SIDE_SCALING_CLIENTS: integer override for asking-device count (default0= auto by active IPs); the divisor is this count minus one, with a minimum of one
- Total power offsets
POSITIVE_POWER_OFFSET: watts subtracted when total power is positive (default10.0)NEGATIVE_POWER_OFFSET: watts subtracted when total power is negative (default10.0)- An exact zero total is left unchanged.
- UDP RPC
UDP_PORTS: comma‑separated list (e.g.1010,2220) for old/new Shelly Pro 3EM stylesUDP_MAX: max UDP payload size (bytes)
- mDNS
MDNS_ENABLE:true|falseMDNS_HOSTNAME: instance name (defaults toDEVICE_ID)MDNS_IP: optional explicit IP to advertise (helps with multi‑homed hosts)
- Modbus TCP
MODBUS_ENABLE:true|falseMODBUS_PORT: TCP port for the Modbus server (default502)MODBUS_BIND: Bind address (default0.0.0.0)MODBUS_UNIT_ID: Unit identifier (default1)
- Payload shape
STRICT_MINIMAL_PAYLOAD: whentrue, HTTP/WSEM.GetStatusreturns only{a_act_power,b_act_power,c_act_power,total_act_power}(some gateways prefer this).
- Persistence
STATE_PATH: defaults to/data/state.json(mounted via volume in Compose).- Energy state and forecast observations are buffered in memory and flushed to disk once every 60 minutes to limit disk churn. A shutdown between flushes can therefore lose up to one hour of new data.
- Forecasting
FORECAST_ENABLE: enable history collection and forecasts (defaulttrue). Until a model is available, current readings are returned.FORECAST_HORIZON_STEPS: number of polling steps ahead to predict (default1; a step isPOLL_INTERVAL). Changing it invalidates the active model and triggers a complete cold retrain for the new target window.FORECAST_HISTORY_PATH: SQLite observation store (default/data/power_history.sqlite3); new observations are written in hourly batches.FORECAST_MODEL_DIR: promoted LightGBM models and metrics (default/data/forecast_model).FORECAST_TRAIN_HOUR: local hour for daily child-process training (default2).FORECAST_MIN_SAMPLES: minimum supervised samples required to train (default1000).FORECAST_WINDOW_SIZE: number of consecutive readings in the input vector used to make each prediction (default5). Changing it invalidates the active model and forces a cold retrain.FORECAST_VALIDATION_FRACTION: newest chronological fraction used for validation (default0.2).FORECAST_HISTORY_DAYS: observation retention period (default30).FORECAST_MAPE_FLOOR_WATTS: denominator floor used by MAPE around zero (default10).FORECAST_SERVE_INTERVAL: seconds between serving consecutive power consumers (default0.25). Requests are served in arrival order using the latest available reading instead of waiting for a new source poll.- Training uses all observations retained by
FORECAST_HISTORY_DAYS(30 days by default). Daily training warm-starts each phase model from the active LightGBM booster. A candidate is atomically promoted only when its mean phase validation MAPE is under 10% and beats the incumbent on the same validation slice. Until a qualifying model exists for the configured horizon and input window, current readings are returned as the fallback.
APIs
- HTTP JSON‑RPC (POST
/rpc): send a JSON‑RPC 2.0 request, e.g.{ "id": 1, "method": "EM.GetStatus", "params": {"id": 0} }
- HTTP GET RPC:
/rpc?method=EM.GetStatus&id=0/rpc/EM.GetStatus?id=0- Response is the method result object, e.g.
{ "a_act_power": 123.4, ... }
- WebSocket RPC:
- Connect
ws://<ip>/rpcand send the same JSON‑RPC envelopes as POST/rpc. - On connect you’ll receive a
NotifyFullStatus; during operationNotifyStatusmessages are broadcast.
- Connect
- Shelly‑style UDP RPC (b2500 compatible):
- Send to UDP port
1010or2220(configurable viaUDP_PORTS) - Request (example):
{"id":1,"src":"cli","method":"EM.GetStatus","params":{"id":0}} - Response (example):
{"id":1,"src":"<DEVICE_ID>","dst":"unknown","result":{"a_act_power":X.X,"b_act_power":Y.Y,"c_act_power":Z.Z,"total_act_power":T.TTT}} - Also supports
EM1.GetStatus->{ "result": { "act_power": T.TTT } } - If payload includes
"jsonrpc":"2.0", responds with JSON‑RPC envelope (fallback handler).
- Send to UDP port
Example Commands
- HTTP GET:
curl http://<ip>/shellycurl "http://<ip>/rpc/EM.GetStatus?id=0"
- HTTP POST JSON‑RPC:
curl -s http://<ip>/rpc -H 'Content-Type: application/json' -d '{"id":1,"method":"EM.GetStatus","params":{"id":0}}'
- WebSocket JSON‑RPC (using websocat):
websocat ws://<ip>/rpc- Then send:
{"id":1,"method":"EM.GetStatus","params":{"id":0}}
- UDP (netcat):
echo -n '{"id":1,"src":"cli","method":"EM.GetStatus","params":{"id":0}}' | nc -u -w1 <ip> 2220
Home Assistant polling
- Each
POLL_INTERVAL, HA sensors are fetched. Missing orunknown/unavailablevalues are treated asNone(or0.0for power). Phase power values feed energy integration (kWh) over time. - Power consumers are served one at a time, in arrival order, with
FORECAST_SERVE_INTERVALbetween them. Each consumer receives the latest available result immediately after that short delay; requests do not wait for a fresh source poll. - Energy counters persist to
STATE_PATH. You can reset counters via RPC:EMData.ResetCounters.
Shelly Web API polling
- When
INPUT_SOURCE=shelly_webapi, each poll auto-detects the API style per configured Shelly device. It first tries Gen2 RPC (/rpc/EM.GetStatus?id=0, fallback/rpc?method=EM.GetStatus&id=0) and falls back to legacy Gen1/emeter/0..2when needed. - For multiple devices (
SHELLY_BASE_URLS), per-phase values are aggregated:a/b/c_act_poweranda/b/c_current: summed across Shelly devices.a/b/c_voltageanda/b/c_pf: averaged across Shelly devices that returned values.
Shelly app notes
- Use host networking in Docker for best compatibility with mDNS/UDP. Ensure the phone and host are on the same subnet/VLAN.
- If discovery fails:
- Try adding by IP in the app.
- Set
MDNS_IPto the actual LAN IP and restart. - Ensure firewall allows UDP 5353 (mDNS) and the chosen UDP RPC ports (e.g., 1010/2220).
- Keep HTTP on port 80 (default in image) as some apps assume it.
Troubleshooting
- Check health:
curl http://<ip>/healthz(image includes a Docker HEALTHCHECK) - Inspect metrics:
curl http://<ip>/metrics - Open the UI: http:///ui (auto-refresh every 5s)
- Inspect logs:
docker logs -f shelly3em-virtual - Verify endpoints hit by the app (look for GET /shelly, /rpc calls, WS /rpc handshakes).
- Confirm UDP replies with netcat; try both 1010 and 2220 depending on your consumer.
Security
- Do not commit real HA tokens. Use a read‑only long‑lived token.
- This service is intended for trusted LANs. It provides unauthenticated endpoints by design to mimic Shelly devices.
Modbus register map (summary)
- Instantaneous values (float32, big-endian; accessible via holding or input registers):
3000/3001: Line frequency (Hz)3002-3007: Phase A/B/C voltages (V)3010-3015: Phase A/B/C currents (A)3020-3027: Phase A/B/C active power and total active power (W)3030-3035: Power factor for phases A/B/C
- Energy counters (float32 kWh):
3100-3105: Import energy per phase (A/B/C)3106-3111: Export energy per phase (A/B/C)3112/3113: Total import energy;3114/3115: Total export energy
- Diagnostics and metadata:
3200/3201: Current UNIX timestamp;3202/3203: uptime seconds3300+: Device ID, model, firmware, MAC encoded as ASCII (2 chars per register)3400: Device ready flag (1);3401: phase count (3)
- Commands:
- Write
1to holding register4200to reset accumulated energy counters.
- Write
Limitations
- Not a full Shelly implementation; only a subset of RPC is supported.
- Energy math is approximate and depends on poll timing from HA.
- mDNS behavior can vary across networks/containers; host networking is recommended.
License
- See repository license(s) of dependencies. This project is provided as‑is for personal/home use.
Shoutouts
- tomquist/b2500-meter: https://github.com/tomquist/b2500-meter
- This project’s Shelly-style UDP RPC behavior (EM.GetStatus/EM1.GetStatus formatting and decimal handling) is aligned for compatibility with b2500-meter.
- sdeigm/uni-meter: https://github.com/sdeigm/uni-meter
- A universal meter emulator inspiring the broader idea of adaptable meter emulation on the LAN.