1- from datetime import timedelta , datetime
2- import re
1+ from datetime import datetime , timedelta
2+ import asyncio
33import logging
4+ import re
5+
46import aiohttp
57from bs4 import BeautifulSoup
68
7- from homeassistant .core import HomeAssistant
9+ from homeassistant .core import HomeAssistant , callback
810from homeassistant .components .sensor import SensorEntity
911from homeassistant .config_entries import ConfigEntry
1012from homeassistant .helpers .entity_platform import AddEntitiesCallback
1113from homeassistant .helpers .update_coordinator import DataUpdateCoordinator , CoordinatorEntity , UpdateFailed
1214from homeassistant .helpers .entity import EntityCategory
1315from homeassistant .helpers .restore_state import RestoreEntity
14-
16+ from homeassistant . helpers . event import async_track_state_change_event
1517
1618from .const import DOMAIN , DEFAULT_INTERVAL , DEFAULT_URL , DEFAULT_WALLBOX_API_ENDPOINT
1719
20+
1821_LOGGER = logging .getLogger (__name__ )
1922
2023
@@ -75,12 +78,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e
7578 groups = entry .options .get ("groups" , [])
7679 _LOGGER .debug ("[Enpal] Configuration - URL: %s, Interval: %s, Groups: %s" , url , interval , groups )
7780
81+ last_successful_data = []
82+
7883 async def async_update_data ():
84+ nonlocal last_successful_data
7985 try :
8086 async with aiohttp .ClientSession () as session :
8187 async with session .get (url , timeout = 30 ) as resp :
8288 if resp .status != 200 :
83- raise UpdateFailed (f"Unexpected status code: { resp .status } " )
89+ raise Exception (f"Unexpected status code: { resp .status } " )
8490 html = await resp .text ()
8591
8692 _LOGGER .debug ("[Enpal] HTML content fetched successfully from %s" , url )
@@ -92,7 +98,6 @@ async def async_update_data():
9298 for card in cards :
9399 group = card .find ("h2" ).text .strip ()
94100 if group not in groups :
95- _LOGGER .debug ("[Enpal] Skipping group not in selected groups: %s" , group )
96101 continue
97102
98103 rows = card .find_all ("tr" )[1 :]
@@ -139,14 +144,19 @@ async def async_update_data():
139144 "enabled" : group in groups ,
140145 "enpal_last_update" : timestamp_iso
141146 }
142- _LOGGER .debug ("[Enpal] Parsed sensor: %s" , sensor_data )
143147 sensors .append (sensor_data )
144148
145149 _LOGGER .info ("[Enpal] Loaded %d sensor(s) from HTML" , len (sensors ))
150+ last_successful_data = sensors
146151 return sensors
152+
147153 except Exception as e :
148- _LOGGER .exception ("[Enpal] Error during HTML parsing or request: %s" , e )
149- raise UpdateFailed (f"Data fetch failed: { e } " )
154+ if last_successful_data :
155+ _LOGGER .warning ("[Enpal] Error during update, using last known good values: %s" , e )
156+ return last_successful_data
157+ else :
158+ _LOGGER .exception ("[Enpal] No previous data available" )
159+ raise UpdateFailed (f"Initial data fetch failed: { e } " )
150160
151161 coordinator = DataUpdateCoordinator (
152162 hass ,
@@ -165,6 +175,10 @@ async def async_update_data():
165175 hass .data [DOMAIN ]["coordinator" ] = coordinator
166176
167177 await coordinator .async_config_entry_first_refresh ()
178+ _LOGGER .info ("[Enpal] Verfügbare Sensoren nach HTML-Parsing:" )
179+ for sensor in coordinator .data :
180+ _LOGGER .info ("[Enpal] Name: %s -> UID: %s" , sensor ["name" ], make_id (sensor ["name" ]))
181+
168182
169183 entities = []
170184 for sensor in coordinator .data :
@@ -173,24 +187,34 @@ async def async_update_data():
173187 entities .append (EnpalSensor (uid , sensor , coordinator ))
174188
175189 entities .append (CumulativeEnergySensor (hass , coordinator , "Inverter Power DC Total (Huawei)" , interval ))
190+ entities .append (DailyResetFromEntitySensor (hass , "sensor.inverter_energy_produced_total_dc" ))
191+
192+
176193
177194 if entry .options .get ("use_wallbox_addon" , False ):
178195 wallbox_url = f"{ DEFAULT_WALLBOX_API_ENDPOINT } /status"
179-
180196 _LOGGER .info ("[Enpal] Wallbox add-on enabled, URL: %s" , wallbox_url )
181197
198+ wallbox_data = {}
199+
182200 async def async_wallbox_update ():
201+ nonlocal wallbox_data
183202 try :
184203 async with aiohttp .ClientSession () as session :
185- async with session .get (wallbox_url , timeout = 30 ) as resp :
204+ async with session .get (wallbox_url , timeout = 10 ) as resp :
186205 if resp .status != 200 :
187- raise UpdateFailed (f"Wallbox API Error: { resp .status } " )
206+ raise Exception (f"Wallbox API Error: { resp .status } " )
188207 data = await resp .json ()
189208 _LOGGER .debug ("[Enpal] Wallbox status data: %s" , data )
209+ wallbox_data = data
190210 return data
191211 except Exception as e :
192- _LOGGER .exception ("[Enpal] Error fetching wallbox status: %s" , e )
193- raise UpdateFailed (f"Wallbox update failed: { e } " )
212+ if wallbox_data :
213+ _LOGGER .warning ("[Enpal] Wallbox update failed – using last known data: %s" , e )
214+ return wallbox_data
215+ else :
216+ _LOGGER .warning ("[Enpal] Wallbox update failed – no previous data yet: %s" , e )
217+ raise UpdateFailed (f"Wallbox update failed and no previous data: { e } " )
194218
195219 wallbox_coordinator = DataUpdateCoordinator (
196220 hass ,
@@ -200,12 +224,16 @@ async def async_wallbox_update():
200224 update_interval = timedelta (seconds = interval ),
201225 )
202226
203- await wallbox_coordinator .async_config_entry_first_refresh ()
227+ # KEIN await – Hintergrund-Task starten, damit Home Assistant nicht crasht
228+ hass .async_create_task (wallbox_coordinator .async_refresh ())
204229
205230 entities .extend ([
206231 WallboxModeSensor (wallbox_coordinator ),
207232 WallboxStatusSensor (wallbox_coordinator ),
208233 ])
234+ _LOGGER .debug ("[Enpal] Wallbox-Sensoren hinzugefügt" )
235+
236+
209237
210238 async_add_entities (entities )
211239
@@ -325,6 +353,86 @@ def device_info(self):
325353 }
326354
327355
356+
357+
358+ class DailyResetFromEntitySensor (SensorEntity , RestoreEntity ):
359+ def __init__ (self , hass : HomeAssistant , source_entity_id : str ):
360+ self .hass = hass
361+ self ._attr_name = "Inverter: Energy produced total (DC)"
362+ self ._attr_unique_id = "daily_energy_produced_dc_kwh"
363+ self ._attr_device_class = "energy"
364+ self ._attr_state_class = "total"
365+ self ._attr_native_unit_of_measurement = "kWh"
366+ self ._attr_icon = "mdi:calendar-refresh"
367+ self ._source_entity_id = source_entity_id
368+ self ._today_start_value = None
369+ self ._value = 0.0
370+ self ._last_reset = datetime .now ().date ()
371+
372+ @property
373+ def native_value (self ):
374+ return round (self ._value or 0.0 , 3 )
375+
376+ @property
377+ def extra_state_attributes (self ):
378+ return {
379+ "last_reset" : self ._last_reset .isoformat (),
380+ "start_value" : self ._today_start_value if self ._today_start_value is not None else "Not set"
381+ }
382+
383+ async def async_added_to_hass (self ):
384+ await RestoreEntity .async_added_to_hass (self )
385+ # Statt self.hass.helpers.event.async_track_state_change_event(...),
386+ # verwende den importierten async_track_state_change_event:
387+
388+ async_track_state_change_event (
389+ self .hass , self ._source_entity_id , self ._handle_state_update
390+ )
391+ last_state = await self .async_get_last_state ()
392+ if last_state and last_state .state not in (None , 'unknown' , 'unavailable' ):
393+ try :
394+ self ._value = float (last_state .state )
395+ self ._today_start_value = self ._try_float (last_state .attributes .get ("start_value" ))
396+ last_reset_str = last_state .attributes .get ("last_reset" )
397+ if last_reset_str :
398+ self ._last_reset = datetime .fromisoformat (last_reset_str ).date ()
399+ except Exception :
400+ pass
401+
402+ def _try_float (self , val ):
403+ try :
404+ return float (val )
405+ except (TypeError , ValueError ):
406+ return None
407+
408+ @callback
409+ def _handle_state_update (self , event ):
410+ today = datetime .now ().date ()
411+ try :
412+ new_total = float (event .data ["new_state" ].state )
413+ except (TypeError , ValueError ):
414+ return
415+
416+ if self ._today_start_value is None or self ._last_reset != today :
417+ self ._today_start_value = new_total
418+ self ._last_reset = today
419+ self ._value = 0.0
420+ else :
421+ self ._value = max (new_total - self ._today_start_value , 0 )
422+
423+ self .async_schedule_update_ha_state ()
424+
425+ @property
426+ def device_info (self ):
427+ return {
428+ "identifiers" : {(DOMAIN , "enpal_device" )},
429+ "name" : "Enpal Webgerät" ,
430+ "manufacturer" : "Enpal" ,
431+ "model" : "Webparser" ,
432+ }
433+
434+
435+
328436class WallboxCoordinatorEntity (CoordinatorEntity , SensorEntity ):
329437 def __init__ (self , coordinator , name , unique_id , key ):
330438 super ().__init__ (coordinator )
@@ -345,7 +453,9 @@ def device_info(self):
345453
346454 @property
347455 def native_value (self ):
348- return self .coordinator .data .get (self ._key )
456+ if self .coordinator .data :
457+ return self .coordinator .data .get (self ._key )
458+ return None
349459
350460
351461class WallboxModeSensor (WallboxCoordinatorEntity ):
0 commit comments