1313import httpx
1414
1515from .errors import LoginError
16- from .models import CurrentConsumption , MeterReading , Reading , ReadingKind
16+ from .models import CurrentConsumption , MeterReading , Period , Reading , ReadingKind
1717
1818
1919def _default_headers () -> dict [str , str ]:
@@ -495,16 +495,45 @@ async def get_dashboard_dates(self) -> dict[str, Any]:
495495 )
496496 return await self ._dashboard_batch_get (rel )
497497
498+ async def get_periods (self ) -> list [Period ]:
499+ """
500+ Return all dashboard periods (e.g. calendar years) as start/end datetimes.
501+
502+ Useful for integrations to show "which year" a value belongs to or to request
503+ a specific period via period_index. Values from meter/current consumption are
504+ per-period and typically reset when a new period (e.g. new year) starts.
505+ """
506+ dates = await self .get_dashboard_dates ()
507+ results = (dates .get ("d" ) or {}).get ("results" ) or []
508+ out : list [Period ] = []
509+ for p in results :
510+ if not isinstance (p , dict ):
511+ continue
512+ ab_raw = p .get ("Abdatum" )
513+ bis_raw = p .get ("Bisdatum" )
514+ if not isinstance (ab_raw , str ) or not isinstance (bis_raw , str ):
515+ continue
516+ start = self ._sap_date_to_datetime (ab_raw )
517+ end = self ._sap_date_to_datetime (bis_raw )
518+ if start and end :
519+ out .append (Period (start = start , end = end ))
520+ return out
521+
498522 @staticmethod
499523 def _parse_sap_number (value : str ) -> float :
500524 # Values may be padded with spaces and use dot decimals.
501525 return float (value .strip ().replace ("," , "." ))
502526
503- async def get_meter_reading (self , * , cost_type : str ) -> MeterReading :
527+ async def get_meter_reading (
528+ self , * , cost_type : str , period_index : int = 0
529+ ) -> MeterReading :
504530 """
505531 Get the cumulative meter/index value (Zählerstand) for a cost type.
506532
507- Uses `NP_DASHBOARD_SRV/CumuConsumptionSet`.
533+ Uses `NP_DASHBOARD_SRV/CumuConsumptionSet`. Values are per dashboard period
534+ (e.g. calendar year); they typically reset when a new period starts. Use
535+ get_periods() to list periods and period_index to request a specific one
536+ (0 = first, often the current period).
508537 """
509538 if not self ._logged_in :
510539 await self .login ()
@@ -513,12 +542,14 @@ async def get_meter_reading(self, *, cost_type: str) -> MeterReading:
513542
514543 dates = await self .get_dashboard_dates ()
515544 results = (dates .get ("d" ) or {}).get ("results" ) or []
516- if not results :
545+ periods_list = [p for p in results if isinstance (p , dict )]
546+ if not periods_list :
517547 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." )
548+ if period_index < 0 or period_index >= len (periods_list ):
549+ raise LoginError (
550+ f"period_index { period_index } out of range (0..{ len (periods_list ) - 1 } )."
551+ )
552+ period = periods_list [period_index ]
522553
523554 bis_raw = period .get ("Bisdatum" )
524555 if not isinstance (bis_raw , str ):
@@ -563,17 +594,21 @@ async def get_meter_reading(self, *, cost_type: str) -> MeterReading:
563594 kind = kind ,
564595 )
565596
566- async def get_meter_readings (self ) -> dict [str , MeterReading ]:
597+ async def get_meter_readings (
598+ self , * , period_index : int = 0
599+ ) -> dict [str , MeterReading ]:
567600 """
568- Convenience helper: return meter readings for *all* HZ.. and WW.. cost types.
601+ Return meter readings for *all* HZ.. and WW.. cost types in the given period .
569602
570- Output is keyed by `cost_type` (stable mapping for HA entity IDs).
603+ Output is keyed by `cost_type`. Values are per-period and may reset each year.
604+ Use period_index to select period (0 = first, e.g. current year).
571605 """
572606 dates = await self .get_dashboard_dates ()
573607 results = (dates .get ("d" ) or {}).get ("results" ) or []
574- period = next (( p for p in results if isinstance (p , dict )), None )
575- if not period :
608+ periods_list = [ p for p in results if isinstance (p , dict )]
609+ if not periods_list or period_index < 0 or period_index >= len ( periods_list ) :
576610 return {}
611+ period = periods_list [period_index ]
577612
578613 unit_rows = ((period .get ("Units" ) or {}).get ("results" ) or [])
579614 all_cost_types = sorted (
@@ -587,7 +622,9 @@ async def get_meter_readings(self) -> dict[str, MeterReading]:
587622
588623 out : dict [str , MeterReading ] = {}
589624 for ct in wanted :
590- out [ct ] = await self .get_meter_reading (cost_type = ct )
625+ out [ct ] = await self .get_meter_reading (
626+ cost_type = ct , period_index = period_index
627+ )
591628 return out
592629
593630 async def get_supported_cost_types (self ) -> dict [str , set [str ]]:
@@ -618,9 +655,13 @@ async def get_monthly_consumption(
618655 * ,
619656 cost_type : str ,
620657 in_kwh : bool = True ,
658+ period_index : int | None = None ,
621659 ) -> list [Reading ]:
622660 """
623661 Fetch monthly consumption series for a given CostType (e.g. HZ01, WW01).
662+
663+ If period_index is None, tries all periods and returns the first with data.
664+ If set, only that period is queried (0 = first, e.g. current year).
624665 """
625666 kind = ReadingKind .heating if cost_type .startswith ("HZ" ) else ReadingKind .hot_water
626667 if not self ._logged_in :
@@ -630,9 +671,15 @@ async def get_monthly_consumption(
630671
631672 dates = await self .get_dashboard_dates ()
632673 results = (dates .get ("d" ) or {}).get ("results" ) or []
633- periods : list [dict [str , Any ]] = [p for p in results if isinstance (p , dict )]
674+ periods_list : list [dict [str , Any ]] = [
675+ p for p in results if isinstance (p , dict )
676+ ]
677+ if period_index is not None :
678+ if period_index < 0 or period_index >= len (periods_list ):
679+ return []
680+ periods_list = [periods_list [period_index ]]
634681
635- for period in periods :
682+ for period in periods_list :
636683 bis_raw = period .get ("Bisdatum" )
637684 if not isinstance (bis_raw , str ):
638685 continue
@@ -690,17 +737,19 @@ async def get_monthly_consumptions(
690737 kind : ReadingKind ,
691738 * ,
692739 in_kwh : bool = True ,
740+ period_index : int = 0 ,
693741 ) -> dict [str , list [Reading ]]:
694742 """
695743 Fetch monthly consumption series for *all* cost types matching `kind`.
696744
697- Output is keyed by `cost_type` (e.g. HZ01, HZ02, WW01... ).
745+ Output is keyed by `cost_type`. Use period_index to select period (0 = first ).
698746 """
699747 dates = await self .get_dashboard_dates ()
700748 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 :
749+ periods_list = [ p for p in results if isinstance (p , dict )]
750+ if not periods_list or period_index < 0 or period_index >= len ( periods_list ) :
703751 return {}
752+ period = periods_list [period_index ]
704753
705754 unit_rows = ((period .get ("Units" ) or {}).get ("results" ) or [])
706755 all_cost_types = sorted (
@@ -715,24 +764,34 @@ async def get_monthly_consumptions(
715764
716765 out : dict [str , list [Reading ]] = {}
717766 for ct in wanted :
718- out [ct ] = await self .get_monthly_consumption (cost_type = ct , in_kwh = in_kwh )
767+ out [ct ] = await self .get_monthly_consumption (
768+ cost_type = ct , in_kwh = in_kwh , period_index = period_index
769+ )
719770 return out
720771
721- async def get_current_consumption (self , kind : ReadingKind ) -> CurrentConsumption :
772+ async def get_current_consumption (
773+ self , kind : ReadingKind , * , period_index : int = 0
774+ ) -> CurrentConsumption :
722775 """
723- Returns the value shown in the dashboard cards:
724- 'aktueller Verbrauch' and 'aktueller Verbrauch vom <date>'.
776+ Return the dashboard-style cumulative consumption (e.g. YTD) for the period.
725777
726- Implementation: sum the monthly kWh series for the active period.
778+ This is the sum of monthly kWh for the selected period, so it resets when a
779+ new period (e.g. new year) starts. Use period_index to select period
780+ (0 = first, often current year). as_of is the period end date.
727781 """
728782 if not self ._logged_in :
729783 await self .login ()
730784
731785 dates = await self .get_dashboard_dates ()
732786 results = (dates .get ("d" ) or {}).get ("results" ) or []
733- period = next (( p for p in results if isinstance (p , dict )), None )
734- if not period :
787+ periods_list = [ p for p in results if isinstance (p , dict )]
788+ if not periods_list :
735789 raise LoginError ("No dashboard period found." )
790+ if period_index < 0 or period_index >= len (periods_list ):
791+ raise LoginError (
792+ f"period_index { period_index } out of range (0..{ len (periods_list ) - 1 } )."
793+ )
794+ period = periods_list [period_index ]
736795
737796 bis_raw = period .get ("Bisdatum" )
738797 if not isinstance (bis_raw , str ):
@@ -753,7 +812,9 @@ async def get_current_consumption(self, kind: ReadingKind) -> CurrentConsumption
753812 else :
754813 cost_type = next ((ct for ct in cost_types if ct .startswith ("WW" )), "WW01" )
755814
756- monthly = await self .get_monthly_consumption (cost_type = cost_type , in_kwh = True )
815+ monthly = await self .get_monthly_consumption (
816+ cost_type = cost_type , in_kwh = True , period_index = period_index
817+ )
757818 total = round (sum (r .value for r in monthly ), 3 )
758819 return CurrentConsumption (
759820 as_of = as_of ,
0 commit comments