From 9bc32d94180db0058c32c82fa8fd3551f1178fbc Mon Sep 17 00:00:00 2001 From: merryspankersltd <11749671+merryspankersltd@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:53:44 +0200 Subject: [PATCH] feat: add Shelly EM3 Pro / Pro 3EM 3-phase power meter New power meter package for the Shelly EM3 Pro / Pro 3EM (Gen2/Gen3 RPC API). Polls the meter over HTTP (`EM.GetStatus`) and publishes the arithmetic sum of the three phase active powers as the grid-exchange signal, matching how a 3-phase contract is billed (net metering). - adds solar_router/power_meter_shelly_em3.yaml (per-phase internal sensors, optional HTTP auth header) - adds docs (en/fr), install doc entry and mkdocs nav - adds esp32-em3-router.yaml example (triac + 1-dimmer engine) --- docs/en/installation.md | 2 + docs/en/power_meter_shelly_em3.md | 52 +++++++ docs/fr/installation.md | 2 + docs/fr/power_meter_shelly_em3.md | 52 +++++++ esp32-em3-router.yaml | 70 ++++++++++ mkdocs.yml | 1 + solar_router/power_meter_shelly_em3.yaml | 170 +++++++++++++++++++++++ 7 files changed, 349 insertions(+) create mode 100644 docs/en/power_meter_shelly_em3.md create mode 100644 docs/fr/power_meter_shelly_em3.md create mode 100644 esp32-em3-router.yaml create mode 100644 solar_router/power_meter_shelly_em3.yaml diff --git a/docs/en/installation.md b/docs/en/installation.md index 3dcad51..4d307be 100644 --- a/docs/en/installation.md +++ b/docs/en/installation.md @@ -27,6 +27,8 @@ A **proxy** just need 1 **power meter** package Get power data from Home Assistant sensor * [Shelly EM](power_meter_shelly_em.md) Get power data from a shelly EM +* [Shelly EM3 Pro / Pro 3EM](power_meter_shelly_em3.md) + Get 3-phase power data from a Shelly EM3 Pro / Pro 3EM * [Proxy client](power_meter_proxy_client.md) Get power data from outside from solar router diff --git a/docs/en/power_meter_shelly_em3.md b/docs/en/power_meter_shelly_em3.md new file mode 100644 index 0000000..964f3e2 --- /dev/null +++ b/docs/en/power_meter_shelly_em3.md @@ -0,0 +1,52 @@ +# Shelly EM3 Pro / Pro 3EM Power Meter + +This power meter is designed to get power consumption directly from a Shelly EM3 Pro / Pro 3EM three-phase energy meter over HTTP (Gen2/Gen3 "RPC" API). + +To use this package, add the following lines to your configuration file: + +```yaml linenums="1" +packages: + power_meter: + url: https://github.com/hacf-fr/Solar-Router-for-ESPHome/ + file: solar_router/power_meter_shelly_em3.yaml + vars: + power_meter_ip_address: "192.168.1.21" +``` + +This package needs to know the IP address of the Shelly EM3 Pro / Pro 3EM. This IP address has to be defined by `power_meter_ip_address` into `vars` section as shown above. + +!!! note "Per-phase sensors" + The package exposes the three per-phase active powers as internal sensors (for tuning). Set `show_phase_power: "True"` in `vars` to make them visible in Home Assistant. + +!!! note "HTTP Authentication Header" + This power meter allow to define HTTP Authentication Header with the variable `power_meter_auth_header`. + This variable can be set in `vars` section. + +!!! note "EM component id" + If the EM component id of the meter differs from the default, set `power_meter_emeter_id` in `vars`. + +## Three-phase sum + +On a three-phase contract the meter adds the three phases and bills only the **net** value. When one phase produces more (photovoltaic) than the other two consume, that's the right time to divert energy. + +This power meter therefore uses the *arithmetic sum* of the three phase active powers as the grid-exchange signal: + +``` +S_grid = a_act_power + b_act_power + c_act_power == total_act_power +``` + +* `+` sign : power is taken from the grid +* `-` sign : power is pushed back to the grid + +With routing on the 3-phase sum, the solar router diverts energy only when the whole installation is in surplus, never importing from the grid to run the load. + +This package is activated/deactivated with the variable `power_meter_activated`. By default, a power meter is deactivated at startup. The activation switch in home assistant determines if the power meter should be started or not. + +This power meter can be use in a proxy (a Solar Router only using a power meter). If this power meter is used in a proxy, it is required to activate it at startup by setting `power_meter_activated_at_start` to `1` in your yaml in the `vars` section defining the power meter configuration : + +```yaml linenums="1" +power_meter_activated_at_start: "1" +``` + +!!! warning "Network dependency" + This power meter require the network to gather information about energy exchanged with the grid. diff --git a/docs/fr/installation.md b/docs/fr/installation.md index 758beaf..c699e88 100644 --- a/docs/fr/installation.md +++ b/docs/fr/installation.md @@ -27,6 +27,8 @@ Un **proxy** n'a besoin que d'un package **power meter** Pour obtenir les données de puissance du capteur Home Assistant * [Shelly EM](power_meter_shelly_em.md) Pour obtenir les données de puissance d'un Shelly EM +* [Shelly EM3 Pro / Pro 3EM](power_meter_shelly_em3.md) + Pour obtenir les données de puissance triphasées d'un Shelly EM3 Pro / Pro 3EM * [Client Proxy](power_meter_proxy_client.md) Pour obtenir les données de puissance de l'extérieur du routeur solaire diff --git a/docs/fr/power_meter_shelly_em3.md b/docs/fr/power_meter_shelly_em3.md new file mode 100644 index 0000000..aeb63fd --- /dev/null +++ b/docs/fr/power_meter_shelly_em3.md @@ -0,0 +1,52 @@ +# Power meter Shelly EM3 Pro / Pro 3EM + +Ce *power meter* est conçu pour obtenir la consommation d'énergie directement à partir d'un compteur d'énergie triphasé Shelly EM3 Pro / Pro 3EM via HTTP (API Gen2/Gen3 "RPC"). + +Pour utiliser ce package, ajoutez les lignes suivantes à votre fichier de configuration : + +```yaml linenums="1" +packages: + power_meter: + url: https://github.com/hacf-fr/Solar-Router-for-ESPHome/ + file: solar_router/power_meter_shelly_em3.yaml + vars: + power_meter_ip_address: "192.168.1.21" +``` + +Ce package doit connaître l'adresse IP du Shelly EM3 Pro / Pro 3EM. L'adresse IP doit être définie par `power_meter_ip_address` dans la section `vars` de votre configuration, comme dans l'exemple ci-dessus. + +!!! note "Capteurs par phase" + Le package expose les trois puissances actives par phase en tant que capteurs internes (pour le réglage). Définissez `show_phase_power: "True"` dans `vars` pour les rendre visibles dans Home Assistant. + +!!! note "En-tête d'authentification HTTP" + Ce *power meter* permet de définir l'en-tête d'authentification HTTP avec la variable `power_meter_auth_header`. + Cette variable peut être définie dans la section `vars`. + +!!! note "Identifiant du composant EM" + Si l'identifiant du composant EM du compteur diffère de la valeur par défaut, définissez `power_meter_emeter_id` dans `vars`. + +## Somme triphasée + +Sur un abonnement triphasé, le compteur additionne les trois phases et ne facture que la valeur **nette**. Lorsqu'une phase produit plus (photovoltaïque) que les deux autres ne consomment, c'est le bon moment pour détourner l'énergie. + +Ce *power meter* utilise donc la *somme arithmétique* des trois puissances actives par phase comme signal d'échange avec le réseau : + +``` +S_grid = a_act_power + b_act_power + c_act_power == total_act_power +``` + +* signe `+` : l'énergie est prélevée du réseau +* signe `-` : l'énergie est réinjectée sur le réseau + +Avec le détournement sur la somme triphasée, le routeur solaire ne détourne l'énergie que lorsque toute l'installation est en surplus, sans jamais prélever du réseau pour alimenter la charge. + +Ce package est activé/désactivé à l'aide d'une variable globale `power_meter_activated`. Par défaut, un compteur de puissance est désactivé au démarrage. L'interrupteur d'activation dans home assistant détermine si le compteur de puissance doit être démarré ou non. + +Si ce compteur de puissance est utilisé à l'intérieur d'un proxy, il est nécessaire de l'activer au démarrage en définissant `power_meter_activated_at_start` à `1` dans votre fichier de configuration yaml, comme dans l'exemple ci-dessous : + +```yaml linenums="1" +power_meter_activated_at_start: "1" +``` + +!!! warning "Dépendance réseau" + Ce *power meter* nécessite le réseau pour recueillir des informations sur l'énergie échangée avec le réseau électrique. \ No newline at end of file diff --git a/esp32-em3-router.yaml b/esp32-em3-router.yaml new file mode 100644 index 0000000..73eac70 --- /dev/null +++ b/esp32-em3-router.yaml @@ -0,0 +1,70 @@ +# ---------------------------------------------------------------------------------------------------- +# ESPHome configuration - This part depends on your hardware target +# ---------------------------------------------------------------------------------------------------- +# Example: ESP32-based solar router using a 3-phase Shelly EM3 Pro / Pro 3EM as +# the power meter (progressive triac dimmer + 1-dimmer engine). +# ---------------------------------------------------------------------------------------------------- + +esphome: + name: em3-router + friendly_name: EM3 Router + min_version: 2025.5.0 + +esp32: + board: esp32dev + framework: + type: esp-idf + +# Enable logging +logger: + baud_rate: 115200 + level: INFO + logs: + component: ERROR + light: ERROR + +# Enable Home Assistant API +api: + encryption: + key: !secret api_encryption_key + +# Enable over-the-air updates +ota: + - platform: esphome + password: !secret solar_router_ota_password + +# WiFi connection +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + reboot_timeout: 24h + +# Activate web interface +web_server: + port: 80 + +# Define watchdog time (it should be greater than timeout) +http_request: + watchdog_timeout: 12s + +# ---------------------------------------------------------------------------------------------------- +# Customisation +# ---------------------------------------------------------------------------------------------------- +packages: + solar_router: + url: https://github.com/hacf-fr/Solar-Router-for-ESPHome/ + ref: main + refresh: 1d + files: + - path: solar_router/common.yaml + - path: solar_router/power_meter_shelly_em3.yaml + vars: + power_meter_ip_address: "192.168.1.42" + - path: solar_router/regulator_triac.yaml + vars: + regulator_gate_pin: GPIO17 + regulator_zero_crossing_pin: GPIO23 + - path: solar_router/engine_1dimmer.yaml + vars: + green_led_pin: GPIO18 + yellow_led_pin: GPIO19 \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 036596d..cf90b1e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ nav: - JSY-MK-194T: power_meter_jsy-mk-194t.md - Proxy client: power_meter_proxy_client.md - Shelly EM: power_meter_shelly_em.md + - Shelly EM3 Pro / Pro 3EM: power_meter_shelly_em3.md - Regulator: - Triac: regulator_triac.md - Solid State Relay: regulator_solid_state_relay.md diff --git a/solar_router/power_meter_shelly_em3.yaml b/solar_router/power_meter_shelly_em3.yaml new file mode 100644 index 0000000..b8c2bae --- /dev/null +++ b/solar_router/power_meter_shelly_em3.yaml @@ -0,0 +1,170 @@ +<<: !include power_meter_common.yaml + +esphome: + min_version: 2025.5.0 + +# ---------------------------------------------------------------------------------------------------- +# Shelly EM3 Pro / Pro 3EM power meter (Gen2/Gen3 "RPC" API) +# ---------------------------------------------------------------------------------------------------- +# Polls the Shelly three-phase energy meter over HTTP (`EM.GetStatus`) and uses +# the *arithmetic sum* of the three phase active powers as the grid-exchange +# signal: +# +# S_grid = a_act_power + b_act_power + c_act_power == total_act_power +# +# This matches the way a three-phase contract is billed (net metering): +# the three phases are added up and only the net value is billed, so an export +# of -1200 W on phase C fully offsets a (+500 W) + (+800 W) import on A+B. +# +# + sign : power is taken from the grid +# - sign : power is pushed back to the grid +# +# With routing on the 3-phase sum, the router diverts energy only when the +# *whole installation* is in surplus, never importing "from the front door" to +# run the load. Phase-balance/per-phase safety is naturally satisfied because +# simultaneous phase imbalances cannot make the sum go negative. +# +# Package variables (vars): +# power_meter_ip_address (required) IP / hostname of the Shelly EM3 Pro. +# power_meter_emeter_id (optional) EM component id, default "0". +# power_meter_auth_header (optional) HTTP Authorization header (Basic auth). +# show_phase_power (optional) expose per-phase W sensors, default "False". + +substitutions: + # Same defaults as power_meter_common.yaml - re-declared here because a + # substitution block overriding the merged one replaces it entirely. + power_meter_activated_at_start: "0" + power_sign: "1" + # The Shelly EM3 only exposes a NET power (total_act_power): the per-phase + # "consumption" template below is never written in this package, so it stayed + # stuck at "unknown" in HA. Hide it instead of shipping dead weight. + consumption_sensor_internal: "true" + # This package specifics + power_meter_emeter_id: "0" + power_meter_auth_header: "" + show_phase_power: "False" + +http_request: + id: em3_http_request + useragent: esphome/device + timeout: 5s + verify_ssl: False + +script: + # Poll the Em3 meter and publish the grid-exchange power (total_act_power) + - id: power_meter_source + mode: single + then: + - if: + condition: + lambda: 'return network::is_connected();' + then: + - http_request.get: + url: 'http://${power_meter_ip_address}/rpc/EM.GetStatus?id=${power_meter_emeter_id}' + request_headers: + Content-Type: application/json + Authorization: ${power_meter_auth_header} + capture_response: true + max_response_buffer_size: 8192 + on_response: + then: + - logger.log: + format: 'EM3 response status: %d, duration: %u ms' + args: + - response->status_code + - response->duration_ms + - lambda: |- + if (response->status_code != 200) { + ESP_LOGW("em3", "EM3 request failed with status: %d", response->status_code); + id(real_power).publish_state(NAN); + } else { + bool parse_success = json::parse_json(body, [](JsonObject root) -> bool { + if (!root["total_act_power"].is()) { + ESP_LOGW("em3", "EM3 response missing 'total_act_power'"); + return false; + } + id(real_power).publish_state(root["total_act_power"].as() * ${power_sign}); + + if (root["a_act_power"].is()) { + id(em3_phase_a_power).publish_state(root["a_act_power"].as()); + } + if (root["b_act_power"].is()) { + id(em3_phase_b_power).publish_state(root["b_act_power"].as()); + } + if (root["c_act_power"].is()) { + id(em3_phase_c_power).publish_state(root["c_act_power"].as()); + } + return true; + }); + + if (!parse_success) { + id(real_power).publish_state(NAN); + } + } + on_error: + then: + - logger.log: + format: 'EM3 request failed or timeout' + level: WARN + - lambda: |- + id(real_power).publish_state(NAN); + +# Sense the per-phase active powers for debugging / tuning (hidden by default) +sensor: + # Re-declared from power_meter_common.yaml: the '<<' merge only keeps the + # FIRST definition of a key, and this file defines its own 'sensor:' section. + # The engine depends on these two template sensors. + - id: real_power + platform: template + name: "Real Power" + device_class: "power" + unit_of_measurement: "W" + update_interval: 1s + + - id: consumption + platform: template + name: "Consumption" + device_class: "power" + unit_of_measurement: "W" + update_interval: 1s + internal: ${consumption_sensor_internal} + + - platform: template + id: em3_phase_a_power + name: "EM3 Phase A Active Power" + device_class: "power" + unit_of_measurement: "W" + internal: ${show_phase_power} + update_interval: never + - platform: template + id: em3_phase_b_power + name: "EM3 Phase B Active Power" + device_class: "power" + unit_of_measurement: "W" + internal: ${show_phase_power} + update_interval: never + - platform: template + id: em3_phase_c_power + name: "EM3 Phase C Active Power" + device_class: "power" + unit_of_measurement: "W" + internal: ${show_phase_power} + update_interval: never + +time: + # Poll cadence note: + # Polling every second (/1) opens/closes an esp_http_client connection every + # second; over hours this can provoke a stale-transport use-after-free inside + # ESP-IDF's tcp_transport (LoadProhibited in base_poll_read / transport_ssl.c, + # "CRASH DETECTED ON PREVIOUS BOOT"). We therefore poll every 2 seconds (/2): + # ~2x less connection churn, which sacrifices ~1s of reaction time. Revisit + # if a future ESPHome release fixes the http_request/IDF transport reuse race. + - platform: sntp + on_time: + - seconds: /2 + then: + - if: + condition: + - lambda: return id(power_meter_activated) != 0; + then: + - script.execute: power_meter_source \ No newline at end of file