1919from homeassistant .core import HomeAssistant
2020from homeassistant .exceptions import ConfigEntryAuthFailed , ConfigEntryNotReady
2121from homeassistant .helpers import config_validation as cv
22+ from homeassistant .helpers .event import async_track_time_interval
2223from homeassistant .helpers .typing import ConfigType
2324
2425from .api import ErovinietaAPI
2829 CONF_USERNAME ,
2930 DEFAULT_UPDATE_INTERVAL ,
3031 DOMAIN ,
32+ LICENSE_DATA_KEY ,
3133 PLATFORMS ,
3234)
3335from .coordinator import ErovinietaCoordinator
3436from .exceptions import ErovinietaAuthError , ErovinietaConnectionError
37+ from .license import LicenseManager
3538
3639CONFIG_SCHEMA = cv .config_entry_only_config_schema (DOMAIN )
3740
@@ -45,6 +48,77 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
4548
4649async def async_setup_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
4750 """Configurează integrarea dintr-o intrare de configurare."""
51+ hass .data .setdefault (DOMAIN , {})
52+
53+ # ── Inițializare License Manager (o singură instanță per domeniu) ──
54+ if LICENSE_DATA_KEY not in hass .data [DOMAIN ]:
55+ _LOGGER .debug ("[eRovinieta] Inițializez LicenseManager (prima entry)" )
56+ license_mgr = LicenseManager (hass )
57+ await license_mgr .async_load ()
58+ hass .data [DOMAIN ][LICENSE_DATA_KEY ] = license_mgr
59+ _LOGGER .debug (
60+ "[eRovinieta] LicenseManager: status=%s, valid=%s, fingerprint=%s..." ,
61+ license_mgr .status ,
62+ license_mgr .is_valid ,
63+ license_mgr .fingerprint [:16 ],
64+ )
65+
66+ # Heartbeat periodic — intervalul vine de la server (via valid_until)
67+ interval_sec = license_mgr .check_interval_seconds
68+ _LOGGER .debug (
69+ "[eRovinieta] Programez heartbeat periodic la fiecare %d secunde (%d ore)" ,
70+ interval_sec ,
71+ interval_sec // 3600 ,
72+ )
73+
74+ async def _heartbeat_periodic (_now ) -> None :
75+ """Verifică statusul la server dacă cache-ul a expirat."""
76+ mgr : LicenseManager | None = hass .data .get (DOMAIN , {}).get (
77+ LICENSE_DATA_KEY
78+ )
79+ if not mgr :
80+ _LOGGER .debug ("[eRovinieta] Heartbeat: LicenseManager nu există, skip" )
81+ return
82+ if mgr .needs_heartbeat :
83+ _LOGGER .debug ("[eRovinieta] Heartbeat: cache expirat, verific la server" )
84+ await mgr .async_heartbeat ()
85+ else :
86+ _LOGGER .debug ("[eRovinieta] Heartbeat: cache valid, nu e nevoie de verificare" )
87+
88+ cancel_heartbeat = async_track_time_interval (
89+ hass ,
90+ _heartbeat_periodic ,
91+ timedelta (seconds = interval_sec ),
92+ )
93+ hass .data [DOMAIN ]["_cancel_heartbeat" ] = cancel_heartbeat
94+ _LOGGER .debug ("[eRovinieta] Heartbeat programat și stocat în hass.data" )
95+
96+ # ── Notificare re-enable (dacă a fost dezactivată anterior) ──
97+ was_disabled = hass .data .pop (f"{ DOMAIN } _was_disabled" , False )
98+ if was_disabled :
99+ await license_mgr .async_notify_event ("integration_enabled" )
100+
101+ if not license_mgr .is_valid :
102+ _LOGGER .warning (
103+ "[eRovinieta] Integrarea nu are licență validă. "
104+ "Senzorii vor afișa 'Licență necesară'."
105+ )
106+ elif license_mgr .is_trial_valid :
107+ _LOGGER .info (
108+ "[eRovinieta] Perioadă de evaluare — %d zile rămase" ,
109+ license_mgr .trial_days_remaining ,
110+ )
111+ else :
112+ _LOGGER .info (
113+ "[eRovinieta] Licență activă — tip: %s" ,
114+ license_mgr .license_type ,
115+ )
116+ else :
117+ _LOGGER .debug (
118+ "[eRovinieta] LicenseManager există deja (entry suplimentară)"
119+ )
120+
121+ # ── Setup API + Coordinator (logica originală) ──
48122 session = aiohttp .ClientSession (
49123 timeout = aiohttp .ClientTimeout (total = 30 )
50124 )
@@ -82,7 +156,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
82156 raise
83157
84158 # Stocăm coordinatorul
85- hass .data .setdefault (DOMAIN , {})
86159 hass .data [DOMAIN ][entry .entry_id ] = coordinator
87160
88161 # Configurăm platformele
@@ -115,18 +188,152 @@ async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
115188
116189async def async_unload_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
117190 """Dezinstalează integrarea și eliberează resursele."""
191+ _LOGGER .info (
192+ "[eRovinieta] ── async_unload_entry ── entry_id=%s (%s)" ,
193+ entry .entry_id ,
194+ entry .data .get (CONF_USERNAME ),
195+ )
196+
118197 unload_ok = await hass .config_entries .async_unload_platforms (
119198 entry , PLATFORMS
120199 )
200+ _LOGGER .debug ("[eRovinieta] Unload platforme: %s" , "OK" if unload_ok else "EȘUAT" )
121201
122202 if unload_ok and entry .entry_id in hass .data .get (DOMAIN , {}):
123203 coordinator : ErovinietaCoordinator = hass .data [DOMAIN ].pop (
124204 entry .entry_id
125205 )
126206 await coordinator .api .close ()
127- _LOGGER .info (
128- "Integrarea eRovinieta a fost eliminată pentru %s" ,
129- entry .data [CONF_USERNAME ],
207+
208+ # Verifică dacă mai sunt entry-uri active (ignoră cheile interne)
209+ chei_interne = {LICENSE_DATA_KEY , "_cancel_heartbeat" }
210+ entry_ids_ramase = {
211+ k for k in hass .data .get (DOMAIN , {})
212+ if k not in chei_interne
213+ }
214+
215+ _LOGGER .debug (
216+ "[eRovinieta] Entry-uri rămase după unload: %d (%s)" ,
217+ len (entry_ids_ramase ),
218+ entry_ids_ramase or "niciuna" ,
130219 )
131220
221+ if not entry_ids_ramase :
222+ _LOGGER .info ("[eRovinieta] Ultima entry descărcată — curăț domeniul complet" )
223+
224+ # ── Notificare lifecycle (înainte de cleanup!) ──
225+ mgr = hass .data [DOMAIN ].get (LICENSE_DATA_KEY )
226+ if mgr and not hass .is_stopping :
227+ if entry .disabled_by :
228+ await mgr .async_notify_event ("integration_disabled" )
229+ # Flag pentru async_setup_entry: la re-enable, trimitem "enabled"
230+ hass .data [f"{ DOMAIN } _was_disabled" ] = True
231+ else :
232+ # Salvăm fingerprint-ul pentru async_remove_entry
233+ hass .data .setdefault (f"{ DOMAIN } _notify" , {}).update ({
234+ "fingerprint" : mgr .fingerprint ,
235+ "license_key" : mgr ._data .get ("license_key" , "" ),
236+ })
237+ _LOGGER .debug (
238+ "[eRovinieta] Fingerprint salvat pentru async_remove_entry"
239+ )
240+
241+ # Oprește heartbeat-ul periodic
242+ cancel_hb = hass .data [DOMAIN ].pop ("_cancel_heartbeat" , None )
243+ if cancel_hb :
244+ cancel_hb ()
245+ _LOGGER .debug ("[eRovinieta] Heartbeat periodic oprit" )
246+
247+ # Elimină LicenseManager
248+ hass .data [DOMAIN ].pop (LICENSE_DATA_KEY , None )
249+ _LOGGER .debug ("[eRovinieta] LicenseManager eliminat" )
250+
251+ # Elimină domeniul complet
252+ hass .data .pop (DOMAIN , None )
253+ _LOGGER .debug ("[eRovinieta] hass.data[%s] eliminat complet" , DOMAIN )
254+
255+ _LOGGER .info ("[eRovinieta] Cleanup complet — domeniul %s descărcat" , DOMAIN )
256+ else :
257+ _LOGGER .info (
258+ "Integrarea eRovinieta a fost eliminată pentru %s" ,
259+ entry .data [CONF_USERNAME ],
260+ )
261+ else :
262+ _LOGGER .error ("[eRovinieta] Unload EȘUAT pentru entry_id=%s" , entry .entry_id )
263+
132264 return unload_ok
265+
266+
267+ async def async_remove_entry (hass : HomeAssistant , entry : ConfigEntry ) -> None :
268+ """Curăță complet la ștergerea integrării.
269+
270+ Dacă e ultima entry, notifică serverul de licențe.
271+ """
272+ _LOGGER .debug (
273+ "[eRovinieta] ── async_remove_entry ── entry_id=%s (%s)" ,
274+ entry .entry_id ,
275+ entry .data .get (CONF_USERNAME ),
276+ )
277+
278+ # ── Notificare licență (doar la ultima entry) ──
279+ remaining = hass .config_entries .async_entries (DOMAIN )
280+ if not remaining :
281+ notify_data = hass .data .pop (f"{ DOMAIN } _notify" , None )
282+ if notify_data and notify_data .get ("fingerprint" ):
283+ await _send_lifecycle_event (
284+ notify_data ["fingerprint" ],
285+ notify_data .get ("license_key" , "" ),
286+ "integration_removed" ,
287+ )
288+
289+
290+ async def _send_lifecycle_event (
291+ fingerprint : str , license_key : str , action : str
292+ ) -> None :
293+ """Trimite un eveniment lifecycle direct (fără LicenseManager).
294+
295+ Folosit în async_remove_entry când LicenseManager nu mai există.
296+ """
297+ import hashlib
298+ import hmac as hmac_lib
299+ import json
300+ import time
301+
302+ import aiohttp
303+
304+ from .license import INTEGRATION , LICENSE_API_URL
305+
306+ timestamp = int (time .time ())
307+ payload = {
308+ "fingerprint" : fingerprint ,
309+ "timestamp" : timestamp ,
310+ "action" : action ,
311+ "license_key" : license_key ,
312+ "integration" : INTEGRATION ,
313+ }
314+ data = {k : v for k , v in payload .items () if k != "hmac" }
315+ msg = json .dumps (data , sort_keys = True ).encode ()
316+ payload ["hmac" ] = hmac_lib .new (
317+ fingerprint .encode (), msg , hashlib .sha256
318+ ).hexdigest ()
319+
320+ try :
321+ async with aiohttp .ClientSession () as session :
322+ async with session .post (
323+ f"{ LICENSE_API_URL } /notify" ,
324+ json = payload ,
325+ timeout = aiohttp .ClientTimeout (total = 10 ),
326+ headers = {
327+ "Content-Type" : "application/json" ,
328+ "User-Agent" : "eRovinieta-HA-Integration/3.0" ,
329+ },
330+ ) as resp :
331+ if resp .status == 200 :
332+ result = await resp .json ()
333+ if not result .get ("success" ):
334+ _LOGGER .warning (
335+ "[eRovinieta] Server a refuzat '%s': %s" ,
336+ action , result .get ("error" ),
337+ )
338+ except Exception as err : # noqa: BLE001
339+ _LOGGER .debug ("[eRovinieta] Nu s-a putut raporta '%s': %s" , action , err )
0 commit comments