Skip to content

Commit c3b96d5

Browse files
committed
Sensor for daily solar production (reset at 0:00h)
1 parent 9a1fe9a commit c3b96d5

1 file changed

Lines changed: 80 additions & 60 deletions

File tree

custom_components/enpal_webparser/sensor.py

Lines changed: 80 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
from datetime import timedelta, datetime
2-
import re
1+
from datetime import datetime, timedelta
2+
import asyncio
33
import logging
4+
import re
5+
46
import aiohttp
57
from bs4 import BeautifulSoup
68

7-
from homeassistant.core import HomeAssistant
9+
from homeassistant.core import HomeAssistant, callback
810
from homeassistant.components.sensor import SensorEntity
911
from homeassistant.config_entries import ConfigEntry
1012
from homeassistant.helpers.entity_platform import AddEntitiesCallback
1113
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, CoordinatorEntity, UpdateFailed
1214
from homeassistant.helpers.entity import EntityCategory
1315
from homeassistant.helpers.restore_state import RestoreEntity
14-
16+
from homeassistant.helpers.event import async_track_state_change_event
1517

1618
from .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,26 +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))
176-
entities.append(DailyResetEnergySensor(hass, coordinator, "Inverter: Energy produced today (DC)"))
190+
entities.append(DailyResetFromEntitySensor(hass, "sensor.inverter_energy_produced_total_dc"))
191+
177192

178193

179194
if entry.options.get("use_wallbox_addon", False):
180195
wallbox_url = f"{DEFAULT_WALLBOX_API_ENDPOINT}/status"
181-
182196
_LOGGER.info("[Enpal] Wallbox add-on enabled, URL: %s", wallbox_url)
183197

198+
wallbox_data = {}
199+
184200
async def async_wallbox_update():
201+
nonlocal wallbox_data
185202
try:
186203
async with aiohttp.ClientSession() as session:
187-
async with session.get(wallbox_url, timeout=30) as resp:
204+
async with session.get(wallbox_url, timeout=10) as resp:
188205
if resp.status != 200:
189-
raise UpdateFailed(f"Wallbox API Error: {resp.status}")
206+
raise Exception(f"Wallbox API Error: {resp.status}")
190207
data = await resp.json()
191208
_LOGGER.debug("[Enpal] Wallbox status data: %s", data)
209+
wallbox_data = data
192210
return data
193211
except Exception as e:
194-
_LOGGER.exception("[Enpal] Error fetching wallbox status: %s", e)
195-
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}")
196218

197219
wallbox_coordinator = DataUpdateCoordinator(
198220
hass,
@@ -202,12 +224,16 @@ async def async_wallbox_update():
202224
update_interval=timedelta(seconds=interval),
203225
)
204226

205-
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())
206229

207230
entities.extend([
208231
WallboxModeSensor(wallbox_coordinator),
209232
WallboxStatusSensor(wallbox_coordinator),
210233
])
234+
_LOGGER.debug("[Enpal] Wallbox-Sensoren hinzugefügt")
235+
236+
211237

212238
async_add_entities(entities)
213239

@@ -327,19 +353,19 @@ def device_info(self):
327353
}
328354

329355

330-
class DailyResetEnergySensor(SensorEntity, RestoreEntity):
331-
def __init__(self, hass: HomeAssistant, coordinator: DataUpdateCoordinator, sensor_name: str):
356+
357+
358+
class DailyResetFromEntitySensor(SensorEntity, RestoreEntity):
359+
def __init__(self, hass: HomeAssistant, source_entity_id: str):
332360
self.hass = hass
333-
self._attr_name = "Inverter: Energy produced today (DC)"
361+
self._attr_name = "Inverter: Energy produced total (DC)"
334362
self._attr_unique_id = "daily_energy_produced_dc_kwh"
335363
self._attr_device_class = "energy"
336364
self._attr_state_class = "total"
337365
self._attr_native_unit_of_measurement = "kWh"
338366
self._attr_icon = "mdi:calendar-refresh"
339-
self._coordinator = coordinator
340-
self._source_uid = make_id(sensor_name)
367+
self._source_entity_id = source_entity_id
341368
self._today_start_value = None
342-
self._last_total = None
343369
self._value = 0.0
344370
self._last_reset = datetime.now().date()
345371

@@ -351,59 +377,50 @@ def native_value(self):
351377
def extra_state_attributes(self):
352378
return {
353379
"last_reset": self._last_reset.isoformat(),
354-
"start_value": self._today_start_value,
355-
"last_total": self._last_total,
380+
"start_value": self._today_start_value if self._today_start_value is not None else "Not set"
356381
}
357382

358383
async def async_added_to_hass(self):
359-
await super().async_added_to_hass()
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+
)
360391
last_state = await self.async_get_last_state()
361-
362392
if last_state and last_state.state not in (None, 'unknown', 'unavailable'):
363393
try:
364394
self._value = float(last_state.state)
365-
_LOGGER.info("[Enpal] Restored daily value: %.3f kWh", self._value)
366-
except ValueError:
367-
self._value = 0.0
368-
369-
self._today_start_value = self._try_restore_float(last_state.attributes.get("start_value"))
370-
self._last_total = self._try_restore_float(last_state.attributes.get("last_total"))
371-
last_reset_str = last_state.attributes.get("last_reset")
372-
if last_reset_str:
373-
try:
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:
374398
self._last_reset = datetime.fromisoformat(last_reset_str).date()
375-
except Exception:
376-
self._last_reset = datetime.now().date()
399+
except Exception:
400+
pass
377401

378-
self._coordinator.async_add_listener(self._handle_coordinator_update)
379-
380-
def _try_restore_float(self, value):
402+
def _try_float(self, val):
381403
try:
382-
return float(value)
404+
return float(val)
383405
except (TypeError, ValueError):
384406
return None
385407

386-
def _handle_coordinator_update(self):
408+
@callback
409+
def _handle_state_update(self, event):
387410
today = datetime.now().date()
388-
for sensor in self._coordinator.data:
389-
if make_id(sensor["name"]) == self._source_uid:
390-
try:
391-
total_kwh = float(sensor["value"])
392-
_LOGGER.debug("[Enpal] Tageswert: Gesamt=%.3f, Start=%.3f", total_kwh, self._today_start_value or 0.0)
393-
394-
if self._last_reset != today or self._today_start_value is None:
395-
_LOGGER.info("[Enpal] Neuer Tag erkannt – Tagesstartwert: %.3f", total_kwh)
396-
self._today_start_value = total_kwh
397-
self._last_reset = today
398-
self._value = 0.0
411+
try:
412+
new_total = float(event.data["new_state"].state)
413+
except (TypeError, ValueError):
414+
return
399415

400-
self._value = max(total_kwh - self._today_start_value, 0)
401-
self._last_total = total_kwh
402-
except Exception as e:
403-
_LOGGER.warning("[Enpal] Fehler bei Tageswert-Berechnung: %s", e)
404-
break
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)
405422

406-
self.async_write_ha_state()
423+
self.async_schedule_update_ha_state()
407424

408425
@property
409426
def device_info(self):
@@ -415,6 +432,7 @@ def device_info(self):
415432
}
416433

417434

435+
418436
class WallboxCoordinatorEntity(CoordinatorEntity, SensorEntity):
419437
def __init__(self, coordinator, name, unique_id, key):
420438
super().__init__(coordinator)
@@ -435,7 +453,9 @@ def device_info(self):
435453

436454
@property
437455
def native_value(self):
438-
return self.coordinator.data.get(self._key)
456+
if self.coordinator.data:
457+
return self.coordinator.data.get(self._key)
458+
return None
439459

440460

441461
class WallboxModeSensor(WallboxCoordinatorEntity):

0 commit comments

Comments
 (0)