# tasmota-dash Energy proxy between five Tasmota smart plugs on the LAN and a Homepage dashboard. Homepage's `customapi` widget cannot do arithmetic, cannot format numbers unambiguously, and has no table layout, so everything derived is computed and preformatted here and the widget only maps field names to labels. ## Files | File | Role | |---|---| | `tasmota_proxy.py` | The whole service. Stdlib only, no dependencies. | | `test_proxy.py` | Self-check: `python3 test_proxy.py`, prints `ok`. Fakes the plugs, no LAN needed. | | `tasmota-proxy.service` | systemd unit. Deployed to `/etc/systemd/system/`. | | `homepage-services.yaml` | Snippet for Homepage's `services.yaml`. | | `homepage-settings.yaml` | `layout:` block for Homepage's `settings.yaml`, controls the rows. | Underscore in `tasmota_proxy.py` is deliberate: a dash is not importable, and the test imports the module. The unit file's `ExecStart` must match. ## Endpoints - `/` one plug: `ac`, `washer`, `dishwasher`, `pc`, `spare` - `/total` sum across plugs, plus rolling 7/30-day figures - `/history` per-day kWh, 30 days, JSON - `/graph` self-contained HTML bar chart, linked from the totals widget - `/` plug list and available paths Unknown plug name returns 404, an upstream failure 502. ## Deployment Runs on the Homepage host, listening on `127.0.0.1:8099`. ```bash scp tasmota_proxy.py :/opt/tasmota-proxy/ scp tasmota-proxy.service :/etc/systemd/system/ ssh 'systemctl daemon-reload && systemctl restart tasmota-proxy' ``` `tasmota_proxy.py` and `homepage-services.yaml` are a matched pair. Deploying one without the other renders blank widget rows, because the YAML maps fields (`power_fmt`, `today_fmt`, ...) that only the newer proxy emits. Homepage may run in a container. If it does, `127.0.0.1:8099` is the container's loopback, not the host's, and the widget URLs need the host IP with the proxy bound accordingly. ## Configuration All through environment variables in the unit file: - `TASMOTA_PLUGS` — `name=host,name=host,...`, defines both the plugs and their URL paths - `PORT` — default 8099 - `RATE_MARGINAL` / `RATE_ALLIN` — €/kWh from the invoice. Marginal is the consumption quota alone; all-in is total bill divided by total kWh. Widgets show all-in. - `HISTORY_DB` — default `/var/lib/tasmota-proxy/history.db` `StateDirectory=tasmota-proxy` in the unit is load-bearing. `ProtectSystem=strict` makes the filesystem read-only, so without it the first history write crashes the service. ## Daily history Tasmota exposes only `Today`, `Yesterday` and `Total`; there is no per-day series to fetch and no peak/maximum field of any kind. So each `/total` poll writes yesterday's finished kWh per plug into SQLite, keyed `(day, plug)` so repeat polls and restarts overwrite instead of accumulating. Homepage polls every 10s, so a day is captured as long as the proxy runs at some point during it. Consequences worth knowing: - Today is deliberately excluded from rolling sums; it is unfinished. - An offline plug is skipped, so its day-row is short. A later poll *the same day* fills it in; after midnight that day is lost, because Tasmota cannot be asked about a day before yesterday. - Extended downtime leaves permanent gaps. ## Working on the plugs Read state before writing it. Tasmota's HTTP API is `http:///cm?cmnd=`, with `%20` before an argument to set it, no argument to query. **Firmware varies, probe before assuming.** On these plugs: - `EnergyReset1/2/3` do **not** exist, they return `{"Command":"Unknown"}`. The working commands are `EnergyTotal`, `EnergyToday`, `EnergyYesterday`. - `VoltSet` / `PowerSet` do not exist either. Calibration goes through `VoltageCal`, `CurrentCal`, `PowerCal`. - `cmnd=Cmd=0` is not the argument syntax; it arrives as `CMD =0` and fails. Use `cmnd=Cmd%200`. Counter resets are irreversible. Confirm scope with the user first, and name which plugs are excluded. ### Calibration All five are NOUS A1T, same template, and should share calibration values: `VoltageCal 1550`, `CurrentCal 3500`, `PowerCal 12530`. Stock `VoltageCal` is 220 and is wrong for this hardware: voltage reads ~34 V instead of ~239. Because power factor and apparent power are derived from voltage, one wrong value cascades into physically impossible readings, PF above 1.0, apparent power below real power, ReactivePower pinned at 3276. Diagnosing a suspect plug: compare against a known-good one on the same mains, and sanity-check the physics. Mains is ~230-240 V, PF cannot exceed 1.0, apparent power cannot be below real power. A plug failing those is miscalibrated, and its kWh totals are wrong too, not just its instantaneous reading. Voltage needs no meter, since nominal mains is known. Current and power need a known resistive load with a nameplate rating. PF a few percent above 1.0 is residual per-unit variation and is not worth chasing for cost tracking. ## Conventions - Stdlib only. No dependencies, and no reason to add one. - Preformatted display strings (`power_fmt`, `today_fmt`, ...) exist because Homepage's locale formatting renders 1593 W as an ambiguous "1,593". Numeric fields stay in the JSON alongside them for any other consumer. - `1585 W` formats as `1.58 kW`, not 1.59. Python rounds the exact half to even. Verified in the test, not a bug. - Widget labels are Italian, matching the rest of the dashboard. - Non-trivial logic keeps its assertion in `test_proxy.py`. Run it before committing.