|
13 | 13 | import httpx |
14 | 14 |
|
15 | 15 | from .errors import LoginError |
16 | | -from .models import Reading, ReadingKind |
| 16 | +from .models import CurrentConsumption, MeterReading, Reading, ReadingKind |
17 | 17 |
|
18 | 18 |
|
19 | 19 | def _default_headers() -> dict[str, str]: |
@@ -495,6 +495,98 @@ async def get_dashboard_dates(self) -> dict[str, Any]: |
495 | 495 | ) |
496 | 496 | return await self._dashboard_batch_get(rel) |
497 | 497 |
|
| 498 | + @staticmethod |
| 499 | + def _parse_sap_number(value: str) -> float: |
| 500 | + # Values may be padded with spaces and use dot decimals. |
| 501 | + return float(value.strip().replace(",", ".")) |
| 502 | + |
| 503 | + async def get_meter_reading(self, *, cost_type: str) -> MeterReading: |
| 504 | + """ |
| 505 | + Get the cumulative meter/index value (Zählerstand) for a cost type. |
| 506 | +
|
| 507 | + Uses `NP_DASHBOARD_SRV/CumuConsumptionSet`. |
| 508 | + """ |
| 509 | + if not self._logged_in: |
| 510 | + await self.login() |
| 511 | + if not self._user_unit_id: |
| 512 | + raise LoginError("Missing UserUnitID.") |
| 513 | + |
| 514 | + dates = await self.get_dashboard_dates() |
| 515 | + results = (dates.get("d") or {}).get("results") or [] |
| 516 | + if not results: |
| 517 | + raise LoginError("No dashboard periods found.") |
| 518 | + |
| 519 | + period = next((p for p in results if isinstance(p, dict)), None) |
| 520 | + if not period: |
| 521 | + raise LoginError("No dashboard period object found.") |
| 522 | + |
| 523 | + bis_raw = period.get("Bisdatum") |
| 524 | + if not isinstance(bis_raw, str): |
| 525 | + raise LoginError("Missing Bisdatum in dashboard period.") |
| 526 | + |
| 527 | + bis_dt = self._sap_date_to_datetime(bis_raw) |
| 528 | + if not bis_dt: |
| 529 | + raise LoginError("Could not parse Bisdatum.") |
| 530 | + |
| 531 | + bis_filter_dt = bis_dt.replace(hour=0, minute=0, second=0, microsecond=0) + timedelta( |
| 532 | + hours=23 |
| 533 | + ) |
| 534 | + bis_filter = bis_filter_dt.strftime("%Y-%m-%dT%H:%M:%S") |
| 535 | + |
| 536 | + rel = ( |
| 537 | + f"CumuConsumptionSet?sap-client={quote(self.sap_client)}&$filter=" |
| 538 | + f"Nutzein%20eq%20%27{quote(self._user_unit_id)}%27%20and%20" |
| 539 | + f"Bis%20eq%20datetime%27{quote(bis_filter)}%27%20and%20" |
| 540 | + f"Kotyp%20eq%20%27{quote(cost_type)}%27" |
| 541 | + ) |
| 542 | + |
| 543 | + data = await self._dashboard_batch_get(rel) |
| 544 | + rows = (data.get("d") or {}).get("results") or [] |
| 545 | + if not rows: |
| 546 | + raise LoginError(f"No CumuConsumptionSet results for cost type {cost_type}.") |
| 547 | + |
| 548 | + row = rows[0] |
| 549 | + val_raw = row.get("Verbrauch") |
| 550 | + if not isinstance(val_raw, str): |
| 551 | + raise LoginError("CumuConsumptionSet missing Verbrauch.") |
| 552 | + value = self._parse_sap_number(val_raw) |
| 553 | + unit = row.get("MassreadTxt") or row.get("Massread") |
| 554 | + unit_s = str(unit) if unit else None |
| 555 | + kind = ReadingKind.heating if cost_type.startswith("HZ") else ReadingKind.hot_water |
| 556 | + |
| 557 | + # CumuConsumptionSet uses the period end (Bis) as the timestamp. |
| 558 | + return MeterReading( |
| 559 | + timestamp=bis_dt, |
| 560 | + value=value, |
| 561 | + unit=unit_s, |
| 562 | + cost_type=cost_type, |
| 563 | + kind=kind, |
| 564 | + ) |
| 565 | + |
| 566 | + async def get_meter_readings(self) -> list[MeterReading]: |
| 567 | + """ |
| 568 | + Convenience helper: return meter readings for the first HZ.. and WW.. cost types. |
| 569 | + """ |
| 570 | + dates = await self.get_dashboard_dates() |
| 571 | + results = (dates.get("d") or {}).get("results") or [] |
| 572 | + period = next((p for p in results if isinstance(p, dict)), None) |
| 573 | + if not period: |
| 574 | + return [] |
| 575 | + 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)) |
| 588 | + return out |
| 589 | + |
498 | 590 | async def get_supported_cost_types(self) -> dict[str, set[str]]: |
499 | 591 | """ |
500 | 592 | Return cost types available per period. |
@@ -590,6 +682,51 @@ async def get_monthly_consumption( |
590 | 682 |
|
591 | 683 | return [] |
592 | 684 |
|
| 685 | + async def get_current_consumption(self, kind: ReadingKind) -> CurrentConsumption: |
| 686 | + """ |
| 687 | + Returns the value shown in the dashboard cards: |
| 688 | + 'aktueller Verbrauch' and 'aktueller Verbrauch vom <date>'. |
| 689 | +
|
| 690 | + Implementation: sum the monthly kWh series for the active period. |
| 691 | + """ |
| 692 | + if not self._logged_in: |
| 693 | + await self.login() |
| 694 | + |
| 695 | + dates = await self.get_dashboard_dates() |
| 696 | + results = (dates.get("d") or {}).get("results") or [] |
| 697 | + period = next((p for p in results if isinstance(p, dict)), None) |
| 698 | + if not period: |
| 699 | + raise LoginError("No dashboard period found.") |
| 700 | + |
| 701 | + bis_raw = period.get("Bisdatum") |
| 702 | + if not isinstance(bis_raw, str): |
| 703 | + raise LoginError("Missing Bisdatum in dashboard period.") |
| 704 | + as_of = self._sap_date_to_datetime(bis_raw) |
| 705 | + if not as_of: |
| 706 | + raise LoginError("Could not parse Bisdatum.") |
| 707 | + |
| 708 | + unit_rows = ((period.get("Units") or {}).get("results") or []) |
| 709 | + cost_types = [ |
| 710 | + r.get("CostType") |
| 711 | + for r in unit_rows |
| 712 | + if isinstance(r, dict) and isinstance(r.get("CostType"), str) |
| 713 | + ] |
| 714 | + |
| 715 | + if kind == ReadingKind.heating: |
| 716 | + cost_type = next((ct for ct in cost_types if ct.startswith("HZ")), "HZ01") |
| 717 | + else: |
| 718 | + cost_type = next((ct for ct in cost_types if ct.startswith("WW")), "WW01") |
| 719 | + |
| 720 | + monthly = await self.get_monthly_consumption(cost_type=cost_type, in_kwh=True) |
| 721 | + total = round(sum(r.value for r in monthly), 3) |
| 722 | + return CurrentConsumption( |
| 723 | + as_of=as_of, |
| 724 | + value=total, |
| 725 | + unit="kWh", |
| 726 | + kind=kind, |
| 727 | + cost_type=cost_type, |
| 728 | + ) |
| 729 | + |
593 | 730 | async def get_readings(self, kind: ReadingKind) -> list[Reading]: |
594 | 731 | if not self._logged_in: |
595 | 732 | await self.login() |
|
0 commit comments