Skip to content

Commit a886dbd

Browse files
tomquistclaude
andauthored
Add multi-phase support for VZLogger powermeter (#332)
* Add 3-phase support for VZLogger powermeter Accept comma-separated UUIDs in the VZLOGGER section so 3-phase smart meters return one value per phase. UUIDs are fetched in parallel via asyncio.gather. Single-UUID configs continue to return a one-element list, matching the str | list[str] idiom used by Tasmota and JSON HTTP. * Link PR #332 in VZLogger 3-phase changelog entry --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1f8445a commit a886dbd

6 files changed

Lines changed: 37 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- 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))
1010
- 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))
1111
- 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))
12+
- 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))
1213
- 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
1314
- 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
1415
- Added optional Marstek cloud auto-registration for managed fake CT devices at startup ([#237](https://github.com/tomquist/astrameter/pull/237))

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,16 @@ PORT = 8080
599599
UUID = your-uuid
600600
```
601601

602+
For 3-phase meters, provide comma-separated UUIDs (one per phase); phases are
603+
fetched in parallel:
604+
605+
```ini
606+
[VZLOGGER]
607+
IP = 192.168.1.106
608+
PORT = 8080
609+
UUID = uuid-l1, uuid-l2, uuid-l3
610+
```
611+
602612
### ESPHome
603613

604614
```ini

config.ini.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@ THROTTLE_INTERVAL = 0
211211
#IP = 192.168.1.106
212212
#PORT = 8080
213213
#UUID = your-uuid
214+
## For 3-phase meters, use comma-separated UUIDs (one per phase):
215+
#UUID = uuid-l1, uuid-l2, uuid-l3
214216
## Per-powermeter throttling override (optional)
215217
#THROTTLE_INTERVAL = 1
216218

src/astrameter/config/config_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ def create_vzlogger_powermeter(
508508
return VZLogger(
509509
config.get(section, "IP", fallback=""),
510510
config.get(section, "PORT", fallback=""),
511-
config.get(section, "UUID", fallback=""),
511+
_split_labels(config.get(section, "UUID", fallback="")),
512512
)
513513

514514

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import asyncio
2+
13
import aiohttp
24

35
from .base import Powermeter
46

57

68
class VZLogger(Powermeter):
7-
def __init__(self, ip: str, port: str, uuid: str):
9+
def __init__(self, ip: str, port: str, uuid: str | list[str]):
810
self.ip = ip
911
self.port = port
10-
self.uuid = uuid
12+
self.uuids = [uuid] if isinstance(uuid, str) else list(uuid)
1113
self.session: aiohttp.ClientSession | None = None
1214

1315
async def start(self) -> None:
@@ -20,12 +22,13 @@ async def stop(self) -> None:
2022
await self.session.close()
2123
self.session = None
2224

23-
async def get_json(self):
25+
async def get_json(self, uuid: str):
2426
if not self.session:
2527
raise RuntimeError("Session not started; call start() first")
26-
url = f"http://{self.ip}:{self.port}/{self.uuid}"
28+
url = f"http://{self.ip}:{self.port}/{uuid}"
2729
async with self.session.get(url) as resp:
2830
return await resp.json(content_type=None)
2931

3032
async def get_powermeter_watts(self) -> list[float]:
31-
return [int((await self.get_json())["data"][0]["tuples"][0][1])]
33+
results = await asyncio.gather(*(self.get_json(u) for u in self.uuids))
34+
return [int(r["data"][0]["tuples"][0][1]) for r in results]

src/astrameter/powermeter/vzlogger_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,18 @@ async def test_vzlogger_get_powermeter_watts(mock_aiohttp_session):
1010
await vzlogger.start()
1111
assert await vzlogger.get_powermeter_watts() == [900]
1212
await vzlogger.stop()
13+
14+
15+
async def test_vzlogger_three_phase(mock_aiohttp_session):
16+
mock_aiohttp_session.set_json({"data": [{"tuples": [[None, 900]]}]})
17+
with patch("aiohttp.ClientSession", return_value=mock_aiohttp_session):
18+
vzlogger = VZLogger("192.168.1.9", "8088", ["uuid-l1", "uuid-l2", "uuid-l3"])
19+
await vzlogger.start()
20+
assert await vzlogger.get_powermeter_watts() == [900, 900, 900]
21+
urls = [c.args[0] for c in mock_aiohttp_session.get.call_args_list]
22+
assert urls == [
23+
"http://192.168.1.9:8088/uuid-l1",
24+
"http://192.168.1.9:8088/uuid-l2",
25+
"http://192.168.1.9:8088/uuid-l3",
26+
]
27+
await vzlogger.stop()

0 commit comments

Comments
 (0)