Skip to content

Commit bb8d1cb

Browse files
authored
Add files via upload
1 parent e0b7860 commit bb8d1cb

6 files changed

Lines changed: 1312 additions & 7 deletions

File tree

custom_components/erovinieta/__init__.py

Lines changed: 211 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from homeassistant.core import HomeAssistant
2020
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
2121
from homeassistant.helpers import config_validation as cv
22+
from homeassistant.helpers.event import async_track_time_interval
2223
from homeassistant.helpers.typing import ConfigType
2324

2425
from .api import ErovinietaAPI
@@ -28,10 +29,12 @@
2829
CONF_USERNAME,
2930
DEFAULT_UPDATE_INTERVAL,
3031
DOMAIN,
32+
LICENSE_DATA_KEY,
3133
PLATFORMS,
3234
)
3335
from .coordinator import ErovinietaCoordinator
3436
from .exceptions import ErovinietaAuthError, ErovinietaConnectionError
37+
from .license import LicenseManager
3538

3639
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
3740

@@ -45,6 +48,77 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
4548

4649
async 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

116189
async 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

Comments
 (0)