diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e9dc20a..8896d6a1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
## Next
-- **Added** experimental ESPHome external component `ct002` to run the CT002/CT003 emulator directly on an ESP32. See `esphome.example.yaml`.
+- **Added** experimental ESPHome external component `ct002` to run the CT002/CT003 emulator directly on an ESP32. See `esphome.example.yaml` and the per-meter grid-power sensor reference in `docs/esphome-powermeters.md` (which also lists meters not yet supported on the ESP, e.g. Enphase Envoy and the SMA Energy Meter). Per-source `config.ini` documentation moved out of the README into `docs/powermeters.md`.
- **Added** Modbus UDP support via a `TRANSPORT = TCP|UDP` option in the `[MODBUS]` section (defaults to `TCP`).
- **Fixed** Home Assistant powermeter timing out on startup with "Timeout waiting for Home Assistant state" when the configured sensor hasn't changed value since AstraMeter started.
- **Fixed** `DEVICE_TYPE = shellypro3em_new` (and `shellypro3em_old`) generating an invalid `device-N` source id that the B2500 rejected, so the CT was never detected. These variants now default to a proper `shellypro3em-*` id like the combined `shellypro3em` type ([#389](https://github.com/tomquist/astrameter/issues/389)).
diff --git a/README.md b/README.md
index 9d1efebc..a960a72b 100644
--- a/README.md
+++ b/README.md
@@ -170,7 +170,7 @@ ct002:
power_sensor_l1: grid_l1
```
-Everything else is optional. See **[`esphome.example.yaml`](esphome.example.yaml)** for the complete, annotated config — three-phase sensors, the cross-phase filter pipeline (Hampel / smoothing / deadband / PID), balancer and saturation tuning, and the two optional sub-blocks below — with every knob shown at its default.
+Everything else is optional. See **[`esphome.example.yaml`](esphome.example.yaml)** for the complete, annotated config — three-phase sensors, the cross-phase filter pipeline (Hampel / smoothing / deadband / PID), balancer and saturation tuning, and the two optional sub-blocks below — with every knob shown at its default. For the grid-power `sensor:` configuration per meter type (and which meters aren't supported on the ESP yet), see **[docs/esphome-powermeters.md](docs/esphome-powermeters.md)**.
Two optional sub-blocks nest under the same `ct002:` key:
@@ -191,7 +191,7 @@ For details on the CT002/CT003 UDP protocol used by Marstek storage systems, see
## Configuration
-Configuration is managed via `config.ini`. Each powermeter type has specific settings.
+Configuration is managed via `config.ini`. Each powermeter type has specific settings — see the per-source reference in **[docs/powermeters.md](docs/powermeters.md)** (and **[docs/esphome-powermeters.md](docs/esphome-powermeters.md)** for the ESPHome external component).
### General Configuration
@@ -459,422 +459,23 @@ PID_OUTPUT_MAX = 800
PID_MODE = bias
```
-### Shelly
+### Powermeter sources
-#### Shelly 1PM
-```ini
-[SHELLY]
-TYPE = 1PM
-IP = 192.168.1.100
-USER = username
-PASS = password
-METER_INDEX = meter1
-```
-
-#### Shelly Plus 1PM
-```ini
-[SHELLY]
-TYPE = PLUS1PM
-IP = 192.168.1.100
-USER = username
-PASS = password
-METER_INDEX = meter1
-```
-
-#### Shelly EM
-```ini
-[SHELLY]
-TYPE = EM
-IP = 192.168.1.100
-USER = username
-PASS = password
-METER_INDEX = meter1
-```
-
-#### Shelly 3EM
-```ini
-[SHELLY]
-TYPE = 3EM
-IP = 192.168.1.100
-USER = username
-PASS = password
-METER_INDEX = meter1
-```
-
-#### Shelly 3EM Pro
-```ini
-[SHELLY]
-TYPE = 3EMPro
-IP = 192.168.1.100
-USER = username
-PASS = password
-METER_INDEX = meter1
-```
-
-### Tasmota
-
-```ini
-[TASMOTA]
-IP = 192.168.1.101
-USER = tasmota_user
-PASS = tasmota_pass
-JSON_STATUS = StatusSNS
-JSON_PAYLOAD_MQTT_PREFIX = SML
-JSON_POWER_MQTT_LABEL = Power
-JSON_POWER_INPUT_MQTT_LABEL = Power1
-JSON_POWER_OUTPUT_MQTT_LABEL = Power2
-JSON_POWER_CALCULATE = True
-```
-
-For 3-phase meters, use comma-separated labels:
-
-```ini
-[TASMOTA]
-IP = 192.168.1.101
-JSON_STATUS = StatusSNS
-JSON_PAYLOAD_MQTT_PREFIX = eBZ
-JSON_POWER_MQTT_LABEL = Power_L1,Power_L2,Power_L3
-```
-
-For 3-phase with `JSON_POWER_CALCULATE`, provide matching comma-separated
-input and output labels (counts must be equal):
-
-```ini
-[TASMOTA]
-IP = 192.168.1.101
-JSON_STATUS = StatusSNS
-JSON_PAYLOAD_MQTT_PREFIX = SML
-JSON_POWER_INPUT_MQTT_LABEL = Power_In_L1,Power_In_L2,Power_In_L3
-JSON_POWER_OUTPUT_MQTT_LABEL = Power_Out_L1,Power_Out_L2,Power_Out_L3
-JSON_POWER_CALCULATE = True
-```
-
-### Shrdzm
-
-```ini
-[SHRDZM]
-IP = 192.168.1.102
-USER = shrdzm_user
-PASS = shrdzm_pass
-```
-
-### Emlog
-
-```ini
-[EMLOG]
-IP = 192.168.1.103
-METER_INDEX = 0
-JSON_POWER_CALCULATE = True
-```
-
-### IoBroker
-
-```ini
-[IOBROKER]
-IP = 192.168.1.104
-PORT = 8087
-CURRENT_POWER_ALIAS = Alias.0.power
-POWER_CALCULATE = True
-POWER_INPUT_ALIAS = Alias.0.power_in
-POWER_OUTPUT_ALIAS = Alias.0.power_out
-```
-
-### HomeAssistant
-```ini
-[HOMEASSISTANT]
-IP = 192.168.1.105
-PORT = 8123
-# Use HTTPS - if empty False is Fallback
-HTTPS = ""|True|False
-ACCESSTOKEN = YOUR_ACCESS_TOKEN
-# The entity or entities (comma-separated for 3-phase) that provide current power
-CURRENT_POWER_ENTITY = ""|sensor.current_power|sensor.phase1,sensor.phase2,sensor.phase3
-# If False or Empty the power is not calculated - if empty False is Fallback
-POWER_CALCULATE = ""|True|False
-# The entity ID or IDs (comma-separated for 3-phase) that provide power input
-POWER_INPUT_ALIAS = ""|sensor.power_input|sensor.power_in_1,sensor.power_in_2,sensor.power_in_3
-# The entity ID or IDs (comma-separated for 3-phase) that provide power output
-POWER_OUTPUT_ALIAS = ""|sensor.power_output|sensor.power_out_1,sensor.power_out_2,sensor.power_out_3
-# Is a Path Prefix needed?
-API_PATH_PREFIX = ""|/core
-# Per-powermeter throttling override (recommended: 2-3 seconds for HomeAssistant)
-THROTTLE_INTERVAL = 2
-```
-
-Example: Variant 1 with a single combined input & output sensor
-```ini
-[HOMEASSISTANT]
-IP = 192.168.1.105
-PORT = 8123
-HTTPS = True
-ACCESSTOKEN = YOUR_ACCESS_TOKEN
-CURRENT_POWER_ENTITY = sensor.current_power
-```
-
-Example: Variant 2 with separate input & output sensors
-```ini
-[HOMEASSISTANT]
-IP = 192.168.1.105
-PORT = 8123
-HTTPS = True
-ACCESSTOKEN = YOUR_ACCESS_TOKEN
-POWER_CALCULATE = True
-POWER_INPUT_ALIAS = sensor.power_input
-POWER_OUTPUT_ALIAS = sensor.power_output
-```
-
-Example: Variant 3 with three-phase power monitoring
-```ini
-[HOMEASSISTANT]
-IP = 192.168.1.105
-PORT = 8123
-HTTPS = True
-ACCESSTOKEN = YOUR_ACCESS_TOKEN
-CURRENT_POWER_ENTITY = sensor.phase1,sensor.phase2,sensor.phase3
-```
-
-Example: Variant 4 with three-phase power calculation
-```ini
-[HOMEASSISTANT]
-IP = 192.168.1.105
-PORT = 8123
-HTTPS = True
-ACCESSTOKEN = YOUR_ACCESS_TOKEN
-POWER_CALCULATE = True
-POWER_INPUT_ALIAS = sensor.power_in_1,sensor.power_in_2,sensor.power_in_3
-POWER_OUTPUT_ALIAS = sensor.power_out_1,sensor.power_out_2,sensor.power_out_3
-# Per-powermeter throttling override (recommended: 2-3 seconds for HomeAssistant)
-# THROTTLE_INTERVAL = 2
-```
-
-### VZLogger
-
-```ini
-[VZLOGGER]
-IP = 192.168.1.106
-PORT = 8080
-UUID = your-uuid
-```
-
-For 3-phase meters, provide comma-separated UUIDs (one per phase); phases are
-fetched in parallel:
-
-```ini
-[VZLOGGER]
-IP = 192.168.1.106
-PORT = 8080
-UUID = uuid-l1, uuid-l2, uuid-l3
-```
-
-### ESPHome
-
-```ini
-[ESPHOME]
-IP = 192.168.1.107
-PORT = 6052
-DOMAIN = your_domain
-ID = your_id
-```
-
-### AMIS Reader
-
-```ini
-[AMIS_READER]
-IP = 192.168.1.108
-```
-
-### Modbus TCP/UDP
-
-```ini
-[MODBUS]
-HOST = 192.168.1.100
-PORT = 502
-UNIT_ID = 1
-ADDRESS = 0
-COUNT = 1
-DATA_TYPE = UINT16
-BYTE_ORDER = BIG
-WORD_ORDER = BIG
-REGISTER_TYPE = HOLDING # or INPUT
-TRANSPORT = TCP # or UDP
-```
-
-### MQTT
-
-```ini
-[MQTT]
-BROKER = broker.example.com
-PORT = 1883
-TOPIC = home/powermeter
-JSON_PATH = $.path.to.value (Optional for JSON payloads)
-USERNAME = mqtt_user (Optional)
-PASSWORD = mqtt_pass (Optional)
-# Optional: connect over TLS (mqtts://) — default false
-# TLS = false
-# Per-powermeter throttling override
-# THROTTLE_INTERVAL = 2
-```
-
-Instead of `BROKER`/`PORT`/`USERNAME`/`PASSWORD`/`TLS`, you can provide a single `URI` of the form `mqtt[s]://[user[:pass]@]host[:port]` (use `mqtts://` for TLS; credentials and port are optional). When `URI` is set, the individual `BROKER`/`PORT`/`USERNAME`/`PASSWORD`/`TLS` fields are ignored.
-
-```ini
-[MQTT]
-URI = mqtts://user:pass@broker.example.com:8883
-TOPIC = home/powermeter
-```
-
-The `JSON_PATH` option is used to extract the power value from a JSON payload. The path must be a [valid JSONPath expression](https://goessner.net/articles/JsonPath/).
-If the payload is a simple integer value, you can omit this option.
-
-Both `JSON_PATH` and `JSON_PATHS` are parsed with the [`jsonpath-ng` extended syntax](https://github.com/h2non/jsonpath-ng#extensions), so you can chain extensions like `` `split(...)` `` or `` `sub(/regex/, replacement)` `` to massage a payload value before it's converted to a float — for instance `$.state.`split( , 0, -1)`` or `$.state.`sub(/[^0-9.\-]+$/, )`` to strip a unit suffix like `"331.74 W"`. See the [JSON HTTP](#json-http) section below for more examples.
-
-#### Multi-phase MQTT
-
-For three-phase setups, there are two options:
-
-**Option 1: Multiple topics** — one topic per phase, each publishing a plain numeric value (or JSON with the same path):
-
-```ini
-[MQTT]
-BROKER = broker.example.com
-TOPICS = home/power/l1, home/power/l2, home/power/l3
-```
-
-**Option 2: Single topic with multiple JSON paths** — one topic publishing a JSON message containing all phases:
-
-```ini
-[MQTT]
-BROKER = broker.example.com
-TOPIC = home/powermeter
-JSON_PATHS = $.phases[0].power, $.phases[1].power, $.phases[2].power
-```
-
-`TOPICS` takes precedence over `TOPIC`, and `JSON_PATHS` takes precedence over `JSON_PATH`. You can combine `TOPICS` with `JSON_PATH` (same path applied to each topic) or with `JSON_PATHS` (one path per topic, counts must match).
-
-### JSON HTTP
-
-```ini
-[JSON_HTTP]
-URL = http://example.com/api
-# Comma separated JSON paths - single path for 1-phase or three for 3-phase
-JSON_PATHS = $.power
-USERNAME = user (Optional)
-PASSWORD = pass (Optional)
-# Additional headers separated by ';' using 'Key: Value'
-HEADERS = Authorization: Bearer token
-```
-
-`JSON_PATHS` is parsed with the [`jsonpath-ng` extended syntax](https://github.com/h2non/jsonpath-ng#extensions), so you can chain extensions like `` `split(...)` `` or `` `sub(/regex/, replacement)` `` to massage the value before it's converted to a float. For example, an openHAB `Number:Power` item returns `"331.74 W"` — strip the unit with either of:
-
-```ini
-JSON_PATHS = $.state.`split( , 0, -1)`
-JSON_PATHS = $.state.`sub(/[^0-9.\-]+$/, )`
-```
-
-### TQ Energy Manager
-
-```ini
-[TQ_EM]
-IP = 192.168.1.100
-#PASSWORD = pass
-#TIMEOUT = 5.0 (Optional)
-```
-
-### HomeWizard
-
-Reads a [HomeWizard](https://www.homewizard.com/) P1 dongle (or compatible device) over the local **WebSocket** API (`wss://`). Obtain a token once via `POST /api/user` while confirming on the device; see the [HomeWizard API docs](https://api-documentation.homewizard.com/docs/v2/).
-
-```ini
-[HOMEWIZARD]
-IP = 192.168.1.110
-TOKEN = YOUR_32_CHAR_HEX_TOKEN
-SERIAL = your_device_serial
-# Optional: disable TLS certificate verification on a trusted LAN if verification fails (default True)
-# VERIFY_SSL = True
-# THROTTLE_INTERVAL = 0
-```
-
-### Enphase Envoy (IQ Gateway)
-
-Reads grid power from an [Enphase IQ Gateway / Envoy](https://enphase.com/installers/microinverters/iq-gateway) over the local HTTPS API (`/production.json?details=1`). The reading comes from the `net-consumption` measurement (positive = grid import, negative = export). Per-phase readings are reported automatically when the gateway exposes them; otherwise the aggregate single-phase value is used. Requires consumption CTs installed on the Envoy.
-
-```ini
-[ENVOY]
-HOST = 192.168.1.120
-# Option A: pre-obtained long-lived JWT (recommended)
-TOKEN = eyJ...
-# Option B: let AstraMeter fetch and refresh tokens via the Enphase Enlighten cloud
-# USERNAME = you@example.com
-# PASSWORD = your-enphase-password
-# SERIAL = 123456789012
-# Envoy ships a self-signed certificate; verification is disabled by default.
-# VERIFY_SSL = False
-```
-
-**Token acquisition.** Generate a long-lived (~1 year) static token at . Alternatively, configure `USERNAME`/`PASSWORD`/`SERIAL` and AstraMeter will fetch a token on first use and refresh it automatically when the Envoy returns 401.
-
-**TLS.** `VERIFY_SSL` defaults to `False` because Enphase does not publish a CA bundle for the IQ Gateway's self-signed certificate. This option **only affects the local Envoy connection** — Enphase Enlighten cloud requests (login and token endpoints) always verify TLS using the system trust store, regardless of this setting.
-
-**MFA.** The auto-fetch flow does not support Enlighten accounts with multi-factor authentication enabled. Those users must supply a static `TOKEN`.
-
-**CT direction.** If your readings have the wrong sign (export shows as import or vice versa), one or more CTs are mounted backwards. Flip them in software with the global `POWER_MULTIPLIER = -1` (or per-phase, e.g. `POWER_MULTIPLIER = 1, -1, 1`).
-
-### SMA Energy Meter
-
-Reads an [SMA Energy Meter](https://www.sma.de/) (EM 1.0/2.0) or Sunny Home Manager via the **Speedwire** multicast protocol (UDP). The listener joins the default multicast group and reports per-phase active power (L1, L2, L3). Use `SERIAL_NUMBER = 0` to auto-detect the first meter seen on the network, or set the device serial to pin a specific unit. Like other UDP-based features, this requires the host to receive multicast traffic (use Docker host networking or equivalent).
-
-```ini
-[SMA_ENERGY_METER]
-MULTICAST_GROUP = 239.12.255.254
-PORT = 9522
-SERIAL_NUMBER = 0
-# INTERFACE = 192.168.1.10
-# THROTTLE_INTERVAL = 0
-```
-
-### Modbus
-
-```ini
-[MODBUS]
-HOST =
-PORT =
-UNIT_ID =
-ADDRESS =
-COUNT =
-DATA_TYPE = UINT16
-BYTE_ORDER = BIG
-WORD_ORDER = BIG
-REGISTER_TYPE = HOLDING
-TRANSPORT = TCP
-```
-
-`TRANSPORT` selects the Modbus transport: `TCP` (default) or `UDP`.
-
-### Script
-
-You can also use a custom script to get the power values. The script should output at most 3 integer values, separated by a line break.
-```ini
-[SCRIPT]
-COMMAND = /path/to/your/script.sh
-```
-
-### SML
-
-```ini
-[SML]
-SERIAL = /dev/ttyUSB0
-# Optional: override default OBIS hex registers (12 hex digits each; defaults match common German eHZ meters)
-#OBIS_POWER_CURRENT = 0100100700ff
-#OBIS_POWER_L1 = 0100240700ff
-#OBIS_POWER_L2 = 0100380700ff
-#OBIS_POWER_L3 = 01004c0700ff
-```
-
-Read from a powermeter that is connected via USB and that transmits SML (Smart Message Language) data via an IR head. **`SERIAL` is required**: local device path to the serial interface (e.g. `/dev/ttyUSB0` on Linux).
+The per-source configuration for every supported meter lives in dedicated
+reference docs — find your meter and copy the matching section:
-**Multi-phase:** If the meter exposes per-phase instantaneous active power for L1–L3 (`Summenwirkleistung` / default OBIS above), those three values are used automatically. Otherwise the aggregate instantaneous power register (`aktuelle Wirkleistung` / `OBIS_POWER_CURRENT`) is used as a single reading. When both are present in the same SML list, per-phase values take precedence.
+- **[docs/powermeters.md](docs/powermeters.md)** — `config.ini` sections for the
+ Python add-on / Docker / direct install (Shelly, Tasmota, Shrdzm, Emlog,
+ IoBroker, HomeAssistant, VZLogger, ESPHome, AMIS Reader, Modbus, MQTT, JSON
+ HTTP, TQ Energy Manager, HomeWizard, Enphase Envoy, SMA Energy Meter, Script,
+ SML).
+- **[docs/esphome-powermeters.md](docs/esphome-powermeters.md)** — the equivalent
+ grid-power `sensor:` configuration when running the
+ [ESPHome external component](#esphome-external-component-run-on-an-esp32) on an
+ ESP32, including which meters aren't supported on the ESP yet.
-**OBIS overrides:** Only needed if your meter uses different register addresses; values must be exactly 12 hexadecimal characters (lowercase or uppercase).
+The value transformation, PID controller, and per-powermeter options
+(throttling, smoothing, deadband, Hampel) documented above apply to every source.
### Multiple Powermeters
diff --git a/docs/esphome-powermeters.md b/docs/esphome-powermeters.md
new file mode 100644
index 00000000..407850f3
--- /dev/null
+++ b/docs/esphome-powermeters.md
@@ -0,0 +1,820 @@
+# Powermeter Configuration Reference (ESPHome external component)
+
+When you run AstraMeter as the [ESPHome external
+component](../README.md#esphome-external-component-run-on-an-esp32) on an ESP32,
+the `ct002:` block does **not** talk to your meter directly. Instead it consumes
+**any ESPHome `sensor`** that reports grid power in watts. So "configuring a
+powermeter" here means: *give ESPHome a sensor that reads your meter, then point
+`ct002:` at it.*
+
+## How a reading reaches the emulator
+
+There is no "powermeter" object in the ESPHome component — the integration is a
+plain **id reference**. Every example below publishes a watts value into a sensor
+whose `id` is `grid_l1` (and `grid_l2` / `grid_l3` for the other phases). The
+`ct002:` block names those ids in its `power_sensor_l*` keys. That id match is
+the entire wiring; whenever the sensor publishes a new value, the emulator picks
+it up on its next Marstek CT002 poll — you never call `ct002:` directly.
+
+**Each section below is a complete, copy-pasteable config** for one meter — from
+`external_components:` through `ct002:`. To keep them focused, every example
+**omits the `wifi:`, `api:`, `ota:`, and board (`esp32:`) blocks** — add those
+for your hardware (see [`esphome.example.yaml`](../esphome.example.yaml) for a
+full board config). What's shown is complete for the meter → emulator wiring.
+
+Per-phase calibration/throttling (`offset:`, `multiply:`, `throttle:`) goes in
+`filters:` **on the sensor**, not in `ct002:` — see the
+[main README note](../README.md#esphome-external-component-run-on-an-esp32).
+Running the Python add-on instead? See [powermeters.md](powermeters.md).
+
+> The polling/lambda examples are **illustrative**. ESPHome's `http_request`,
+> `json`, and lambda APIs differ slightly between releases — check the linked
+> component docs for the exact syntax on your version.
+
+## Support legend
+
+| Tier | Meaning |
+|------|---------|
+| 🟢 **Native** | A built-in ESPHome component reads this exact source. |
+| 🔵 **Generic** | No device-specific component, but ESPHome's built-in `http_request`+`json` or `mqtt_subscribe` reads it with a small lambda. |
+| 🟠 **Alternate** | The exact API the Python class uses has no ESPHome port, but the *same device* also speaks a protocol ESPHome reads natively (Modbus/MQTT/P1). |
+| 🔴 **Not yet available** | No practical way to read this on an ESP32 today. Documented so we know what to build. |
+
+## Contents
+
+- [Shelly](#shelly) — 🔵 Generic (or 🟢 native if you flash the Shelly)
+- [Tasmota](#tasmota) — 🔵 Generic (or 🟢 native if you flash the device)
+- [Shrdzm](#shrdzm) — 🔵 Generic
+- [Emlog](#emlog) — 🔵 Generic
+- [IoBroker](#iobroker) — 🔵 Generic
+- [HomeAssistant](#homeassistant) — 🟢 Native
+- [VZLogger](#vzlogger) — 🔵 Generic (or 🟢 native by reading the meter directly)
+- [ESPHome](#esphome) — 🟢 Native (it's already ESPHome)
+- [AMIS Reader](#amis-reader) — 🔵 Generic
+- [Modbus](#modbus) — 🟢 Native (RS485 serial; see TCP caveat)
+- [MQTT](#mqtt) — 🟢 Native
+- [JSON HTTP](#json-http) — 🟢 Native (generic `http_request`)
+- [SML](#sml) — 🟢 Native
+- [TQ Energy Manager](#tq-energy-manager) — 🟠 Alternate (Modbus/MQTT)
+- [HomeWizard](#homewizard) — 🟠 Alternate (local v1 HTTP, or native P1)
+- [Enphase Envoy (IQ Gateway)](#enphase-envoy-iq-gateway) — 🔴 Not yet available
+- [SMA Energy Meter](#sma-energy-meter) — 🔴 Not yet available
+
+> **Script** (the Python `[SCRIPT]` source) has no ESPHome equivalent by design —
+> an ESP32 can't run a host shell command — so it is intentionally omitted here.
+>
+> **The 🔵 generic HTTP sections** all share the same shape: a `template` sensor
+> named `grid_l1` holds the value, an `interval:` polls the URL, and a lambda
+> parses the JSON body with the built-in
+> [`json::parse_json`](https://esphome.io/components/json/) helper and publishes
+> into `grid_l1`. Only the URL and the lambda field differ between them. The
+> [`http_request`](https://esphome.io/components/http_request/) and
+> [`json`](https://esphome.io/components/json/) components are built in — no
+> extra external component needed.
+
+## Shelly
+
+**Tier: 🔵 Generic** (poll over the network) — or **🟢 Native** if the Shelly is
+ESP32-based and you flash ESPHome onto it.
+
+Most Shelly devices are reachable over HTTP from the ct002 ESP32. Gen2/Gen3/Pro
+expose an RPC API; Gen1 a REST `/status`. Single-phase (Shelly Plus 1PM / Pro
+family, RPC `apower`):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.100/rpc/Switch.GetStatus?id=0
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["apower"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+Three-phase (Shelly 3EM Pro, RPC `EM.GetStatus`) — three template sensors, one
+poll, all three phases on `ct002:`:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+ - platform: template
+ id: grid_l2
+ unit_of_measurement: W
+ device_class: power
+ - platform: template
+ id: grid_l3
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.100/rpc/EM.GetStatus?id=0
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["a_act_power"]);
+ id(grid_l2).publish_state(root["b_act_power"]);
+ id(grid_l3).publish_state(root["c_act_power"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+ power_sensor_l2: grid_l2
+ power_sensor_l3: grid_l3
+```
+
+(This splits `EM.GetStatus` into per-phase readings; the Python `[SHELLY]`
+`3EMPro` source instead reads the aggregate `total_act_power` from the same
+response. Both are valid — use whichever your setup needs.)
+
+Gen1 (Shelly 1PM/EM/3EM) expose `http:///status` with a `meters[]` /
+`emeters[]` array — point the lambda at `root["emeters"][0]["power"]` etc.
+
+**Native alternative:** Shelly hardware is ESP-based, so you can flash ESPHome
+directly onto it and read its onboard energy chip (BL0942 / ADE7953 / ADE7880)
+as a native sensor. If that Shelly is ESP32-based (e.g. Shelly Pro 3EM) it can
+even run the `ct002:` component itself. See
+[devices.esphome.io](https://devices.esphome.io/) for per-model configs.
+
+## Tasmota
+
+**Tier: 🔵 Generic** — or **🟢 Native** if you flash ESPHome onto the device.
+
+Tasmota answers `GET /cm?cmnd=status%2010` with sensor JSON nested under
+`StatusSNS`. Adapt the prefix/label to your meter (here `SML`/`Power`):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.101/cm?cmnd=status%2010
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["StatusSNS"]["SML"]["Power"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+**Native alternative:** the device is ESP-based — flashing ESPHome lets you read
+the underlying energy-monitor chip (CSE7766 / HLW8012 / BL0942 / ADE7953)
+directly as a native sensor.
+
+## Shrdzm
+
+**Tier: 🔵 Generic.** The SHRDZM module serves `GET /getLastData?user=…&password=…`
+returning OBIS keys; grid power is `1.7.0` (import) minus `2.7.0` (export):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.102/getLastData?user=USER&password=PASS
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ float in = root["1.7.0"];
+ float out = root["2.7.0"];
+ id(grid_l1).publish_state(in - out);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+## Emlog
+
+**Tier: 🔵 Generic.** EmLog serves
+`GET /pages/getinformation.php?heute&meterindex=` with `Leistung170`
+(import) and `Leistung270` (export):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.103/pages/getinformation.php?heute&meterindex=0
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ float in = root["Leistung170"];
+ float out = root["Leistung270"];
+ id(grid_l1).publish_state(in - out);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+## IoBroker
+
+**Tier: 🔵 Generic.** With ioBroker's simpleAPI adapter, `GET /getBulk/`
+returns a JSON array (`GET /getPlainValue/` returns a bare number):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.104:8087/getBulk/Alias.0.power
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonArray arr) -> bool {
+ id(grid_l1).publish_state(arr[0]["val"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+**Alternative:** if you run ioBroker's MQTT adapter, publish the state to a topic
+and read it with the native [`mqtt_subscribe`](#mqtt) sensor instead (simpler and
+push-based).
+
+## HomeAssistant
+
+**Tier: 🟢 Native.** Use the built-in
+[`homeassistant`](https://esphome.io/components/sensor/homeassistant/) sensor
+platform — the ESP subscribes to a HA entity over the native API (so the `api:`
+block, normally part of the omitted boilerplate, is shown here because it's
+required for this source):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+api: # native API link to Home Assistant is required for this source
+
+sensor:
+ - platform: homeassistant
+ id: grid_l1
+ entity_id: sensor.grid_power
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+For three-phase, add `grid_l2` / `grid_l3` sensors pointing at the per-phase
+entities and set `power_sensor_l2` / `power_sensor_l3`. This is the same data
+path the Python `[HOMEASSISTANT]` source uses, but pushed to the ESP instead of
+polled from a server.
+
+## VZLogger
+
+**Tier: 🔵 Generic** — or **🟢 Native** by reading the meter directly.
+
+vzlogger's HTTP interface serves `GET /` with the latest tuple at
+`data[0].tuples[0][1]`:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.106:8080/your-uuid
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["data"][0]["tuples"][0][1]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+**Native alternative:** vzlogger itself just reads a physical meter (usually
+SML or DLMS/D0 over an IR head). You can skip vzlogger entirely and read that
+meter directly on the ESP with the native [`sml`](#sml) component (or
+[`dsmr`](https://esphome.io/components/sensor/dsmr/) for P1/D0), removing the
+middleware.
+
+## ESPHome
+
+**Tier: 🟢 Native.** The Python `[ESPHOME]` source polls another ESPHome
+device's web-server REST API. On the ESP32 there's no bridge to build — if your
+grid-power source is already an ESPHome device, either define that meter's sensor
+in the **same** YAML as `ct002:` (any native chip / Modbus / pulse-counter sensor
+with `id: grid_l1`), or import another ESPHome node's entity via Home Assistant.
+The latter, complete:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+api: # required to import the other node's entity from Home Assistant
+
+sensor:
+ - platform: homeassistant # the other ESPHome node's entity, via HA
+ id: grid_l1
+ entity_id: sensor.other_esphome_grid_power
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+You can also subscribe over [MQTT](#mqtt) if both nodes share a broker.
+
+## AMIS Reader
+
+**Tier: 🔵 Generic.** The AMIS reader serves `GET /rest` with a `saldo` field
+(signed grid power):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.108/rest
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["saldo"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+## Modbus
+
+**Tier: 🟢 Native** — with one important caveat (see below). Use the built-in
+[`modbus_controller`](https://esphome.io/components/sensor/modbus_controller/)
+sensor over an RS485 transceiver wired to the ESP:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+uart:
+ id: mod_uart
+ tx_pin: GPIO17
+ rx_pin: GPIO16
+ baud_rate: 9600
+ stop_bits: 1
+
+modbus:
+ id: modbus1
+ uart_id: mod_uart
+
+modbus_controller:
+ - id: meter
+ address: 1 # Modbus unit / slave id
+ modbus_id: modbus1
+ update_interval: 1s
+
+sensor:
+ - platform: modbus_controller
+ modbus_controller_id: meter
+ id: grid_l1
+ register_type: holding # or read (input)
+ address: 0
+ value_type: U_WORD # S_DWORD / U_DWORD / FP32 / … to match your meter
+ unit_of_measurement: W
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+> **Modbus-TCP caveat.** ESPHome's `modbus_controller` is a **serial (RS485)**
+> master — it does not open a raw Modbus-TCP socket the way the Python
+> `[MODBUS]` source does with `TRANSPORT = TCP`. To read a network Modbus-TCP
+> meter from the ESP you need either a wired RS485 connection to the meter, or a
+> Modbus-TCP↔RTU gateway. Map `DATA_TYPE`/`BYTE_ORDER`/`WORD_ORDER` from your
+> Python config onto `value_type` and the register's byte/word order.
+
+## MQTT
+
+**Tier: 🟢 Native.** For a plain numeric payload, use
+[`mqtt_subscribe`](https://esphome.io/components/sensor/mqtt_subscribe/):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+mqtt:
+ broker: 192.168.1.10
+ port: 1883
+
+sensor:
+ - platform: mqtt_subscribe
+ id: grid_l1
+ topic: home/powermeter
+ unit_of_measurement: W
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+For a **JSON** payload (the Python `JSON_PATH` case), `mqtt_subscribe` only
+handles bare floats, so extract the field with `on_json_message` into a template
+sensor:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+mqtt:
+ broker: 192.168.1.10
+ port: 1883
+ on_json_message:
+ topic: home/powermeter
+ then:
+ - sensor.template.publish:
+ id: grid_l1
+ state: !lambda 'return x["path"]["to"]["value"];'
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+For three-phase, subscribe to three topics (or read three fields) into
+`grid_l1/2/3` and set `power_sensor_l2` / `power_sensor_l3`.
+
+## JSON HTTP
+
+**Tier: 🟢 Native** (generic `http_request`). Point the URL at your endpoint and
+set the lambda to your JSON field. Headers and basic auth are supported on the
+`http_request.get` action:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://example.com/api
+ headers:
+ Authorization: Bearer token
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["power"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+## SML
+
+**Tier: 🟢 Native.** Smart meters that emit SML over an IR head map directly to
+ESPHome's built-in [`sml`](https://esphome.io/components/sml/) component (the
+ESP-side equivalent of the Python `[SML]` source). Wire a photo-transistor to a
+UART RX pin, then select the OBIS register:
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+uart:
+ id: uart_bus
+ rx_pin: GPIO16
+ baud_rate: 9600
+ data_bits: 8
+ parity: NONE
+ stop_bits: 1
+
+sml:
+ id: mysml
+ uart_id: uart_bus
+
+sensor:
+ - platform: sml
+ id: grid_l1
+ sml_id: mysml
+ obis_code: "1-0:16.7.0" # aggregate active power (Python OBIS_POWER_CURRENT)
+ unit_of_measurement: W
+ # per-phase instead: 1-0:36.7.0 (L1), 1-0:56.7.0 (L2), 1-0:76.7.0 (L3)
+ # → add grid_l2 / grid_l3 sensors and set power_sensor_l2 / l3 below
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+The default `obis_code` here matches the Python source's default
+`OBIS_POWER_CURRENT` (`0100100700ff` → `1-0:16.7.0`); the per-phase codes match
+its `OBIS_POWER_L1/L2/L3` defaults.
+
+## TQ Energy Manager
+
+**Tier: 🟠 Alternate.** The Python `[TQ_EM]` source talks to the device's
+proprietary session/login JSON API (`/start.php` + `/mum-webservice/data.php`),
+which has no ESPHome port. However, the TQ Energy Manager (EM420 and similar)
+also exposes **Modbus RTU/TCP and MQTT** — read it through one of those instead.
+
+Via Modbus (RS485 to the EM; use the active-power register from the TQ Modbus
+register map — `address` / `value_type` below are placeholders):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+uart:
+ id: mod_uart
+ tx_pin: GPIO17
+ rx_pin: GPIO16
+ baud_rate: 9600
+ stop_bits: 1
+
+modbus:
+ id: modbus1
+ uart_id: mod_uart
+
+modbus_controller:
+ - id: tq_em
+ address: 1
+ modbus_id: modbus1
+ update_interval: 1s
+
+sensor:
+ - platform: modbus_controller
+ modbus_controller_id: tq_em
+ id: grid_l1
+ register_type: holding
+ address: 0 # ← set to the TQ active-power register
+ value_type: S_DWORD # ← match the register's type
+ unit_of_measurement: W
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+Alternatively enable the EM's MQTT export and use the [`mqtt_subscribe`](#mqtt)
+config above with the EM's power topic.
+
+## HomeWizard
+
+**Tier: 🟠 Alternate.** The Python `[HOMEWIZARD]` source uses the v2 WebSocket
+API (TLS + token), which has no ESPHome component. Easiest ESP path: enable
+*Local API* in the HomeWizard app and poll the **v1 HTTP API** at
+`GET /api/v1/data`; grid power is `active_power_w` (and `active_power_l1_w` …
+`_l3_w` for three-phase):
+
+```yaml
+external_components:
+ - source: github://tomquist/astrameter@develop
+ components: [ct002]
+
+http_request:
+ useragent: esphome/astrameter
+ timeout: 5s
+
+sensor:
+ - platform: template
+ id: grid_l1
+ unit_of_measurement: W
+ device_class: power
+
+interval:
+ - interval: 1s
+ then:
+ - http_request.get:
+ url: http://192.168.1.110/api/v1/data
+ capture_response: true
+ on_response:
+ then:
+ - lambda: |-
+ json::parse_json(body, [](JsonObject root) -> bool {
+ id(grid_l1).publish_state(root["active_power_w"]);
+ return true;
+ });
+
+ct002:
+ id: ct002_main
+ power_sensor_l1: grid_l1
+```
+
+**Native alternative:** the HomeWizard dongle just reads your smart meter's P1
+telegram. With your own P1-reader hardware you can skip the dongle and use
+ESPHome's native [`dsmr`](https://esphome.io/components/sensor/dsmr/) component
+(or [`sml`](#sml) for SML meters) on the ESP.
+
+## Enphase Envoy (IQ Gateway)
+
+**Tier: 🔴 Not yet available.** There is currently **no ESPHome component** for
+the Enphase Envoy / IQ Gateway, so there is no config to copy yet. The Python
+`[ENVOY]` source reads the local `/production.json?details=1` endpoint, which
+requires:
+
+- **HTTPS to a self-signed certificate** on the gateway, and
+- a **JWT bearer token** — either a long-lived static token or one fetched and
+ refreshed via the Enphase Enlighten **cloud** (login → entrez token endpoint),
+ including transparent re-auth on HTTP 401.
+
+ESPHome's stock `http_request` can't comfortably do the cloud token exchange and
+refresh loop, so this needs a **purpose-built external component**.
+
+*To implement:* an external component that (1) holds a static JWT or performs the
+Enlighten login + entrez token fetch over TLS, (2) GETs `production.json` from
+the local gateway (TLS, self-signed), (3) parses the `consumption[] →
+net-consumption` entry (per-phase `lines[].wNow`, else aggregate `wNow`), and
+(4) re-auths on 401. References: the AstraMeter Python implementation in
+`src/astrameter/powermeter/envoy.py`, plus existing ESP32 work in
+[collin80/Envoy](https://github.com/collin80/Envoy) and
+[Matthew1471/Enphase-API](https://github.com/Matthew1471/Enphase-API).
+
+## SMA Energy Meter
+
+**Tier: 🔴 Not yet available.** There is currently **no ESPHome component** for
+the SMA **Speedwire** protocol, so there is no config to copy yet. The Python
+`[SMA_ENERGY_METER]` source joins the `239.12.255.254:9522` UDP multicast group
+and decodes SMA's binary OBIS channel stream (validating the `SMA\0` magic,
+protocol id `0x6069`, SUSY/serial, then reading the active-power channels and
+dividing the raw value by 10).
+
+*To implement:* an external component that joins the multicast group on the ESP's
+network interface and parses the Speedwire datagram (per-phase
+`L1/L2/L3 = power_plus − power_minus`, else total). References: the AstraMeter
+Python implementation in `src/astrameter/powermeter/sma_energy_meter.py`, and the
+protocol as implemented by [sma2mqtt](https://github.com/vindolin/sma2mqtt) and
+[SMA-Speedwire](https://github.com/J0B10/SMA-Speedwire).
diff --git a/docs/powermeters.md b/docs/powermeters.md
new file mode 100644
index 00000000..3e2fa024
--- /dev/null
+++ b/docs/powermeters.md
@@ -0,0 +1,439 @@
+# Powermeter Configuration Reference (Python add-on / Docker / direct install)
+
+This is the per-source reference for the Python AstraMeter (Home Assistant
+add-on, Docker, or direct install). **Find your meter in the list below and copy
+the matching `config.ini` section.**
+
+These sections only cover the *source* of the grid-power reading. Options that
+apply to **any** powermeter — throttling, wait-for-fresh-push, EMA smoothing,
+deadband, Hampel outlier rejection — plus [Value
+Transformation](../README.md#value-transformation), the [PID
+Controller](../README.md#pid-controller), and running [Multiple
+Powermeters](../README.md#multiple-powermeters) are documented in the main
+[README](../README.md#configuration).
+
+> **Running on an ESP32 instead of the Python add-on?** See
+> [esphome-powermeters.md](esphome-powermeters.md) for the equivalent
+> grid-power sensor configuration for the ESPHome external component.
+
+## Contents
+
+- [Shelly](#shelly)
+- [Tasmota](#tasmota)
+- [Shrdzm](#shrdzm)
+- [Emlog](#emlog)
+- [IoBroker](#iobroker)
+- [HomeAssistant](#homeassistant)
+- [VZLogger](#vzlogger)
+- [ESPHome](#esphome)
+- [AMIS Reader](#amis-reader)
+- [Modbus (TCP/UDP)](#modbus-tcpudp)
+- [MQTT](#mqtt)
+- [JSON HTTP](#json-http)
+- [TQ Energy Manager](#tq-energy-manager)
+- [HomeWizard](#homewizard)
+- [Enphase Envoy (IQ Gateway)](#enphase-envoy-iq-gateway)
+- [SMA Energy Meter](#sma-energy-meter)
+- [Script](#script)
+- [SML](#sml)
+
+## Shelly
+
+### Shelly 1PM
+```ini
+[SHELLY]
+TYPE = 1PM
+IP = 192.168.1.100
+USER = username
+PASS = password
+METER_INDEX = meter1
+```
+
+### Shelly Plus 1PM
+```ini
+[SHELLY]
+TYPE = PLUS1PM
+IP = 192.168.1.100
+USER = username
+PASS = password
+METER_INDEX = meter1
+```
+
+### Shelly EM
+```ini
+[SHELLY]
+TYPE = EM
+IP = 192.168.1.100
+USER = username
+PASS = password
+METER_INDEX = meter1
+```
+
+### Shelly 3EM
+```ini
+[SHELLY]
+TYPE = 3EM
+IP = 192.168.1.100
+USER = username
+PASS = password
+METER_INDEX = meter1
+```
+
+### Shelly 3EM Pro
+```ini
+[SHELLY]
+TYPE = 3EMPro
+IP = 192.168.1.100
+USER = username
+PASS = password
+METER_INDEX = meter1
+```
+
+## Tasmota
+
+```ini
+[TASMOTA]
+IP = 192.168.1.101
+USER = tasmota_user
+PASS = tasmota_pass
+JSON_STATUS = StatusSNS
+JSON_PAYLOAD_MQTT_PREFIX = SML
+JSON_POWER_MQTT_LABEL = Power
+JSON_POWER_INPUT_MQTT_LABEL = Power1
+JSON_POWER_OUTPUT_MQTT_LABEL = Power2
+JSON_POWER_CALCULATE = True
+```
+
+For 3-phase meters, use comma-separated labels:
+
+```ini
+[TASMOTA]
+IP = 192.168.1.101
+JSON_STATUS = StatusSNS
+JSON_PAYLOAD_MQTT_PREFIX = eBZ
+JSON_POWER_MQTT_LABEL = Power_L1,Power_L2,Power_L3
+```
+
+For 3-phase with `JSON_POWER_CALCULATE`, provide matching comma-separated
+input and output labels (counts must be equal):
+
+```ini
+[TASMOTA]
+IP = 192.168.1.101
+JSON_STATUS = StatusSNS
+JSON_PAYLOAD_MQTT_PREFIX = SML
+JSON_POWER_INPUT_MQTT_LABEL = Power_In_L1,Power_In_L2,Power_In_L3
+JSON_POWER_OUTPUT_MQTT_LABEL = Power_Out_L1,Power_Out_L2,Power_Out_L3
+JSON_POWER_CALCULATE = True
+```
+
+## Shrdzm
+
+```ini
+[SHRDZM]
+IP = 192.168.1.102
+USER = shrdzm_user
+PASS = shrdzm_pass
+```
+
+## Emlog
+
+```ini
+[EMLOG]
+IP = 192.168.1.103
+METER_INDEX = 0
+JSON_POWER_CALCULATE = True
+```
+
+## IoBroker
+
+```ini
+[IOBROKER]
+IP = 192.168.1.104
+PORT = 8087
+CURRENT_POWER_ALIAS = Alias.0.power
+POWER_CALCULATE = True
+POWER_INPUT_ALIAS = Alias.0.power_in
+POWER_OUTPUT_ALIAS = Alias.0.power_out
+```
+
+## HomeAssistant
+```ini
+[HOMEASSISTANT]
+IP = 192.168.1.105
+PORT = 8123
+# Use HTTPS - if empty False is Fallback
+HTTPS = ""|True|False
+ACCESSTOKEN = YOUR_ACCESS_TOKEN
+# The entity or entities (comma-separated for 3-phase) that provide current power
+CURRENT_POWER_ENTITY = ""|sensor.current_power|sensor.phase1,sensor.phase2,sensor.phase3
+# If False or Empty the power is not calculated - if empty False is Fallback
+POWER_CALCULATE = ""|True|False
+# The entity ID or IDs (comma-separated for 3-phase) that provide power input
+POWER_INPUT_ALIAS = ""|sensor.power_input|sensor.power_in_1,sensor.power_in_2,sensor.power_in_3
+# The entity ID or IDs (comma-separated for 3-phase) that provide power output
+POWER_OUTPUT_ALIAS = ""|sensor.power_output|sensor.power_out_1,sensor.power_out_2,sensor.power_out_3
+# Is a Path Prefix needed?
+API_PATH_PREFIX = ""|/core
+# Per-powermeter throttling override (recommended: 2-3 seconds for HomeAssistant)
+THROTTLE_INTERVAL = 2
+```
+
+Example: Variant 1 with a single combined input & output sensor
+```ini
+[HOMEASSISTANT]
+IP = 192.168.1.105
+PORT = 8123
+HTTPS = True
+ACCESSTOKEN = YOUR_ACCESS_TOKEN
+CURRENT_POWER_ENTITY = sensor.current_power
+```
+
+Example: Variant 2 with separate input & output sensors
+```ini
+[HOMEASSISTANT]
+IP = 192.168.1.105
+PORT = 8123
+HTTPS = True
+ACCESSTOKEN = YOUR_ACCESS_TOKEN
+POWER_CALCULATE = True
+POWER_INPUT_ALIAS = sensor.power_input
+POWER_OUTPUT_ALIAS = sensor.power_output
+```
+
+Example: Variant 3 with three-phase power monitoring
+```ini
+[HOMEASSISTANT]
+IP = 192.168.1.105
+PORT = 8123
+HTTPS = True
+ACCESSTOKEN = YOUR_ACCESS_TOKEN
+CURRENT_POWER_ENTITY = sensor.phase1,sensor.phase2,sensor.phase3
+```
+
+Example: Variant 4 with three-phase power calculation
+```ini
+[HOMEASSISTANT]
+IP = 192.168.1.105
+PORT = 8123
+HTTPS = True
+ACCESSTOKEN = YOUR_ACCESS_TOKEN
+POWER_CALCULATE = True
+POWER_INPUT_ALIAS = sensor.power_in_1,sensor.power_in_2,sensor.power_in_3
+POWER_OUTPUT_ALIAS = sensor.power_out_1,sensor.power_out_2,sensor.power_out_3
+# Per-powermeter throttling override (recommended: 2-3 seconds for HomeAssistant)
+# THROTTLE_INTERVAL = 2
+```
+
+## VZLogger
+
+```ini
+[VZLOGGER]
+IP = 192.168.1.106
+PORT = 8080
+UUID = your-uuid
+```
+
+For 3-phase meters, provide comma-separated UUIDs (one per phase); phases are
+fetched in parallel:
+
+```ini
+[VZLOGGER]
+IP = 192.168.1.106
+PORT = 8080
+UUID = uuid-l1, uuid-l2, uuid-l3
+```
+
+## ESPHome
+
+```ini
+[ESPHOME]
+IP = 192.168.1.107
+PORT = 6052
+DOMAIN = your_domain
+ID = your_id
+```
+
+## AMIS Reader
+
+```ini
+[AMIS_READER]
+IP = 192.168.1.108
+```
+
+## Modbus (TCP/UDP)
+
+```ini
+[MODBUS]
+HOST = 192.168.1.100
+PORT = 502
+UNIT_ID = 1
+ADDRESS = 0
+COUNT = 1
+DATA_TYPE = UINT16
+BYTE_ORDER = BIG
+WORD_ORDER = BIG
+REGISTER_TYPE = HOLDING # or INPUT
+TRANSPORT = TCP # or UDP
+```
+
+`TRANSPORT` selects the Modbus transport: `TCP` (default) or `UDP`.
+
+## MQTT
+
+```ini
+[MQTT]
+BROKER = broker.example.com
+PORT = 1883
+TOPIC = home/powermeter
+JSON_PATH = $.path.to.value (Optional for JSON payloads)
+USERNAME = mqtt_user (Optional)
+PASSWORD = mqtt_pass (Optional)
+# Optional: connect over TLS (mqtts://) — default false
+# TLS = false
+# Per-powermeter throttling override
+# THROTTLE_INTERVAL = 2
+```
+
+Instead of `BROKER`/`PORT`/`USERNAME`/`PASSWORD`/`TLS`, you can provide a single `URI` of the form `mqtt[s]://[user[:pass]@]host[:port]` (use `mqtts://` for TLS; credentials and port are optional). When `URI` is set, the individual `BROKER`/`PORT`/`USERNAME`/`PASSWORD`/`TLS` fields are ignored.
+
+```ini
+[MQTT]
+URI = mqtts://user:pass@broker.example.com:8883
+TOPIC = home/powermeter
+```
+
+The `JSON_PATH` option is used to extract the power value from a JSON payload. The path must be a [valid JSONPath expression](https://goessner.net/articles/JsonPath/).
+If the payload is a simple integer value, you can omit this option.
+
+Both `JSON_PATH` and `JSON_PATHS` are parsed with the [`jsonpath-ng` extended syntax](https://github.com/h2non/jsonpath-ng#extensions), so you can chain extensions like `` `split(...)` `` or `` `sub(/regex/, replacement)` `` to massage a payload value before it's converted to a float — for instance `` `$.state.`split( , 0, -1)` `` or `` `$.state.`sub(/[^0-9.\-]+$/, )` `` to strip a unit suffix like `"331.74 W"`. See the [JSON HTTP](#json-http) section below for more examples.
+
+### Multi-phase MQTT
+
+For three-phase setups, there are two options:
+
+**Option 1: Multiple topics** — one topic per phase, each publishing a plain numeric value (or JSON with the same path):
+
+```ini
+[MQTT]
+BROKER = broker.example.com
+TOPICS = home/power/l1, home/power/l2, home/power/l3
+```
+
+**Option 2: Single topic with multiple JSON paths** — one topic publishing a JSON message containing all phases:
+
+```ini
+[MQTT]
+BROKER = broker.example.com
+TOPIC = home/powermeter
+JSON_PATHS = $.phases[0].power, $.phases[1].power, $.phases[2].power
+```
+
+`TOPICS` takes precedence over `TOPIC`, and `JSON_PATHS` takes precedence over `JSON_PATH`. You can combine `TOPICS` with `JSON_PATH` (same path applied to each topic) or with `JSON_PATHS` (one path per topic, counts must match).
+
+## JSON HTTP
+
+```ini
+[JSON_HTTP]
+URL = http://example.com/api
+# Comma separated JSON paths - single path for 1-phase or three for 3-phase
+JSON_PATHS = $.power
+USERNAME = user (Optional)
+PASSWORD = pass (Optional)
+# Additional headers separated by ';' using 'Key: Value'
+HEADERS = Authorization: Bearer token
+```
+
+`JSON_PATHS` is parsed with the [`jsonpath-ng` extended syntax](https://github.com/h2non/jsonpath-ng#extensions), so you can chain extensions like `` `split(...)` `` or `` `sub(/regex/, replacement)` `` to massage the value before it's converted to a float. For example, an openHAB `Number:Power` item returns `"331.74 W"` — strip the unit with either of:
+
+```ini
+JSON_PATHS = $.state.`split( , 0, -1)`
+JSON_PATHS = $.state.`sub(/[^0-9.\-]+$/, )`
+```
+
+## TQ Energy Manager
+
+```ini
+[TQ_EM]
+IP = 192.168.1.100
+#PASSWORD = pass
+#TIMEOUT = 5.0 (Optional)
+```
+
+## HomeWizard
+
+Reads a [HomeWizard](https://www.homewizard.com/) P1 dongle (or compatible device) over the local **WebSocket** API (`wss://`). Obtain a token once via `POST /api/user` while confirming on the device; see the [HomeWizard API docs](https://api-documentation.homewizard.com/docs/v2/).
+
+```ini
+[HOMEWIZARD]
+IP = 192.168.1.110
+TOKEN = YOUR_32_CHAR_HEX_TOKEN
+SERIAL = your_device_serial
+# Optional: disable TLS certificate verification on a trusted LAN if verification fails (default True)
+# VERIFY_SSL = True
+# THROTTLE_INTERVAL = 0
+```
+
+## Enphase Envoy (IQ Gateway)
+
+Reads grid power from an [Enphase IQ Gateway / Envoy](https://enphase.com/installers/microinverters/iq-gateway) over the local HTTPS API (`/production.json?details=1`). The reading comes from the `net-consumption` measurement (positive = grid import, negative = export). Per-phase readings are reported automatically when the gateway exposes them; otherwise the aggregate single-phase value is used. Requires consumption CTs installed on the Envoy.
+
+```ini
+[ENVOY]
+HOST = 192.168.1.120
+# Option A: pre-obtained long-lived JWT (recommended)
+TOKEN = eyJ...
+# Option B: let AstraMeter fetch and refresh tokens via the Enphase Enlighten cloud
+# USERNAME = you@example.com
+# PASSWORD = your-enphase-password
+# SERIAL = 123456789012
+# Envoy ships a self-signed certificate; verification is disabled by default.
+# VERIFY_SSL = False
+```
+
+**Token acquisition.** Generate a long-lived (~1 year) static token at . Alternatively, configure `USERNAME`/`PASSWORD`/`SERIAL` and AstraMeter will fetch a token on first use and refresh it automatically when the Envoy returns 401.
+
+**TLS.** `VERIFY_SSL` defaults to `False` because Enphase does not publish a CA bundle for the IQ Gateway's self-signed certificate. This option **only affects the local Envoy connection** — Enphase Enlighten cloud requests (login and token endpoints) always verify TLS using the system trust store, regardless of this setting.
+
+**MFA.** The auto-fetch flow does not support Enlighten accounts with multi-factor authentication enabled. Those users must supply a static `TOKEN`.
+
+**CT direction.** If your readings have the wrong sign (export shows as import or vice versa), one or more CTs are mounted backwards. Flip them in software with the global `POWER_MULTIPLIER = -1` (or per-phase, e.g. `POWER_MULTIPLIER = 1, -1, 1`).
+
+## SMA Energy Meter
+
+Reads an [SMA Energy Meter](https://www.sma.de/) (EM 1.0/2.0) or Sunny Home Manager via the **Speedwire** multicast protocol (UDP). The listener joins the default multicast group and reports per-phase active power (L1, L2, L3). Use `SERIAL_NUMBER = 0` to auto-detect the first meter seen on the network, or set the device serial to pin a specific unit. Like other UDP-based features, this requires the host to receive multicast traffic (use Docker host networking or equivalent).
+
+```ini
+[SMA_ENERGY_METER]
+MULTICAST_GROUP = 239.12.255.254
+PORT = 9522
+SERIAL_NUMBER = 0
+# INTERFACE = 192.168.1.10
+# THROTTLE_INTERVAL = 0
+```
+
+## Script
+
+You can also use a custom script to get the power values. The script should output at most 3 integer values, separated by a line break.
+```ini
+[SCRIPT]
+COMMAND = /path/to/your/script.sh
+```
+
+## SML
+
+```ini
+[SML]
+SERIAL = /dev/ttyUSB0
+# Optional: override default OBIS hex registers (12 hex digits each; defaults match common German eHZ meters)
+#OBIS_POWER_CURRENT = 0100100700ff
+#OBIS_POWER_L1 = 0100240700ff
+#OBIS_POWER_L2 = 0100380700ff
+#OBIS_POWER_L3 = 01004c0700ff
+```
+
+Read from a powermeter that is connected via USB and that transmits SML (Smart Message Language) data via an IR head. **`SERIAL` is required**: local device path to the serial interface (e.g. `/dev/ttyUSB0` on Linux).
+
+**Multi-phase:** If the meter exposes per-phase instantaneous active power for L1–L3 (`Summenwirkleistung` / default OBIS above), those three values are used automatically. Otherwise the aggregate instantaneous power register (`aktuelle Wirkleistung` / `OBIS_POWER_CURRENT`) is used as a single reading. When both are present in the same SML list, per-phase values take precedence.
+
+**OBIS overrides:** Only needed if your meter uses different register addresses; values must be exactly 12 hexadecimal characters (lowercase or uppercase).