#!/usr/bin/env python3 # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Self-check: run `python3 test_proxy.py`. Fakes the plugs, exercises routing and totals.""" import datetime as dt import json import os import tempfile import urllib.request os.environ["HISTORY_DB"] = os.path.join(tempfile.mkdtemp(), "test.db") os.environ["TASMOTA_PLUGS"] = "ac=1.1.1.1,washer=2.2.2.2,dead=3.3.3.3" os.environ["RATE_MARGINAL"] = "0.1" os.environ["RATE_ALLIN"] = "0.2" import tasmota_proxy as p # noqa: E402 SAMPLE = { "1.1.1.1": {"Power": 1000, "Voltage": 230, "Current": 4.3, "Today": 2.0, "Yesterday": 1.0, "Total": 10.0}, "2.2.2.2": {"Power": 500, "Voltage": 231, "Current": 2.1, "Today": 1.0, "Yesterday": 0.5, "Total": 5.0}, } class FakeResponse: def __init__(self, host): if host not in SAMPLE: raise OSError("plug offline") self._body = json.dumps({"StatusSNS": {"ENERGY": SAMPLE[host]}}) def read(self): return self._body.encode() def __enter__(self): return self def __exit__(self, *a): return False p.urllib.request.urlopen = lambda url, timeout=None: FakeResponse( urllib.parse.urlparse(url).hostname ) one = p.fetch("1.1.1.1") assert one["power"] == 1000, one assert one["cost"]["today_allin"] == 0.4, one # 2.0 kWh * 0.2 assert one["cost"]["now_hourly_marginal"] == 0.1, one # 1 kW * 0.1 allp = p.fetch_all() assert set(allp) == {"ac", "washer", "dead"}, allp assert "error" in allp["dead"], allp # offline plug isolated assert "error" not in allp["ac"], allp t = p.total() assert t["power"] == 1500, t # dead plug excluded, not zero-filled assert t["today_kwh"] == 3.0, t assert t["cost"]["today_allin"] == 0.6, t assert t["plugs_ok"] == 2 and t["plugs_total"] == 3, t assert t["offline"] == ["dead"], t # --- power formatting --- # 1585 W -> "1.58 kW": banker's rounding on the exact half, not a typo. for w, want in [(1585, "1.58 kW"), (79.7, "79.7 W"), (0, "0 W"), (999, "999 W"), (1000, "1.00 kW"), (2340.5, "2.34 kW")]: assert p.watts(w) == want, (w, p.watts(w), want) assert one["power_fmt"] == "1.00 kW", one # the 1000 W fake plug assert p.total()["power_fmt"] == "1.50 kW", p.total() # --- paired kWh + cost rows --- assert p.kwh_eur(2.0) == "2 kWh · 0.40 €", p.kwh_eur(2.0) # RATE_ALLIN=0.2 here assert p.kwh_eur(0) == "0 kWh · 0.00 €", p.kwh_eur(0) t = p.total() assert one["today_fmt"] == "2 kWh · 0.40 €", one # per-plug rows assert one["total_fmt"] == "10 kWh · 2.00 €", one t = p.total() assert t["today_fmt"] == "3 kWh · 0.60 €", t # 2.0 + 1.0, dead skipped assert t["total_fmt"] == "15 kWh · 3.00 €", t # --- daily history --- yday = (dt.date.today() - dt.timedelta(days=1)).isoformat() h = p.history() assert h == [{"day": yday, "kwh": 1.5, "cost_allin": 0.3}], h # 1.0 + 0.5, dead skipped p.total() # poll again same day h = p.history() assert len(h) == 1 and h[0]["kwh"] == 1.5, h # idempotent, not doubled # an older day coexists rather than replacing with p.db() as c: c.execute("INSERT OR REPLACE INTO daily VALUES (?, ?, ?)", ((dt.date.today() - dt.timedelta(days=3)).isoformat(), "ac", 4.0)) assert [r["kwh"] for r in p.history()] == [4.0, 1.5], p.history() # oldest first assert p.rolling(7) == 5.5, p.rolling(7) assert p.rolling(2) == 1.5, p.rolling(2) # window excludes the 3-day-old row t = p.total() assert t["last7_kwh"] == 5.5 and t["last7_allin"] == 1.1, t assert "