Skip to content

Commit ae43597

Browse files
committed
Update client to return all HZ/WW cost types.
get_meter_readings() now returns a dict keyed by cost_type, add get_monthly_consumptions() for all series, and update CLI/README/tests accordingly.
1 parent ff3f8b3 commit ae43597

File tree

5 files changed

+172
-17
lines changed

5 files changed

+172
-17
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ poetry run brunata readings --kind heating
4848
poetry run brunata readings --kind hot_water
4949
```
5050

51+
Verbrauchsdaten für **alle** CostTypes abrufen (z.B. `HZ01`, `HZ02`, ... / `WW01`, `WW02`, ...):
52+
53+
```bash
54+
poetry run brunata readings-all --kind heating
55+
poetry run brunata readings-all --kind hot_water
56+
```
57+
5158
Zählerstand abrufen:
5259

5360
```bash
@@ -87,6 +94,8 @@ Wichtige Methoden:
8794
- `BrunataClient.get_supported_cost_types()`
8895
- `BrunataClient.get_readings(...)`
8996
- `BrunataClient.get_monthly_consumption(cost_type=..., in_kwh=...)`
97+
- `BrunataClient.get_monthly_consumptions(kind, in_kwh=...)` (alle passenden CostTypes)
98+
- `BrunataClient.get_meter_readings()` (alle `HZ..` und `WW..`, keyed by `cost_type`)
9099

91100
## Entwicklung
92101

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "brunata-api"
3-
version = "0.1.2"
3+
version = "0.1.3"
44
description = "Async client for BRUdirekt (Brunata) portal"
55
authors = [
66
{name = "Felix Fricke"}

src/brunata_api/cli.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,29 @@ async def _cmd_readings(args: argparse.Namespace) -> int:
100100
return 0
101101

102102

103+
async def _cmd_readings_all(args: argparse.Namespace) -> int:
104+
base_url, username, password, sap_client = _load_config()
105+
kind = ReadingKind(args.kind)
106+
async with BrunataClient(
107+
base_url=base_url, username=username, password=password, sap_client=sap_client
108+
) as c:
109+
series = await c.get_monthly_consumptions(kind)
110+
payload: dict[str, list[dict[str, Any]]] = {
111+
ct: [r.model_dump(mode="json") for r in readings] for ct, readings in series.items()
112+
}
113+
print(json.dumps(payload, indent=2))
114+
return 0
115+
116+
103117
async def _cmd_meter(_: argparse.Namespace) -> int:
104118
base_url, username, password, sap_client = _load_config()
105119
async with BrunataClient(
106120
base_url=base_url, username=username, password=password, sap_client=sap_client
107121
) as c:
108122
readings = await c.get_meter_readings()
109-
payload: list[dict[str, Any]] = [r.model_dump(mode="json") for r in readings]
123+
payload: dict[str, dict[str, Any]] = {
124+
ct: r.model_dump(mode="json") for ct, r in readings.items()
125+
}
110126
print(json.dumps(payload, indent=2))
111127
return 0
112128

@@ -137,6 +153,12 @@ def build_parser() -> argparse.ArgumentParser:
137153
p_readings.add_argument("--kind", choices=[k.value for k in ReadingKind], required=True)
138154
p_readings.set_defaults(func=_cmd_readings)
139155

156+
p_readings_all = sub.add_parser(
157+
"readings-all", help="Fetch monthly series for all cost types (HZ../WW..)."
158+
)
159+
p_readings_all.add_argument("--kind", choices=[k.value for k in ReadingKind], required=True)
160+
p_readings_all.set_defaults(func=_cmd_readings_all)
161+
140162
p_meter = sub.add_parser("meter", help="Fetch Zählerstand (cumulative meter readings).")
141163
p_meter.set_defaults(func=_cmd_meter)
142164

src/brunata_api/client.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -563,28 +563,31 @@ async def get_meter_reading(self, *, cost_type: str) -> MeterReading:
563563
kind=kind,
564564
)
565565

566-
async def get_meter_readings(self) -> list[MeterReading]:
566+
async def get_meter_readings(self) -> dict[str, MeterReading]:
567567
"""
568-
Convenience helper: return meter readings for the first HZ.. and WW.. cost types.
568+
Convenience helper: return meter readings for *all* HZ.. and WW.. cost types.
569+
570+
Output is keyed by `cost_type` (stable mapping for HA entity IDs).
569571
"""
570572
dates = await self.get_dashboard_dates()
571573
results = (dates.get("d") or {}).get("results") or []
572574
period = next((p for p in results if isinstance(p, dict)), None)
573575
if not period:
574-
return []
576+
return {}
577+
575578
unit_rows = ((period.get("Units") or {}).get("results") or [])
576-
cost_types = [
577-
r.get("CostType")
578-
for r in unit_rows
579-
if isinstance(r, dict) and isinstance(r.get("CostType"), str)
580-
]
581-
hz = next((ct for ct in cost_types if ct.startswith("HZ")), None)
582-
ww = next((ct for ct in cost_types if ct.startswith("WW")), None)
583-
out: list[MeterReading] = []
584-
if hz:
585-
out.append(await self.get_meter_reading(cost_type=hz))
586-
if ww:
587-
out.append(await self.get_meter_reading(cost_type=ww))
579+
all_cost_types = sorted(
580+
{
581+
r["CostType"]
582+
for r in unit_rows
583+
if isinstance(r, dict) and isinstance(r.get("CostType"), str)
584+
}
585+
)
586+
wanted = [ct for ct in all_cost_types if ct.startswith(("HZ", "WW"))]
587+
588+
out: dict[str, MeterReading] = {}
589+
for ct in wanted:
590+
out[ct] = await self.get_meter_reading(cost_type=ct)
588591
return out
589592

590593
async def get_supported_cost_types(self) -> dict[str, set[str]]:
@@ -682,6 +685,39 @@ async def get_monthly_consumption(
682685

683686
return []
684687

688+
async def get_monthly_consumptions(
689+
self,
690+
kind: ReadingKind,
691+
*,
692+
in_kwh: bool = True,
693+
) -> dict[str, list[Reading]]:
694+
"""
695+
Fetch monthly consumption series for *all* cost types matching `kind`.
696+
697+
Output is keyed by `cost_type` (e.g. HZ01, HZ02, WW01...).
698+
"""
699+
dates = await self.get_dashboard_dates()
700+
results = (dates.get("d") or {}).get("results") or []
701+
period = next((p for p in results if isinstance(p, dict)), None)
702+
if not period:
703+
return {}
704+
705+
unit_rows = ((period.get("Units") or {}).get("results") or [])
706+
all_cost_types = sorted(
707+
{
708+
r["CostType"]
709+
for r in unit_rows
710+
if isinstance(r, dict) and isinstance(r.get("CostType"), str)
711+
}
712+
)
713+
prefix = "HZ" if kind == ReadingKind.heating else "WW"
714+
wanted = [ct for ct in all_cost_types if ct.startswith(prefix)]
715+
716+
out: dict[str, list[Reading]] = {}
717+
for ct in wanted:
718+
out[ct] = await self.get_monthly_consumption(cost_type=ct, in_kwh=in_kwh)
719+
return out
720+
685721
async def get_current_consumption(self, kind: ReadingKind) -> CurrentConsumption:
686722
"""
687723
Returns the value shown in the dashboard cards:

tests/test_client.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

33
import asyncio
4+
from datetime import UTC, datetime
45

56
import httpx
67

78
from brunata_api.client import BrunataClient
9+
from brunata_api.models import MeterReading, Reading, ReadingKind
810

911

1012
def test_extract_json_objects_finds_embedded_json():
@@ -104,3 +106,89 @@ async def fake_dashboard_batch_get(_: str):
104106
assert readings[0].value == 10.0
105107
assert readings[1].value == 20.0
106108

109+
110+
def test_get_meter_readings_returns_all_hz_ww_cost_types_without_network():
111+
client = BrunataClient(base_url="https://example.invalid", username="u", password="p")
112+
client._logged_in = True # avoid login()
113+
114+
async def fake_get_dashboard_dates():
115+
return {
116+
"d": {
117+
"results": [
118+
{
119+
"Units": {
120+
"results": [
121+
{"CostType": "HZ01"},
122+
{"CostType": "HZ02"},
123+
{"CostType": "WW01"},
124+
{"CostType": "XX99"},
125+
]
126+
}
127+
}
128+
]
129+
}
130+
}
131+
132+
async def fake_get_meter_reading(*, cost_type: str) -> MeterReading:
133+
kind = ReadingKind.heating if cost_type.startswith("HZ") else ReadingKind.hot_water
134+
return MeterReading(
135+
timestamp=datetime(2025, 1, 1, tzinfo=UTC),
136+
value=1.0,
137+
unit="Einh.",
138+
cost_type=cost_type,
139+
kind=kind,
140+
)
141+
142+
client.get_dashboard_dates = fake_get_dashboard_dates # type: ignore[method-assign]
143+
client.get_meter_reading = fake_get_meter_reading # type: ignore[method-assign]
144+
145+
out = asyncio.run(client.get_meter_readings())
146+
assert set(out.keys()) == {"HZ01", "HZ02", "WW01"}
147+
assert out["HZ02"].cost_type == "HZ02"
148+
149+
150+
def test_get_monthly_consumptions_returns_all_cost_types_without_network():
151+
client = BrunataClient(base_url="https://example.invalid", username="u", password="p")
152+
client._logged_in = True # avoid login()
153+
154+
async def fake_get_dashboard_dates():
155+
return {
156+
"d": {
157+
"results": [
158+
{
159+
"Units": {
160+
"results": [
161+
{"CostType": "HZ01"},
162+
{"CostType": "HZ02"},
163+
{"CostType": "WW01"},
164+
]
165+
}
166+
}
167+
]
168+
}
169+
}
170+
171+
seen: list[tuple[str, bool]] = []
172+
173+
async def fake_get_monthly_consumption(*, cost_type: str, in_kwh: bool = True) -> list[Reading]:
174+
seen.append((cost_type, in_kwh))
175+
kind = ReadingKind.heating if cost_type.startswith("HZ") else ReadingKind.hot_water
176+
return [
177+
Reading(
178+
timestamp=datetime(2025, 1, 31, tzinfo=UTC),
179+
value=10.0,
180+
unit="kWh" if in_kwh else "m³",
181+
kind=kind,
182+
)
183+
]
184+
185+
client.get_dashboard_dates = fake_get_dashboard_dates # type: ignore[method-assign]
186+
client.get_monthly_consumption = fake_get_monthly_consumption # type: ignore[method-assign]
187+
188+
out_hz = asyncio.run(client.get_monthly_consumptions(ReadingKind.heating, in_kwh=True))
189+
assert set(out_hz.keys()) == {"HZ01", "HZ02"}
190+
assert seen and all(in_kwh is True for _, in_kwh in seen)
191+
192+
out_ww = asyncio.run(client.get_monthly_consumptions(ReadingKind.hot_water, in_kwh=False))
193+
assert set(out_ww.keys()) == {"WW01"}
194+

0 commit comments

Comments
 (0)