Skip to content

Commit bc4f03d

Browse files
authored
Merge pull request #19 from derolli1976/feature/tageserzeugung
* Added additional solar production sensor with daily reset * Also: More robust loading of wallbox entities (timeout issues)
2 parents bab155e + c3b96d5 commit bc4f03d

1 file changed

Lines changed: 126 additions & 16 deletions

File tree

custom_components/enpal_webparser/sensor.py

Lines changed: 126 additions & 16 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,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+
328436
class 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

351461
class WallboxModeSensor(WallboxCoordinatorEntity):

0 commit comments

Comments
 (0)