diff --git a/CHANGELOG.md b/CHANGELOG.md index 216b5817..c1a2318a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added SMA Energy Meter / Sunny Home Manager support via Speedwire multicast with auto-detection and per-phase readings ([#252](https://github.com/tomquist/astrameter/pull/252)) - Added SML powermeter support for smart meters over a local serial port (IR head), with optional per-phase OBIS overrides ([#229](https://github.com/tomquist/astrameter/pull/229)) - Added multi-phase support for Tasmota (`JSON_POWER_MQTT_LABEL`) and MQTT (`TOPICS` / `JSON_PATHS`) powermeters ([#136](https://github.com/tomquist/astrameter/issues/136), [#280](https://github.com/tomquist/astrameter/pull/280)) +- Added multi-phase support for the VZLogger powermeter via comma-separated `UUID` values; phases are fetched in parallel ([#332](https://github.com/tomquist/astrameter/pull/332)) - Added PID controller support for any powermeter via `PID_KP`, `PID_KI`, `PID_KD`, `PID_OUTPUT_MAX`, and `PID_MODE` config options (global or per-section), with built-in anti-windup - Added `POWER_OFFSET` and `POWER_MULTIPLIER` transforms for any powermeter, including per-phase calibration, sign flipping, and phase nulling ([#250](https://github.com/tomquist/astrameter/pull/250)); the Home Assistant app exposes both as optional advanced fields - Added optional Marstek cloud auto-registration for managed fake CT devices at startup ([#237](https://github.com/tomquist/astrameter/pull/237)) diff --git a/README.md b/README.md index 34439977..7af28bbc 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,16 @@ 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 diff --git a/config.ini.example b/config.ini.example index 62069784..a871612b 100644 --- a/config.ini.example +++ b/config.ini.example @@ -211,6 +211,8 @@ THROTTLE_INTERVAL = 0 #IP = 192.168.1.106 #PORT = 8080 #UUID = your-uuid +## For 3-phase meters, use comma-separated UUIDs (one per phase): +#UUID = uuid-l1, uuid-l2, uuid-l3 ## Per-powermeter throttling override (optional) #THROTTLE_INTERVAL = 1 diff --git a/src/astrameter/config/config_loader.py b/src/astrameter/config/config_loader.py index 3f49cfd1..b4273b7c 100644 --- a/src/astrameter/config/config_loader.py +++ b/src/astrameter/config/config_loader.py @@ -508,7 +508,7 @@ def create_vzlogger_powermeter( return VZLogger( config.get(section, "IP", fallback=""), config.get(section, "PORT", fallback=""), - config.get(section, "UUID", fallback=""), + _split_labels(config.get(section, "UUID", fallback="")), ) diff --git a/src/astrameter/powermeter/vzlogger.py b/src/astrameter/powermeter/vzlogger.py index 732aacaf..26d9cda9 100644 --- a/src/astrameter/powermeter/vzlogger.py +++ b/src/astrameter/powermeter/vzlogger.py @@ -1,13 +1,15 @@ +import asyncio + import aiohttp from .base import Powermeter class VZLogger(Powermeter): - def __init__(self, ip: str, port: str, uuid: str): + def __init__(self, ip: str, port: str, uuid: str | list[str]): self.ip = ip self.port = port - self.uuid = uuid + self.uuids = [uuid] if isinstance(uuid, str) else list(uuid) self.session: aiohttp.ClientSession | None = None async def start(self) -> None: @@ -20,12 +22,13 @@ async def stop(self) -> None: await self.session.close() self.session = None - async def get_json(self): + async def get_json(self, uuid: str): if not self.session: raise RuntimeError("Session not started; call start() first") - url = f"http://{self.ip}:{self.port}/{self.uuid}" + url = f"http://{self.ip}:{self.port}/{uuid}" async with self.session.get(url) as resp: return await resp.json(content_type=None) async def get_powermeter_watts(self) -> list[float]: - return [int((await self.get_json())["data"][0]["tuples"][0][1])] + results = await asyncio.gather(*(self.get_json(u) for u in self.uuids)) + return [int(r["data"][0]["tuples"][0][1]) for r in results] diff --git a/src/astrameter/powermeter/vzlogger_test.py b/src/astrameter/powermeter/vzlogger_test.py index a1cb9121..1cfa7db8 100644 --- a/src/astrameter/powermeter/vzlogger_test.py +++ b/src/astrameter/powermeter/vzlogger_test.py @@ -10,3 +10,18 @@ async def test_vzlogger_get_powermeter_watts(mock_aiohttp_session): await vzlogger.start() assert await vzlogger.get_powermeter_watts() == [900] await vzlogger.stop() + + +async def test_vzlogger_three_phase(mock_aiohttp_session): + mock_aiohttp_session.set_json({"data": [{"tuples": [[None, 900]]}]}) + with patch("aiohttp.ClientSession", return_value=mock_aiohttp_session): + vzlogger = VZLogger("192.168.1.9", "8088", ["uuid-l1", "uuid-l2", "uuid-l3"]) + await vzlogger.start() + assert await vzlogger.get_powermeter_watts() == [900, 900, 900] + urls = [c.args[0] for c in mock_aiohttp_session.get.call_args_list] + assert urls == [ + "http://192.168.1.9:8088/uuid-l1", + "http://192.168.1.9:8088/uuid-l2", + "http://192.168.1.9:8088/uuid-l3", + ] + await vzlogger.stop()