1 """The Netatmo data handler."""
3 from __future__
import annotations
5 from collections
import deque
6 from dataclasses
import dataclass
7 from datetime
import datetime, timedelta
8 from itertools
import islice
11 from typing
import Any
15 from pyatmo.modules.device_types
import (
16 DeviceCategory
as NetatmoDeviceCategory,
17 DeviceType
as NetatmoDeviceType,
24 async_dispatcher_connect,
25 async_dispatcher_send,
35 NETATMO_CREATE_BATTERY,
36 NETATMO_CREATE_CAMERA,
37 NETATMO_CREATE_CAMERA_LIGHT,
38 NETATMO_CREATE_CLIMATE,
42 NETATMO_CREATE_ROOM_SENSOR,
43 NETATMO_CREATE_SELECT,
44 NETATMO_CREATE_SENSOR,
45 NETATMO_CREATE_SWITCH,
46 NETATMO_CREATE_WEATHER_SENSOR,
50 WEBHOOK_NACAMERA_CONNECTION,
54 _LOGGER = logging.getLogger(__name__)
56 SIGNAL_NAME =
"signal_name"
61 PUBLIC = NetatmoDeviceType.public
65 ACCOUNT:
"async_update_topology",
66 HOME:
"async_update_status",
67 WEATHER:
"async_update_weather_stations",
68 AIR_CARE:
"async_update_air_care",
69 PUBLIC:
"async_update_public_weather",
70 EVENT:
"async_update_events",
91 """Netatmo device class."""
93 data_handler: NetatmoDataHandler
94 device: pyatmo.modules.Module
101 """Netatmo home class."""
103 data_handler: NetatmoDataHandler
111 """Netatmo room class."""
113 data_handler: NetatmoDataHandler
121 """Class for keeping track of Netatmo data class metadata."""
126 subscriptions: set[CALLBACK_TYPE |
None]
132 """Manages the Netatmo data handling."""
134 account: pyatmo.AsyncAccount
135 _interval_factor: int
137 def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) ->
None:
138 """Initialize self."""
141 self.
_auth_auth = hass.data[DOMAIN][config_entry.entry_id][AUTH]
142 self.publisher: dict[str, NetatmoPublisher] = {}
143 self._queue: deque = deque()
145 if config_entry.data[
"auth_implementation"] == cloud.DOMAIN:
155 """Set up the Netatmo data handler."""
165 f
"signal-{DOMAIN}-webhook-None",
172 await self.
subscribesubscribe(ACCOUNT, ACCOUNT,
None)
174 await self.
hasshass.config_entries.async_forward_entry_setups(
182 We do up to BATCH_SIZE calls in one update in order
183 to minimize the calls on the api service.
185 for data_class
in islice(self._queue, 0, BATCH_SIZE * self.
_interval_factor_interval_factor):
186 if data_class.next_scan >
time():
189 if publisher := data_class.name:
193 self.publisher[publisher].next_scan = (
194 time() + data_class.interval * 10
197 self.publisher[publisher].next_scan =
time() + data_class.interval
199 self._queue.rotate(BATCH_SIZE)
201 _LOGGER.debug(
"Calls per hour: %i", cph)
203 for publisher
in self.publisher.values():
204 publisher.next_scan += 60
211 """Prioritize data retrieval for given data class entry."""
212 self.publisher[signal_name].next_scan =
time()
213 self._queue.rotate(-(self._queue.index(self.publisher[signal_name])))
216 """Handle webhook events."""
217 if event[
"data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_ACTIVATION:
218 _LOGGER.debug(
"%s webhook successfully registered", MANUFACTURER)
221 elif event[
"data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_DEACTIVATION:
222 _LOGGER.debug(
"%s webhook unregistered", MANUFACTURER)
225 elif event[
"data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION:
226 _LOGGER.debug(
"%s camera reconnected", MANUFACTURER)
230 """Fetch data and notify."""
234 await getattr(self.
accountaccount, self.publisher[signal_name].method)(
235 **self.publisher[signal_name].kwargs
238 except (pyatmo.NoDevice, pyatmo.ApiError)
as err:
242 except (TimeoutError, aiohttp.ClientConnectorError)
as err:
246 for update_callback
in self.publisher[signal_name].subscriptions:
256 update_callback: CALLBACK_TYPE |
None,
259 """Subscribe to publisher."""
260 if signal_name
in self.publisher:
261 if update_callback
not in self.publisher[signal_name].subscriptions:
262 self.publisher[signal_name].subscriptions.add(update_callback)
265 if publisher ==
"public":
266 kwargs = {
"area_id": self.
accountaccount.register_public_weather_area(**kwargs)}
272 next_scan=
time() + interval,
273 subscriptions={update_callback},
274 method=PUBLISHERS[publisher],
281 self.publisher.pop(signal_name)
284 self._queue.append(self.publisher[signal_name])
285 _LOGGER.debug(
"Publisher %s added", signal_name)
288 self, signal_name: str, update_callback: CALLBACK_TYPE |
None
290 """Unsubscribe from publisher."""
291 if update_callback
not in self.publisher[signal_name].subscriptions:
294 self.publisher[signal_name].subscriptions.remove(update_callback)
296 if not self.publisher[signal_name].subscriptions:
297 self._queue.
remove(self.publisher[signal_name])
298 self.publisher.pop(signal_name)
299 _LOGGER.debug(
"Publisher %s removed", signal_name)
303 """Return the webhook state."""
307 """Dispatch the creation of entities."""
308 await self.
subscribesubscribe(WEATHER, WEATHER,
None)
309 await self.
subscribesubscribe(AIR_CARE, AIR_CARE,
None)
313 for home
in self.
accountaccount.homes.values():
314 signal_home = f
"{HOME}-{home.entity_id}"
316 await self.
subscribesubscribe(HOME, signal_home,
None, home_id=home.entity_id)
317 await self.
subscribesubscribe(EVENT, signal_home,
None, home_id=home.entity_id)
323 self.
hasshass.data[DOMAIN][DATA_PERSONS][home.entity_id] = {
324 person.entity_id: person.pseudo
for person
in home.persons.values()
331 """Set up home coach/air care modules."""
332 for module
in self.
accountaccount.modules.values():
333 if module.device_category
is NetatmoDeviceCategory.air_care:
336 NETATMO_CREATE_WEATHER_SENSOR,
346 """Set up modules."""
347 netatmo_type_signal_map = {
348 NetatmoDeviceCategory.camera: [
349 NETATMO_CREATE_CAMERA,
350 NETATMO_CREATE_CAMERA_LIGHT,
352 NetatmoDeviceCategory.dimmer: [NETATMO_CREATE_LIGHT],
353 NetatmoDeviceCategory.shutter: [NETATMO_CREATE_COVER],
354 NetatmoDeviceCategory.switch: [
355 NETATMO_CREATE_LIGHT,
356 NETATMO_CREATE_SWITCH,
357 NETATMO_CREATE_SENSOR,
359 NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR],
360 NetatmoDeviceCategory.fan: [NETATMO_CREATE_FAN],
362 for module
in home.modules.values():
363 if not module.device_category:
366 for signal
in netatmo_type_signal_map.get(module.device_category, []):
377 if module.device_category
is NetatmoDeviceCategory.weather:
380 NETATMO_CREATE_WEATHER_SENSOR,
389 def setup_rooms(self, home: pyatmo.Home, signal_home: str) ->
None:
391 for room
in home.rooms.values():
392 if NetatmoDeviceCategory.climate
in room.features:
395 NETATMO_CREATE_CLIMATE,
404 for module
in room.modules.values():
405 if module.device_category
is NetatmoDeviceCategory.climate:
408 NETATMO_CREATE_BATTERY,
417 if "humidity" in room.features:
420 NETATMO_CREATE_ROOM_SENSOR,
430 self, home: pyatmo.Home, signal_home: str
432 """Set up climate schedule per home."""
433 if NetatmoDeviceCategory.climate
in [
434 next(iter(x))
for x
in [room.features
for room
in home.rooms.values()]
if x
436 self.
hasshass.data[DOMAIN][DATA_SCHEDULES][home.entity_id] = self.
accountaccount.homes[
442 NETATMO_CREATE_SELECT,
None async_dispatch(self)
None setup_rooms(self, pyatmo.Home home, str signal_home)
None handle_event(self, dict event)
None setup_air_care(self)
None subscribe(self, str publisher, str signal_name, CALLBACK_TYPE|None update_callback, **Any kwargs)
None unsubscribe(self, str signal_name, CALLBACK_TYPE|None update_callback)
bool async_fetch_data(self, str signal_name)
None setup_modules(self, pyatmo.Home home, str signal_home)
None setup_climate_schedule_select(self, pyatmo.Home home, str signal_home)
None async_force_update(self, str signal_name)
None __init__(self, HomeAssistant hass, ConfigEntry config_entry)
None async_update(self, datetime event_time)
bool remove(self, _T matcher)
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)