2
2
3
3
from collections .abc import Mapping
4
4
from dataclasses import dataclass
5
- from datetime import date
5
+ from datetime import date , datetime , time
6
6
from typing import Any
7
+ from zoneinfo import ZoneInfo
7
8
8
- from pysuez import PySuezError , SuezClient
9
+ from pysuez import DayDataResult , PySuezError , SuezClient
9
10
11
+ from homeassistant .components .recorder import get_instance
12
+ from homeassistant .components .recorder .models import StatisticData , StatisticMetaData
13
+ from homeassistant .components .recorder .statistics import (
14
+ StatisticsRow ,
15
+ async_add_external_statistics ,
16
+ get_last_statistics ,
17
+ )
10
18
from homeassistant .config_entries import ConfigEntry
11
- from homeassistant .const import CONF_PASSWORD , CONF_USERNAME
19
+ from homeassistant .const import (
20
+ CONF_PASSWORD ,
21
+ CONF_USERNAME ,
22
+ CURRENCY_EURO ,
23
+ UnitOfVolume ,
24
+ )
12
25
from homeassistant .core import _LOGGER , HomeAssistant
13
26
from homeassistant .exceptions import ConfigEntryError
14
27
from homeassistant .helpers .update_coordinator import DataUpdateCoordinator , UpdateFailed
@@ -53,6 +66,12 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
53
66
always_update = True ,
54
67
config_entry = config_entry ,
55
68
)
69
+ self ._counter_id = self .config_entry .data [CONF_COUNTER_ID ]
70
+ self ._cost_statistic_id = f"{ DOMAIN } :{ self ._counter_id } _water_cost_statistics"
71
+ self ._water_statistic_id = (
72
+ f"{ DOMAIN } :{ self ._counter_id } _water_consumption_statistics"
73
+ )
74
+ self .config_entry .async_on_unload (self ._clear_statistics )
56
75
57
76
async def _async_setup (self ) -> None :
58
77
self ._suez_client = SuezClient (
@@ -79,10 +98,142 @@ async def _async_update_data(self) -> SuezWaterData:
79
98
},
80
99
price = (await self ._suez_client .get_price ()).price ,
81
100
)
101
+ await self ._update_statistics (data .price )
82
102
except PySuezError as err :
83
103
_LOGGER .exception (err )
84
104
raise UpdateFailed (
85
105
f"Suez coordinator error communicating with API: { err } "
86
106
) from err
87
107
_LOGGER .debug ("Successfully fetched suez data" )
88
108
return data
109
+
110
+ async def _update_statistics (self , current_price : float ) -> None :
111
+ """Update daily statistics."""
112
+ _LOGGER .debug ("Updating statistics for %s" , self ._water_statistic_id )
113
+
114
+ water_last_stat = await self ._get_last_stat (self ._water_statistic_id )
115
+ cost_last_stat = await self ._get_last_stat (self ._cost_statistic_id )
116
+
117
+ consumption_sum = 0.0
118
+ cost_sum = 0.0
119
+ last_stats = None
120
+
121
+ if water_last_stat is not None :
122
+ last_stats = datetime .fromtimestamp (water_last_stat ["start" ]).date ()
123
+ if water_last_stat ["sum" ] is not None :
124
+ consumption_sum = water_last_stat ["sum" ]
125
+ if cost_last_stat is not None :
126
+ if cost_last_stat ["sum" ] is not None and cost_last_stat ["sum" ] is not None :
127
+ cost_sum = cost_last_stat ["sum" ]
128
+
129
+ _LOGGER .debug (
130
+ "Updating suez stat since %s for %s" ,
131
+ str (last_stats ),
132
+ water_last_stat ,
133
+ )
134
+ usage = await self ._suez_client .fetch_all_daily_data (
135
+ since = last_stats ,
136
+ )
137
+ if usage is None or len (usage ) <= 0 :
138
+ _LOGGER .debug ("No recent usage data. Skipping update" )
139
+ return
140
+ _LOGGER .debug ("fetched data: %s" , len (usage ))
141
+
142
+ consumption_statistics , cost_statistics = self ._build_statistics (
143
+ current_price , consumption_sum , cost_sum , last_stats , usage
144
+ )
145
+
146
+ self ._persist_statistics (consumption_statistics , cost_statistics )
147
+
148
+ def _build_statistics (
149
+ self ,
150
+ current_price : float ,
151
+ consumption_sum : float ,
152
+ cost_sum : float ,
153
+ last_stats : date | None ,
154
+ usage : list [DayDataResult ],
155
+ ) -> tuple [list [StatisticData ], list [StatisticData ]]:
156
+ """Build statistics data from fetched data."""
157
+ consumption_statistics = []
158
+ cost_statistics = []
159
+
160
+ for data in usage :
161
+ if last_stats is not None and data .date <= last_stats :
162
+ continue
163
+ consumption_date = datetime .combine (
164
+ data .date , time (0 , 0 , 0 , 0 ), ZoneInfo ("Europe/Paris" )
165
+ )
166
+
167
+ consumption_sum += data .day_consumption
168
+ consumption_statistics .append (
169
+ StatisticData (
170
+ start = consumption_date ,
171
+ state = data .day_consumption ,
172
+ sum = consumption_sum ,
173
+ )
174
+ )
175
+ day_cost = (data .day_consumption / 1000 ) * current_price
176
+ cost_sum += day_cost
177
+ cost_statistics .append (
178
+ StatisticData (
179
+ start = consumption_date ,
180
+ state = day_cost ,
181
+ sum = cost_sum ,
182
+ )
183
+ )
184
+
185
+ return consumption_statistics , cost_statistics
186
+
187
+ def _persist_statistics (
188
+ self ,
189
+ consumption_statistics : list [StatisticData ],
190
+ cost_statistics : list [StatisticData ],
191
+ ) -> None :
192
+ """Persist given statistics in recorder."""
193
+ consumption_metadata = self ._get_statistics_metadata (
194
+ id = self ._water_statistic_id , name = "Consumption" , unit = UnitOfVolume .LITERS
195
+ )
196
+ cost_metadata = self ._get_statistics_metadata (
197
+ id = self ._cost_statistic_id , name = "Cost" , unit = CURRENCY_EURO
198
+ )
199
+
200
+ _LOGGER .debug (
201
+ "Adding %s statistics for %s" ,
202
+ len (consumption_statistics ),
203
+ self ._water_statistic_id ,
204
+ )
205
+ async_add_external_statistics (
206
+ self .hass , consumption_metadata , consumption_statistics
207
+ )
208
+ async_add_external_statistics (self .hass , cost_metadata , cost_statistics )
209
+
210
+ _LOGGER .debug ("Updated statistics for %s" , self ._water_statistic_id )
211
+
212
+ def _get_statistics_metadata (
213
+ self , id : str , name : str , unit : str
214
+ ) -> StatisticMetaData :
215
+ """Build statistics metadata for requested configuration."""
216
+ return StatisticMetaData (
217
+ has_mean = False ,
218
+ has_sum = True ,
219
+ name = f"Suez water { name } { self ._counter_id } " ,
220
+ source = DOMAIN ,
221
+ statistic_id = id ,
222
+ unit_of_measurement = unit ,
223
+ )
224
+
225
+ async def _get_last_stat (self , id : str ) -> StatisticsRow | None :
226
+ """Find last registered statistics of given id."""
227
+ last_stat = await get_instance (self .hass ).async_add_executor_job (
228
+ get_last_statistics , self .hass , 1 , id , True , {"sum" }
229
+ )
230
+ if last_stat is None or len (last_stat ) == 0 :
231
+ return None
232
+ return last_stat [id ][0 ]
233
+
234
+ def _clear_statistics (self ) -> None :
235
+ """Clear suez water statistics."""
236
+ instance = get_instance (self .hass )
237
+ instance .async_clear_statistics (
238
+ [self ._water_statistic_id , self ._cost_statistic_id ]
239
+ )
0 commit comments