1+ from __future__ import annotations
2+
3+ from datetime import datetime
4+ from typing import Any
5+
6+ from homeassistant .config_entries import ConfigEntry
7+ from homeassistant .core import HomeAssistant , callback
8+ from homeassistant .helpers .event import async_track_utc_time_change
9+
10+ # Stałe i „klucze” współdzielone w integracji:
11+ # - DOMAIN: nazwa domeny integracji (np. "pstryk")
12+ # - PLATFORMS: lista platform HA, które rejestrujemy (np. ["sensor"])
13+ # - CONF_*: nazwy pól przechowywanych w entry.data (tokeny, user_id)
14+ # - DATA_*: klucze do słownika hass.data[DOMAIN][entry_id]
15+ from .const import (
16+ DOMAIN ,
17+ PLATFORMS ,
18+ CONF_USER_ID ,
19+ CONF_ACCESS ,
20+ CONF_REFRESH ,
21+ DATA_CLIENT ,
22+ DATA_COORDINATOR ,
23+ DATA_UNSUB ,
24+ )
25+
26+ # Koordynator to warstwa „cache + harmonogram” z HA (DataUpdateCoordinator),
27+ # do której podłączają się encje (np. sensory), aby pobierać z niej aktualne dane.
28+ from .coordinator import PstrykCoordinator
29+
30+ # Klient HTTP do Pstryk API. Potrafi:
31+ # - zbudować się z tokenów (from_tokens)
32+ # - odświeżyć access_token (refresh_access)
33+ # - samodzielnie retry’ować request po 401 (w _get_json)
34+ from .pstryklib .pstryk_api_client import PstrykApiClient
35+
36+
37+ async def async_setup (hass : HomeAssistant , config : dict ) -> bool :
38+ """Minimalny setup integracji na starcie HA.
39+
40+ Wywoływany wcześnie, przed wczytaniem config entries.
41+ W tej integracji nic tu nie robimy — zwracamy True, aby HA wiedział, że „setup” się powiódł.
42+ """
43+ return True
44+
45+
46+ async def async_setup_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
47+ """Główna inicjalizacja JEDNEGO wpisu konfiguracyjnego (ConfigEntry).
48+
49+ Co się tu dzieje (kolejność kroków):
50+ 1) Przygotowanie przestrzeni w hass.data i lok. store na uchwyty tej instancji.
51+ 2) Odczyt tokenów i user_id z entry.data zapisanych przez config_flow.
52+ 3) Jeśli tokenów brakuje — inicjujemy reauth i PRZERYWAMY setup (return False).
53+ 4) Tworzymy klienta API z tokenów.
54+ 5) Wykonujemy „opportunistic refresh” access_token na starcie (jeśli refresh_token ważny).
55+ - jeżeli access się zmienił, aktualizujemy entry.data (persist).
56+ - jeśli refresh się nie uda (np. refresh_token wygasł) — odpalamy reauth i PRZERYWAMY setup.
57+ 6) Tworzymy koordynatora + robimy pierwsze odświeżenie danych.
58+ 7) Rejestrujemy zegarowy callback (UTC) na HH:59:30, który żąda odświeżenia koordynatora.
59+ Uwaga: to idzie w UTC — przy DST offset względem czasu lokalnego się zmienia.
60+ 8) Odkładamy uchwyty (client, coordinator, unsub) do hass.data[DOMAIN][entry_id].
61+ 9) Forwardujemy setup platform (np. sensor.py).
62+ """
63+
64+ # 1) Struktura pamięci dla tej instancji wpisu (entry)
65+ hass .data .setdefault (DOMAIN , {})
66+ store : dict [str , Any ] = {}
67+
68+ # 2) Dane konfiguracyjne zapisane przez config_flow (tokeny, user_id)
69+ data = entry .data or {}
70+ access = data .get (CONF_ACCESS )
71+ refresh = data .get (CONF_REFRESH )
72+ user_id = data .get (CONF_USER_ID )
73+
74+ # 3) Jeśli nie mamy kompletu tokenów — nie uruchamiamy pół-aktywnej integracji.
75+ # Wywołujemy reauth flow i kończymy. Po udanym reauth HA ponownie spróbuje setup_entry.
76+ if not access or not refresh :
77+ hass .async_create_task (
78+ hass .config_entries .flow .async_init (
79+ DOMAIN ,
80+ context = {"source" : "reauth" , "entry_id" : entry .entry_id },
81+ data = entry .data ,
82+ )
83+ )
84+ return False
85+
86+ # 4) Budujemy klienta z tokenów. Klient sam umie odświeżyć access przy 401.
87+ client = PstrykApiClient .from_tokens (access = access , refresh = refresh , user_id = user_id )
88+
89+ # 5) „Opportunistic refresh”: na starcie odśwież access_token (o ile refresh_token jeszcze żyje).
90+ # Dzięki temu od początku mamy „świeży” access i unikamy 401 przy pierwszych callach koordynatora.
91+ try :
92+ old_access = access
93+ await client .refresh_access ()
94+ if client .access_token and client .access_token != old_access :
95+ # Zapisz nowy access do entry.data (persist na dysk), opcjonalnie user_id jeśli backend zwrócił.
96+ new_data = dict (data )
97+ new_data [CONF_ACCESS ] = client .access_token
98+ new_data [CONF_USER_ID ] = client .user_id
99+ hass .config_entries .async_update_entry (entry , data = new_data )
100+ except Exception :
101+ # Jeżeli tu wpadniemy, to najczęściej znaczy: refresh_token wygasł / jest nieprawidłowy.
102+ # Uruchamiamy reauth i NIE podnosimy integracji (return False). Po reauth HA znów zawoła setup_entry.
103+ hass .async_create_task (
104+ hass .config_entries .flow .async_init (
105+ DOMAIN ,
106+ context = {"source" : "reauth" , "entry_id" : entry .entry_id },
107+ data = entry .data ,
108+ )
109+ )
110+ return False
111+
112+ # 6) Koordynator pobiera dane z API i udostępnia je encjom. Pierwsze odświeżenie na starcie.
113+ coordinator = PstrykCoordinator (hass , client )
114+ await coordinator .async_config_entry_first_refresh ()
115+
116+ # 7) Harmonogram odświeżeń: celowo „tuż przed” zmianą doby (xx:59:30 UTC),
117+ @callback
118+ async def _on_tick (now : datetime ) -> None :
119+ # Ręcznie wołamy koordynatora o refresh. Sam coordinator decyduje co i jak pobrać.
120+ await coordinator .async_request_refresh ()
121+
122+ unsub = async_track_utc_time_change (
123+ hass ,
124+ _on_tick ,
125+ minute = 59 ,
126+ second = 30 ,
127+ )
128+
129+ # 8) Odkładamy uchwyty do hass.data, żeby inne moduły (np. sensor.py) mogły ich użyć.
130+ # store trzymamy „per entry”, aby przy wielu licznikach każde entry miało własne zasoby.
131+ store [DATA_CLIENT ] = client
132+ store [DATA_COORDINATOR ] = coordinator
133+ store [DATA_UNSUB ] = unsub
134+ hass .data [DOMAIN ][entry .entry_id ] = store
135+
136+ # 9) Rejestrujemy platformy (np. tworzenie sensorów). HA zawoła odpowiednie pliki (sensor.py itd.).
137+ await hass .config_entries .async_forward_entry_setups (entry , PLATFORMS )
138+ return True
139+
140+
141+ async def async_unload_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
142+ """Sprzątanie po danym wpisie (ConfigEntry) przy wyłączaniu/usuwaniu integracji.
143+
144+ Kroki:
145+ 1) Wyjmij store dla tego entry z hass.data[DOMAIN].
146+ 2) Odsubskrybuj timer (jeśli był zarejestrowany).
147+ 3) Poproś HA o unload platform (usunie encje z UI i zatrzyma ich aktualizacje).
148+ 4) Zamknij klienta HTTP (zamyka sesję aiohttp).
149+ 5) Zwróć True/False czy unload platform się powiódł.
150+ """
151+
152+ # 1) Zdejmujemy uchwyty ze wspólnej przestrzeni. .pop() zwróci pusty dict, jeśli nie znajdzie.
153+ store = hass .data .get (DOMAIN , {}).pop (entry .entry_id , {})
154+
155+ # 2) Zdejmujemy harmonogram (jeśli był).
156+ unsub = store .get (DATA_UNSUB )
157+ if callable (unsub ):
158+ unsub ()
159+
160+ # 3) Odłącz platformy (np. sensory). Jeśli HA zwróci False — coś nie poszło.
161+ ok = await hass .config_entries .async_unload_platforms (entry , PLATFORMS )
162+
163+ # 4) Zamknij klienta (sesję HTTP). Defensywnie: jeśli klienta nie ma — pomiń.
164+ client = store .get (DATA_CLIENT )
165+ try :
166+ await client .aclose ()
167+ except Exception :
168+ pass
169+
170+ # 5) Zwracamy wynik unloadu platform. Jeśli False — HA może spróbować ponownie.
171+ return ok
0 commit comments