Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config.ini.example
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/astrameter/config/config_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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="")),
)


Expand Down
13 changes: 8 additions & 5 deletions src/astrameter/powermeter/vzlogger.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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]
15 changes: 15 additions & 0 deletions src/astrameter/powermeter/vzlogger_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading