Skip to content

Commit 012f950

Browse files
authored
Merge pull request #11 from aLAN-LDZ/refactor_and_fifo
Refactor and fifo
2 parents 11d4d17 + 2de99c1 commit 012f950

10 files changed

Lines changed: 319 additions & 35 deletions

File tree

custom_components/solaraccelerator/__init__.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
9595
# przy starcie (z RestoreEntity) i przy każdej zmianie z UI.
9696
"command_delay": DEFAULT_COMMAND_DELAY,
9797
"verify_settling": DEFAULT_VERIFY_SETTLING,
98+
# Diagnostyka write_managera — kumulatywne statystyki per entity_id (retry, ok/fail)
99+
# i meta ostatniego batcha. Aktualizowane w WriteManager._process_batch.
100+
"write_stats": {
101+
"entities": {}, # entity_id → {total_commands, total_retries, last_retries, last_status, last_error, last_value, last_attempt_at}
102+
"last_batch_at": None,
103+
"last_batch_size": 0,
104+
"last_batch_acked": 0,
105+
"last_batch_failed": 0,
106+
"last_batch_retried": 0, # liczba komend które wymagały >=1 retry
107+
},
98108
}
99109

100110
# WriteManager — kolejka komend do falownika z worker'em w tle.
@@ -109,16 +119,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
109119

110120

111121
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
112-
"""Wyładuj integrację — anuluj taski w tle, zatrzymaj write_manager i odepnij platformy."""
113-
coordinator_data = hass.data[DOMAIN].get(entry.entry_id, {})
122+
"""Wyładuj integrację — zatrzymaj write_manager i odepnij platformy.
114123
115-
# Anuluj obie pętle w tle — inaczej zostałyby "wiszące" po reloadzie
116-
if task := coordinator_data.get("_task"):
117-
task.cancel()
118-
if live_task := coordinator_data.get("_live_task"):
119-
live_task.cancel()
124+
Pętle godzinowa/live są tworzone przez ``entry.async_create_background_task`` —
125+
HA anuluje je automatycznie przy unload. WriteManager tworzy własny task przez
126+
``hass.async_create_background_task`` więc go zatrzymujemy ręcznie.
127+
"""
128+
coordinator_data = hass.data[DOMAIN].get(entry.entry_id, {})
120129

121-
# Zatrzymaj worker'a write_managera
130+
# Zatrzymaj worker'a write_managera (osobny task niezwiązany z entry)
122131
if write_manager := coordinator_data.get("write_manager"):
123132
write_manager.stop()
124133

custom_components/solaraccelerator/const.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,14 @@
7575

7676
DEFAULT_VERIFY_SETTLING = 5.0 # sekund od ostatniego write do pierwszego verify
7777
MIN_VERIFY_SETTLING = 1.0
78-
MAX_VERIFY_SETTLING = 60.0
78+
MAX_VERIFY_SETTLING = 120.0
79+
80+
# Verify retry — gdy verify pokaże że falownik nie przyjął write (np. Modbus
81+
# odrzucił pakiet), ponawiamy execute+verify do MAX prób. Tylko dla komend
82+
# które wykonały się bez wyjątku — encja unavailable nie jest retry'owana.
83+
DEFAULT_VERIFY_RETRIES = 3 # liczba dodatkowych prób po pierwszym fail
84+
MIN_VERIFY_RETRIES = 0 # 0 = brak retry, klasyczne zachowanie
85+
MAX_VERIFY_RETRIES = 10
7986

8087
# Lista wszystkich pól które integracja może wysyłać do backendu.
8188
# Format: (key, description, unit, category)

custom_components/solaraccelerator/number.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@
2828

2929
from .const import (
3030
DEFAULT_COMMAND_DELAY,
31+
DEFAULT_VERIFY_RETRIES,
3132
DEFAULT_VERIFY_SETTLING,
3233
DOMAIN,
3334
MAX_COMMAND_DELAY,
35+
MAX_VERIFY_RETRIES,
3436
MAX_VERIFY_SETTLING,
3537
MIN_COMMAND_DELAY,
38+
MIN_VERIFY_RETRIES,
3639
MIN_VERIFY_SETTLING,
3740
)
3841

@@ -50,6 +53,7 @@ async def async_setup_entry(
5053
async_add_entities([
5154
SolarAcceleratorCommandDelayNumber(hass, entry, coordinator_data),
5255
SolarAcceleratorVerifySettlingNumber(hass, entry, coordinator_data),
56+
SolarAcceleratorVerifyRetriesNumber(hass, entry, coordinator_data),
5357
])
5458

5559

@@ -156,3 +160,25 @@ def __init__(
156160
"""Zainicjalizuj encję verify_settling."""
157161
super().__init__(hass, entry, coordinator_data, "verify_settling")
158162
self._attr_name = "Opóźnienie przed verify"
163+
164+
165+
class SolarAcceleratorVerifyRetriesNumber(_ConfigNumberBase):
166+
"""Liczba dodatkowych prób execute+verify gdy pierwszy verify się nie powiódł."""
167+
168+
# Liczba prób — bez jednostki "s"
169+
_attr_native_unit_of_measurement = None
170+
_attr_icon = "mdi:reload"
171+
_attr_translation_key = "verify_retries"
172+
_attr_native_min_value = MIN_VERIFY_RETRIES
173+
_attr_native_max_value = MAX_VERIFY_RETRIES
174+
_attr_native_step = 1
175+
176+
_data_key = "verify_retries"
177+
_default_value = DEFAULT_VERIFY_RETRIES
178+
179+
def __init__(
180+
self, hass: HomeAssistant, entry: ConfigEntry, coordinator_data: dict[str, Any]
181+
) -> None:
182+
"""Zainicjalizuj encję verify_retries."""
183+
super().__init__(hass, entry, coordinator_data, "verify_retries")
184+
self._attr_name = "Liczba prób verify"

custom_components/solaraccelerator/sensor.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
SolarAcceleratorNextScheduledSensor,
4343
SolarAcceleratorPriceProviderSensor,
4444
SolarAcceleratorStatusSensor,
45+
SolarAcceleratorWriteStatsSensor,
4546
)
4647

4748
_LOGGER = logging.getLogger(__name__)
@@ -84,21 +85,30 @@ async def async_setup_entry(
8485
SolarAcceleratorLiveStatusSensor(hass, entry, coordinator_data),
8586
SolarAcceleratorLiveLastPushSensor(hass, entry, coordinator_data),
8687
SolarAcceleratorLiveIntervalSensor(hass, entry, coordinator_data),
88+
# Diagnostyka write_managera — retry/status per sterowana encja
89+
SolarAcceleratorWriteStatsSensor(hass, entry, coordinator_data),
8790
])
8891

8992
# Pobierz ceny i zysk od razu na starcie — żeby sensory nie świeciły "unknown"
90-
# przed pierwszą pełną godziną
91-
hass.async_create_task(async_fetch_prices(hass, coordinator_data))
92-
hass.async_create_task(async_fetch_profit(hass, coordinator_data))
93+
# przed pierwszą pełną godziną. Background — żeby wolny serwer nie blokował bootstrap.
94+
entry.async_create_background_task(
95+
hass, async_fetch_prices(hass, coordinator_data), "sa_fetch_prices_init"
96+
)
97+
entry.async_create_background_task(
98+
hass, async_fetch_profit(hass, coordinator_data), "sa_fetch_profit_init"
99+
)
93100

94-
# Pętla godzinowa — pełna paczka danych co pełną godzinę
95-
task = hass.async_create_task(
96-
async_send_data_hourly(hass, entry, coordinator_data)
101+
# Pętla godzinowa — pełna paczka danych co pełną godzinę.
102+
# background_task = HA nie czeka na nią podczas bootstrap (to nieskończony while True).
103+
entry.async_create_background_task(
104+
hass,
105+
async_send_data_hourly(hass, entry, coordinator_data),
106+
"sa_send_data_hourly",
97107
)
98-
coordinator_data["_task"] = task
99108

100109
# Pętla live — szybki push stanu i odbiór komend co kilkanaście sekund
101-
live_task = hass.async_create_task(
102-
async_send_live_data_loop(hass, entry, coordinator_data)
110+
entry.async_create_background_task(
111+
hass,
112+
async_send_live_data_loop(hass, entry, coordinator_data),
113+
"sa_send_live_data_loop",
103114
)
104-
coordinator_data["_live_task"] = live_task

custom_components/solaraccelerator/sensors/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
SolarAcceleratorLastSentSensor,
1515
SolarAcceleratorNextScheduledSensor,
1616
SolarAcceleratorStatusSensor,
17+
SolarAcceleratorWriteStatsSensor,
1718
)
1819
from .live import (
1920
SolarAcceleratorLiveIntervalSensor,
@@ -45,6 +46,7 @@
4546
"SolarAcceleratorLastSentSensor",
4647
"SolarAcceleratorNextScheduledSensor",
4748
"SolarAcceleratorEntitiesCountSensor",
49+
"SolarAcceleratorWriteStatsSensor",
4850
# ceny
4951
"SolarAcceleratorCurrentBuyPriceSensor",
5052
"SolarAcceleratorMinBuyPriceSensor",

custom_components/solaraccelerator/sensors/diagnostic.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@
2727
)
2828
from ._base import SolarAcceleratorSensorBase
2929

30+
# Pusta diagnostyka — gdy write_manager jeszcze nie miał żadnego batcha
31+
_EMPTY_WRITE_STATS: dict[str, Any] = {
32+
"entities": {},
33+
"last_batch_at": None,
34+
"last_batch_size": 0,
35+
"last_batch_acked": 0,
36+
"last_batch_failed": 0,
37+
"last_batch_retried": 0,
38+
}
39+
3040

3141
class SolarAcceleratorStatusSensor(SolarAcceleratorSensorBase):
3242
"""Status połączenia z backendem (``connected``/``auth_error``/``error``/``disconnected``)."""
@@ -187,3 +197,59 @@ def extra_state_attributes(self) -> dict[str, Any]:
187197
attrs["ev_enabled"] = False
188198

189199
return attrs
200+
201+
202+
class SolarAcceleratorWriteStatsSensor(SolarAcceleratorSensorBase):
203+
"""Diagnostyka write_managera — retry i finalny status per sterowana encja.
204+
205+
Main state = liczba komend z ostatniego batcha które wymagały retry (0 = OK).
206+
Atrybuty zawierają meta ostatniego batcha + kumulatywne statystyki per entity_id
207+
od startu integracji: ile razy komenda dla danej encji się powiodła, ile razy
208+
musieliśmy retry'ować, jaka była ostatnia żądana wartość i finalny status.
209+
210+
Sensor odświeża się natychmiast po każdym batchu — WriteManager woła
211+
``coordinator_data['write_stats_notify']`` zaraz po ACK'owaniu komend.
212+
"""
213+
214+
_attr_icon = "mdi:reload-alert"
215+
_attr_state_class = SensorStateClass.MEASUREMENT
216+
_attr_translation_key = "write_stats"
217+
_attr_entity_category = EntityCategory.DIAGNOSTIC
218+
219+
def __init__(
220+
self, hass: HomeAssistant, entry: ConfigEntry, coordinator_data: dict[str, Any]
221+
) -> None:
222+
"""Zainicjalizuj sensor diagnostyki write_managera."""
223+
super().__init__(hass, entry, coordinator_data, "write_stats")
224+
self._attr_name = "Diagnostyka komend"
225+
226+
async def async_added_to_hass(self) -> None:
227+
"""Po dodaniu do HA zarejestruj notifier — write_manager woła go po każdym batchu."""
228+
await super().async_added_to_hass()
229+
# Schedule_update_ha_state można wołać synchronicznie z dowolnego kontekstu
230+
self.coordinator_data["write_stats_notify"] = self.async_schedule_update_ha_state
231+
232+
async def async_will_remove_from_hass(self) -> None:
233+
"""Wyczyść notifier — uniknij wołania callbacka po unmount."""
234+
if self.coordinator_data.get("write_stats_notify") == self.async_schedule_update_ha_state:
235+
self.coordinator_data.pop("write_stats_notify", None)
236+
await super().async_will_remove_from_hass()
237+
238+
@property
239+
def native_value(self) -> int:
240+
"""Liczba komend z ostatniego batcha które wymagały co najmniej 1 retry."""
241+
stats = self.coordinator_data.get("write_stats") or _EMPTY_WRITE_STATS
242+
return int(stats.get("last_batch_retried", 0))
243+
244+
@property
245+
def extra_state_attributes(self) -> dict[str, Any]:
246+
"""Pełna diagnostyka batcha + kumulatywne statystyki per entity."""
247+
stats = self.coordinator_data.get("write_stats") or _EMPTY_WRITE_STATS
248+
return {
249+
"last_batch_at": stats.get("last_batch_at"),
250+
"last_batch_size": stats.get("last_batch_size", 0),
251+
"last_batch_acked": stats.get("last_batch_acked", 0),
252+
"last_batch_failed": stats.get("last_batch_failed", 0),
253+
"last_batch_retried": stats.get("last_batch_retried", 0),
254+
"entities": stats.get("entities", {}),
255+
}

custom_components/solaraccelerator/strings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
"entities_sent": {
191191
"name": "Wysłane encje"
192192
},
193+
"write_stats": {
194+
"name": "Diagnostyka komend"
195+
},
193196
"current_buy_price": {
194197
"name": "Cena zakupu energii"
195198
},
@@ -261,6 +264,9 @@
261264
},
262265
"verify_settling": {
263266
"name": "Opóźnienie przed verify"
267+
},
268+
"verify_retries": {
269+
"name": "Liczba prób verify"
264270
}
265271
}
266272
}

custom_components/solaraccelerator/translations/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
"entities_sent": {
191191
"name": "Entities Sent"
192192
},
193+
"write_stats": {
194+
"name": "Command diagnostics"
195+
},
193196
"current_buy_price": {
194197
"name": "Buy Energy Price"
195198
},
@@ -261,6 +264,9 @@
261264
},
262265
"verify_settling": {
263266
"name": "Settling delay before verify"
267+
},
268+
"verify_retries": {
269+
"name": "Verify retries"
264270
}
265271
}
266272
}

custom_components/solaraccelerator/translations/pl.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@
190190
"entities_sent": {
191191
"name": "Wysłane encje"
192192
},
193+
"write_stats": {
194+
"name": "Diagnostyka komend"
195+
},
193196
"current_buy_price": {
194197
"name": "Cena zakupu energii"
195198
},
@@ -261,6 +264,9 @@
261264
},
262265
"verify_settling": {
263266
"name": "Opóźnienie przed verify"
267+
},
268+
"verify_retries": {
269+
"name": "Liczba prób verify"
264270
}
265271
}
266272
}

0 commit comments

Comments
 (0)