Netatmo entity renaming and clean up (#75337)

This commit is contained in:
Tobias Sauerwein 2022-07-27 14:17:38 +02:00 committed by GitHub
parent 314778cb50
commit 49854b809c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 128 additions and 144 deletions

View file

@ -112,6 +112,8 @@ async def async_setup_entry(
class NetatmoCamera(NetatmoBase, Camera): class NetatmoCamera(NetatmoBase, Camera):
"""Representation of a Netatmo camera.""" """Representation of a Netatmo camera."""
_attr_brand = MANUFACTURER
_attr_has_entity_name = True
_attr_supported_features = CameraEntityFeature.STREAM _attr_supported_features = CameraEntityFeature.STREAM
def __init__( def __init__(
@ -126,14 +128,13 @@ class NetatmoCamera(NetatmoBase, Camera):
Camera.__init__(self) Camera.__init__(self)
super().__init__(data_handler) super().__init__(data_handler)
self._data_classes.append( self._publishers.append(
{"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME}
) )
self._id = camera_id self._id = camera_id
self._home_id = home_id self._home_id = home_id
self._device_name = self._data.get_camera(camera_id=camera_id)["name"] self._device_name = self._data.get_camera(camera_id=camera_id)["name"]
self._attr_name = f"{MANUFACTURER} {self._device_name}"
self._model = camera_type self._model = camera_type
self._netatmo_type = TYPE_SECURITY self._netatmo_type = TYPE_SECURITY
self._attr_unique_id = f"{self._id}-{self._model}" self._attr_unique_id = f"{self._id}-{self._model}"
@ -193,7 +194,7 @@ class NetatmoCamera(NetatmoBase, Camera):
"""Return data for this entity.""" """Return data for this entity."""
return cast( return cast(
pyatmo.AsyncCameraData, pyatmo.AsyncCameraData,
self.data_handler.data[self._data_classes[0]["name"]], self.data_handler.data[self._publishers[0]["name"]],
) )
async def async_camera_image( async def async_camera_image(
@ -219,11 +220,6 @@ class NetatmoCamera(NetatmoBase, Camera):
"""Return True if entity is available.""" """Return True if entity is available."""
return bool(self._alim_status == "on" or self._status == "disconnected") return bool(self._alim_status == "on" or self._status == "disconnected")
@property
def brand(self) -> str:
"""Return the camera brand."""
return MANUFACTURER
@property @property
def motion_detection_enabled(self) -> bool: def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """Return the camera motion detection status."""

View file

@ -127,7 +127,7 @@ async def async_setup_entry(
for home_id in climate_topology.home_ids: for home_id in climate_topology.home_ids:
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
await data_handler.register_data_class( await data_handler.subscribe(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
) )
@ -185,14 +185,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._room = room self._room = room
self._id = self._room.entity_id self._id = self._room.entity_id
self._climate_state_class = ( self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}"
f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name]
)
self._climate_state: pyatmo.AsyncClimate = data_handler.data[
self._climate_state_class
]
self._data_classes.extend( self._publishers.extend(
[ [
{ {
"name": CLIMATE_TOPOLOGY_CLASS_NAME, "name": CLIMATE_TOPOLOGY_CLASS_NAME,
@ -201,7 +197,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
{ {
"name": CLIMATE_STATE_CLASS_NAME, "name": CLIMATE_STATE_CLASS_NAME,
"home_id": self._room.home.entity_id, "home_id": self._room.home.entity_id,
SIGNAL_NAME: self._climate_state_class, SIGNAL_NAME: self._signal_name,
}, },
] ]
) )
@ -254,7 +250,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self.data_handler, self.data_handler,
module, module,
self._id, self._id,
self._climate_state_class, self._signal_name,
), ),
) )
@ -278,7 +274,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
ATTR_SELECTED_SCHEDULE ATTR_SELECTED_SCHEDULE
] = self._selected_schedule ] = self._selected_schedule
self.async_write_ha_state() self.async_write_ha_state()
self.data_handler.async_force_update(self._climate_state_class) self.data_handler.async_force_update(self._signal_name)
return return
home = data["home"] home = data["home"]
@ -295,7 +291,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._attr_target_temperature = self._away_temperature self._attr_target_temperature = self._away_temperature
elif self._attr_preset_mode == PRESET_SCHEDULE: elif self._attr_preset_mode == PRESET_SCHEDULE:
self.async_update_callback() self.async_update_callback()
self.data_handler.async_force_update(self._climate_state_class) self.data_handler.async_force_update(self._signal_name)
self.async_write_ha_state() self.async_write_ha_state()
return return

View file

@ -73,16 +73,16 @@ DATA_HANDLER = "netatmo_data_handler"
SIGNAL_NAME = "signal_name" SIGNAL_NAME = "signal_name"
NETATMO_CREATE_BATTERY = "netatmo_create_battery" NETATMO_CREATE_BATTERY = "netatmo_create_battery"
CONF_CLOUDHOOK_URL = "cloudhook_url"
CONF_WEATHER_AREAS = "weather_areas"
CONF_NEW_AREA = "new_area"
CONF_AREA_NAME = "area_name" CONF_AREA_NAME = "area_name"
CONF_CLOUDHOOK_URL = "cloudhook_url"
CONF_LAT_NE = "lat_ne" CONF_LAT_NE = "lat_ne"
CONF_LON_NE = "lon_ne"
CONF_LAT_SW = "lat_sw" CONF_LAT_SW = "lat_sw"
CONF_LON_NE = "lon_ne"
CONF_LON_SW = "lon_sw" CONF_LON_SW = "lon_sw"
CONF_NEW_AREA = "new_area"
CONF_PUBLIC_MODE = "mode" CONF_PUBLIC_MODE = "mode"
CONF_UUID = "uuid" CONF_UUID = "uuid"
CONF_WEATHER_AREAS = "weather_areas"
OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize"
OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token"
@ -94,53 +94,53 @@ DATA_HOMES = "netatmo_homes"
DATA_PERSONS = "netatmo_persons" DATA_PERSONS = "netatmo_persons"
DATA_SCHEDULES = "netatmo_schedules" DATA_SCHEDULES = "netatmo_schedules"
NETATMO_WEBHOOK_URL = None
NETATMO_EVENT = "netatmo_event" NETATMO_EVENT = "netatmo_event"
NETATMO_WEBHOOK_URL = None
DEFAULT_PERSON = "unknown"
DEFAULT_DISCOVERY = True DEFAULT_DISCOVERY = True
DEFAULT_PERSON = "unknown"
DEFAULT_WEBHOOKS = False DEFAULT_WEBHOOKS = False
ATTR_PSEUDO = "pseudo" ATTR_CAMERA_LIGHT_MODE = "camera_light_mode"
ATTR_EVENT_TYPE = "event_type" ATTR_EVENT_TYPE = "event_type"
ATTR_FACE_URL = "face_url"
ATTR_HEATING_POWER_REQUEST = "heating_power_request" ATTR_HEATING_POWER_REQUEST = "heating_power_request"
ATTR_HOME_ID = "home_id" ATTR_HOME_ID = "home_id"
ATTR_HOME_NAME = "home_name" ATTR_HOME_NAME = "home_name"
ATTR_IS_KNOWN = "is_known"
ATTR_PERSON = "person" ATTR_PERSON = "person"
ATTR_PERSONS = "persons" ATTR_PERSONS = "persons"
ATTR_IS_KNOWN = "is_known" ATTR_PSEUDO = "pseudo"
ATTR_FACE_URL = "face_url"
ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_ID = "schedule_id"
ATTR_SCHEDULE_NAME = "schedule_name" ATTR_SCHEDULE_NAME = "schedule_name"
ATTR_SELECTED_SCHEDULE = "selected_schedule" ATTR_SELECTED_SCHEDULE = "selected_schedule"
ATTR_CAMERA_LIGHT_MODE = "camera_light_mode"
SERVICE_SET_CAMERA_LIGHT = "set_camera_light" SERVICE_SET_CAMERA_LIGHT = "set_camera_light"
SERVICE_SET_SCHEDULE = "set_schedule"
SERVICE_SET_PERSONS_HOME = "set_persons_home"
SERVICE_SET_PERSON_AWAY = "set_person_away" SERVICE_SET_PERSON_AWAY = "set_person_away"
SERVICE_SET_PERSONS_HOME = "set_persons_home"
SERVICE_SET_SCHEDULE = "set_schedule"
# Climate events # Climate events
EVENT_TYPE_SET_POINT = "set_point"
EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point"
EVENT_TYPE_THERM_MODE = "therm_mode"
EVENT_TYPE_SCHEDULE = "schedule" EVENT_TYPE_SCHEDULE = "schedule"
EVENT_TYPE_SET_POINT = "set_point"
EVENT_TYPE_THERM_MODE = "therm_mode"
# Camera events # Camera events
EVENT_TYPE_LIGHT_MODE = "light_mode"
EVENT_TYPE_CAMERA_OUTDOOR = "outdoor"
EVENT_TYPE_CAMERA_ANIMAL = "animal" EVENT_TYPE_CAMERA_ANIMAL = "animal"
EVENT_TYPE_CAMERA_HUMAN = "human" EVENT_TYPE_CAMERA_HUMAN = "human"
EVENT_TYPE_CAMERA_VEHICLE = "vehicle"
EVENT_TYPE_CAMERA_MOVEMENT = "movement" EVENT_TYPE_CAMERA_MOVEMENT = "movement"
EVENT_TYPE_CAMERA_OUTDOOR = "outdoor"
EVENT_TYPE_CAMERA_PERSON = "person" EVENT_TYPE_CAMERA_PERSON = "person"
EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away" EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away"
EVENT_TYPE_CAMERA_VEHICLE = "vehicle"
EVENT_TYPE_LIGHT_MODE = "light_mode"
# Door tags # Door tags
EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" EVENT_TYPE_ALARM_STARTED = "alarm_started"
EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move" EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move"
EVENT_TYPE_DOOR_TAG_OPEN = "tag_open" EVENT_TYPE_DOOR_TAG_OPEN = "tag_open"
EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move"
EVENT_TYPE_OFF = "off" EVENT_TYPE_OFF = "off"
EVENT_TYPE_ON = "on" EVENT_TYPE_ON = "on"
EVENT_TYPE_ALARM_STARTED = "alarm_started"
OUTDOOR_CAMERA_TRIGGERS = [ OUTDOOR_CAMERA_TRIGGERS = [
EVENT_TYPE_CAMERA_ANIMAL, EVENT_TYPE_CAMERA_ANIMAL,
@ -149,46 +149,46 @@ OUTDOOR_CAMERA_TRIGGERS = [
EVENT_TYPE_CAMERA_VEHICLE, EVENT_TYPE_CAMERA_VEHICLE,
] ]
INDOOR_CAMERA_TRIGGERS = [ INDOOR_CAMERA_TRIGGERS = [
EVENT_TYPE_CAMERA_MOVEMENT,
EVENT_TYPE_CAMERA_PERSON,
EVENT_TYPE_CAMERA_PERSON_AWAY,
EVENT_TYPE_ALARM_STARTED, EVENT_TYPE_ALARM_STARTED,
EVENT_TYPE_CAMERA_MOVEMENT,
EVENT_TYPE_CAMERA_PERSON_AWAY,
EVENT_TYPE_CAMERA_PERSON,
] ]
DOOR_TAG_TRIGGERS = [ DOOR_TAG_TRIGGERS = [
EVENT_TYPE_DOOR_TAG_SMALL_MOVE,
EVENT_TYPE_DOOR_TAG_BIG_MOVE, EVENT_TYPE_DOOR_TAG_BIG_MOVE,
EVENT_TYPE_DOOR_TAG_OPEN, EVENT_TYPE_DOOR_TAG_OPEN,
EVENT_TYPE_DOOR_TAG_SMALL_MOVE,
] ]
CLIMATE_TRIGGERS = [ CLIMATE_TRIGGERS = [
EVENT_TYPE_SET_POINT,
EVENT_TYPE_CANCEL_SET_POINT, EVENT_TYPE_CANCEL_SET_POINT,
EVENT_TYPE_SET_POINT,
EVENT_TYPE_THERM_MODE, EVENT_TYPE_THERM_MODE,
] ]
EVENT_ID_MAP = { EVENT_ID_MAP = {
EVENT_TYPE_CAMERA_MOVEMENT: "device_id", EVENT_TYPE_ALARM_STARTED: "device_id",
EVENT_TYPE_CAMERA_PERSON: "device_id",
EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id",
EVENT_TYPE_CAMERA_ANIMAL: "device_id", EVENT_TYPE_CAMERA_ANIMAL: "device_id",
EVENT_TYPE_CAMERA_HUMAN: "device_id", EVENT_TYPE_CAMERA_HUMAN: "device_id",
EVENT_TYPE_CAMERA_MOVEMENT: "device_id",
EVENT_TYPE_CAMERA_OUTDOOR: "device_id", EVENT_TYPE_CAMERA_OUTDOOR: "device_id",
EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id",
EVENT_TYPE_CAMERA_PERSON: "device_id",
EVENT_TYPE_CAMERA_VEHICLE: "device_id", EVENT_TYPE_CAMERA_VEHICLE: "device_id",
EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", EVENT_TYPE_CANCEL_SET_POINT: "room_id",
EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id", EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id",
EVENT_TYPE_DOOR_TAG_OPEN: "device_id", EVENT_TYPE_DOOR_TAG_OPEN: "device_id",
EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id",
EVENT_TYPE_LIGHT_MODE: "device_id", EVENT_TYPE_LIGHT_MODE: "device_id",
EVENT_TYPE_ALARM_STARTED: "device_id",
EVENT_TYPE_CANCEL_SET_POINT: "room_id",
EVENT_TYPE_SET_POINT: "room_id", EVENT_TYPE_SET_POINT: "room_id",
EVENT_TYPE_THERM_MODE: "home_id", EVENT_TYPE_THERM_MODE: "home_id",
} }
MODE_LIGHT_ON = "on"
MODE_LIGHT_OFF = "off"
MODE_LIGHT_AUTO = "auto" MODE_LIGHT_AUTO = "auto"
MODE_LIGHT_OFF = "off"
MODE_LIGHT_ON = "on"
CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO]
WEBHOOK_ACTIVATION = "webhook_activation" WEBHOOK_ACTIVATION = "webhook_activation"
WEBHOOK_DEACTIVATION = "webhook_deactivation" WEBHOOK_DEACTIVATION = "webhook_deactivation"
WEBHOOK_LIGHT_MODE = "NOC-light_mode"
WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection" WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection"
WEBHOOK_PUSH_TYPE = "push_type" WEBHOOK_PUSH_TYPE = "push_type"
WEBHOOK_LIGHT_MODE = "NOC-light_mode"

View file

@ -64,11 +64,11 @@ class NetatmoDevice:
data_handler: NetatmoDataHandler data_handler: NetatmoDataHandler
device: pyatmo.climate.NetatmoModule device: pyatmo.climate.NetatmoModule
parent_id: str parent_id: str
state_class_name: str signal_name: str
@dataclass @dataclass
class NetatmoDataClass: class NetatmoPublisher:
"""Class for keeping track of Netatmo data class metadata.""" """Class for keeping track of Netatmo data class metadata."""
name: str name: str
@ -85,7 +85,7 @@ class NetatmoDataHandler:
self.hass = hass self.hass = hass
self.config_entry = config_entry self.config_entry = config_entry
self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH]
self.data_classes: dict = {} self.publisher: dict[str, NetatmoPublisher] = {}
self.data: dict = {} self.data: dict = {}
self._queue: deque = deque() self._queue: deque = deque()
self._webhook: bool = False self._webhook: bool = False
@ -107,7 +107,7 @@ class NetatmoDataHandler:
await asyncio.gather( await asyncio.gather(
*[ *[
self.register_data_class(data_class, data_class, None) self.subscribe(data_class, data_class, None)
for data_class in ( for data_class in (
CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME,
CAMERA_DATA_CLASS_NAME, CAMERA_DATA_CLASS_NAME,
@ -128,20 +128,18 @@ class NetatmoDataHandler:
if data_class.next_scan > time(): if data_class.next_scan > time():
continue continue
if data_class_name := data_class.name: if publisher := data_class.name:
self.data_classes[data_class_name].next_scan = ( self.publisher[publisher].next_scan = time() + data_class.interval
time() + data_class.interval
)
await self.async_fetch_data(data_class_name) await self.async_fetch_data(publisher)
self._queue.rotate(BATCH_SIZE) self._queue.rotate(BATCH_SIZE)
@callback @callback
def async_force_update(self, data_class_entry: str) -> None: def async_force_update(self, signal_name: str) -> None:
"""Prioritize data retrieval for given data class entry.""" """Prioritize data retrieval for given data class entry."""
self.data_classes[data_class_entry].next_scan = time() self.publisher[signal_name].next_scan = time()
self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) self._queue.rotate(-(self._queue.index(self.publisher[signal_name])))
async def handle_event(self, event: dict) -> None: async def handle_event(self, event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
@ -157,17 +155,17 @@ class NetatmoDataHandler:
_LOGGER.debug("%s camera reconnected", MANUFACTURER) _LOGGER.debug("%s camera reconnected", MANUFACTURER)
self.async_force_update(CAMERA_DATA_CLASS_NAME) self.async_force_update(CAMERA_DATA_CLASS_NAME)
async def async_fetch_data(self, data_class_entry: str) -> None: async def async_fetch_data(self, signal_name: str) -> None:
"""Fetch data and notify.""" """Fetch data and notify."""
if self.data[data_class_entry] is None: if self.data[signal_name] is None:
return return
try: try:
await self.data[data_class_entry].async_update() await self.data[signal_name].async_update()
except pyatmo.NoDevice as err: except pyatmo.NoDevice as err:
_LOGGER.debug(err) _LOGGER.debug(err)
self.data[data_class_entry] = None self.data[signal_name] = None
except pyatmo.ApiError as err: except pyatmo.ApiError as err:
_LOGGER.debug(err) _LOGGER.debug(err)
@ -176,56 +174,52 @@ class NetatmoDataHandler:
_LOGGER.debug(err) _LOGGER.debug(err)
return return
for update_callback in self.data_classes[data_class_entry].subscriptions: for update_callback in self.publisher[signal_name].subscriptions:
if update_callback: if update_callback:
update_callback() update_callback()
async def register_data_class( async def subscribe(
self, self,
data_class_name: str, publisher: str,
data_class_entry: str, signal_name: str,
update_callback: CALLBACK_TYPE | None, update_callback: CALLBACK_TYPE | None,
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Register data class.""" """Subscribe to publisher."""
if data_class_entry in self.data_classes: if signal_name in self.publisher:
if update_callback not in self.data_classes[data_class_entry].subscriptions: if update_callback not in self.publisher[signal_name].subscriptions:
self.data_classes[data_class_entry].subscriptions.append( self.publisher[signal_name].subscriptions.append(update_callback)
update_callback
)
return return
self.data_classes[data_class_entry] = NetatmoDataClass( self.publisher[signal_name] = NetatmoPublisher(
name=data_class_entry, name=signal_name,
interval=DEFAULT_INTERVALS[data_class_name], interval=DEFAULT_INTERVALS[publisher],
next_scan=time() + DEFAULT_INTERVALS[data_class_name], next_scan=time() + DEFAULT_INTERVALS[publisher],
subscriptions=[update_callback], subscriptions=[update_callback],
) )
self.data[data_class_entry] = DATA_CLASSES[data_class_name]( self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs)
self._auth, **kwargs
)
try: try:
await self.async_fetch_data(data_class_entry) await self.async_fetch_data(signal_name)
except KeyError: except KeyError:
self.data_classes.pop(data_class_entry) self.publisher.pop(signal_name)
raise raise
self._queue.append(self.data_classes[data_class_entry]) self._queue.append(self.publisher[signal_name])
_LOGGER.debug("Data class %s added", data_class_entry) _LOGGER.debug("Publisher %s added", signal_name)
async def unregister_data_class( async def unsubscribe(
self, data_class_entry: str, update_callback: CALLBACK_TYPE | None self, signal_name: str, update_callback: CALLBACK_TYPE | None
) -> None: ) -> None:
"""Unregister data class.""" """Unsubscribe from publisher."""
self.data_classes[data_class_entry].subscriptions.remove(update_callback) self.publisher[signal_name].subscriptions.remove(update_callback)
if not self.data_classes[data_class_entry].subscriptions: if not self.publisher[signal_name].subscriptions:
self._queue.remove(self.data_classes[data_class_entry]) self._queue.remove(self.publisher[signal_name])
self.data_classes.pop(data_class_entry) self.publisher.pop(signal_name)
self.data.pop(data_class_entry) self.data.pop(signal_name)
_LOGGER.debug("Data class %s removed", data_class_entry) _LOGGER.debug("Publisher %s removed", signal_name)
@property @property
def webhook(self) -> bool: def webhook(self) -> bool:

View file

@ -17,7 +17,6 @@ from .const import (
DATA_HANDLER, DATA_HANDLER,
DOMAIN, DOMAIN,
EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_LIGHT_MODE,
MANUFACTURER,
SIGNAL_NAME, SIGNAL_NAME,
TYPE_SECURITY, TYPE_SECURITY,
WEBHOOK_LIGHT_MODE, WEBHOOK_LIGHT_MODE,
@ -63,6 +62,7 @@ class NetatmoLight(NetatmoBase, LightEntity):
"""Representation of a Netatmo Presence camera light.""" """Representation of a Netatmo Presence camera light."""
_attr_color_mode = ColorMode.ONOFF _attr_color_mode = ColorMode.ONOFF
_attr_has_entity_name = True
_attr_supported_color_modes = {ColorMode.ONOFF} _attr_supported_color_modes = {ColorMode.ONOFF}
def __init__( def __init__(
@ -76,7 +76,7 @@ class NetatmoLight(NetatmoBase, LightEntity):
LightEntity.__init__(self) LightEntity.__init__(self)
super().__init__(data_handler) super().__init__(data_handler)
self._data_classes.append( self._publishers.append(
{"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME}
) )
self._id = camera_id self._id = camera_id
@ -84,7 +84,6 @@ class NetatmoLight(NetatmoBase, LightEntity):
self._model = camera_type self._model = camera_type
self._netatmo_type = TYPE_SECURITY self._netatmo_type = TYPE_SECURITY
self._device_name: str = self._data.get_camera(camera_id)["name"] self._device_name: str = self._data.get_camera(camera_id)["name"]
self._attr_name = f"{MANUFACTURER} {self._device_name}"
self._is_on = False self._is_on = False
self._attr_unique_id = f"{self._id}-light" self._attr_unique_id = f"{self._id}-light"
@ -123,7 +122,7 @@ class NetatmoLight(NetatmoBase, LightEntity):
"""Return data for this entity.""" """Return data for this entity."""
return cast( return cast(
pyatmo.AsyncCameraData, pyatmo.AsyncCameraData,
self.data_handler.data[self._data_classes[0]["name"]], self.data_handler.data[self._publishers[0]["name"]],
) )
@property @property

View file

@ -1,6 +1,8 @@
"""Base class for Netatmo entities.""" """Base class for Netatmo entities."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
@ -23,7 +25,7 @@ class NetatmoBase(Entity):
def __init__(self, data_handler: NetatmoDataHandler) -> None: def __init__(self, data_handler: NetatmoDataHandler) -> None:
"""Set up Netatmo entity base.""" """Set up Netatmo entity base."""
self.data_handler = data_handler self.data_handler = data_handler
self._data_classes: list[dict] = [] self._publishers: list[dict[str, Any]] = []
self._device_name: str = "" self._device_name: str = ""
self._id: str = "" self._id: str = ""
@ -35,11 +37,11 @@ class NetatmoBase(Entity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Entity created.""" """Entity created."""
for data_class in self._data_classes: for data_class in self._publishers:
signal_name = data_class[SIGNAL_NAME] signal_name = data_class[SIGNAL_NAME]
if "home_id" in data_class: if "home_id" in data_class:
await self.data_handler.register_data_class( await self.data_handler.subscribe(
data_class["name"], data_class["name"],
signal_name, signal_name,
self.async_update_callback, self.async_update_callback,
@ -47,7 +49,7 @@ class NetatmoBase(Entity):
) )
elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME:
await self.data_handler.register_data_class( await self.data_handler.subscribe(
data_class["name"], data_class["name"],
signal_name, signal_name,
self.async_update_callback, self.async_update_callback,
@ -58,13 +60,13 @@ class NetatmoBase(Entity):
) )
else: else:
await self.data_handler.register_data_class( await self.data_handler.subscribe(
data_class["name"], signal_name, self.async_update_callback data_class["name"], signal_name, self.async_update_callback
) )
for sub in self.data_handler.data_classes[signal_name].subscriptions: for sub in self.data_handler.publisher[signal_name].subscriptions:
if sub is None: if sub is None:
await self.data_handler.unregister_data_class(signal_name, None) await self.data_handler.unsubscribe(signal_name, None)
registry = dr.async_get(self.hass) registry = dr.async_get(self.hass)
if device := registry.async_get_device({(DOMAIN, self._id)}): if device := registry.async_get_device({(DOMAIN, self._id)}):
@ -76,8 +78,8 @@ class NetatmoBase(Entity):
"""Run when entity will be removed from hass.""" """Run when entity will be removed from hass."""
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
for data_class in self._data_classes: for data_class in self._publishers:
await self.data_handler.unregister_data_class( await self.data_handler.unsubscribe(
data_class[SIGNAL_NAME], self.async_update_callback data_class[SIGNAL_NAME], self.async_update_callback
) )

View file

@ -46,7 +46,7 @@ async def async_setup_entry(
for home_id in climate_topology.home_ids: for home_id in climate_topology.home_ids:
signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
await data_handler.register_data_class( await data_handler.subscribe(
CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
) )
@ -92,7 +92,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
self._home = self._climate_state.homes[self._home_id] self._home = self._climate_state.homes[self._home_id]
self._data_classes.extend( self._publishers.extend(
[ [
{ {
"name": CLIMATE_TOPOLOGY_CLASS_NAME, "name": CLIMATE_TOPOLOGY_CLASS_NAME,

View file

@ -41,7 +41,6 @@ from .const import (
CONF_WEATHER_AREAS, CONF_WEATHER_AREAS,
DATA_HANDLER, DATA_HANDLER,
DOMAIN, DOMAIN,
MANUFACTURER,
NETATMO_CREATE_BATTERY, NETATMO_CREATE_BATTERY,
SIGNAL_NAME, SIGNAL_NAME,
TYPE_WEATHER, TYPE_WEATHER,
@ -422,7 +421,7 @@ async def async_setup_entry(
) )
continue continue
await data_handler.register_data_class( await data_handler.subscribe(
PUBLICDATA_DATA_CLASS_NAME, PUBLICDATA_DATA_CLASS_NAME,
signal_name, signal_name,
None, None,
@ -487,9 +486,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
super().__init__(data_handler) super().__init__(data_handler)
self.entity_description = description self.entity_description = description
self._data_classes.append( self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name})
{"name": data_class_name, SIGNAL_NAME: data_class_name}
)
self._id = module_info["_id"] self._id = module_info["_id"]
self._station_id = module_info.get("main_device", self._id) self._station_id = module_info.get("main_device", self._id)
@ -507,7 +504,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
f"{module_info.get('module_name', device['type'])}" f"{module_info.get('module_name', device['type'])}"
) )
self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" self._attr_name = f"{self._device_name} {description.name}"
self._model = device["type"] self._model = device["type"]
self._netatmo_type = TYPE_WEATHER self._netatmo_type = TYPE_WEATHER
self._attr_unique_id = f"{self._id}-{description.key}" self._attr_unique_id = f"{self._id}-{description.key}"
@ -517,7 +514,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
"""Return data for this entity.""" """Return data for this entity."""
return cast( return cast(
pyatmo.AsyncWeatherStationData, pyatmo.AsyncWeatherStationData,
self.data_handler.data[self._data_classes[0]["name"]], self.data_handler.data[self._publishers[0]["name"]],
) )
@property @property
@ -598,7 +595,7 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity):
self._id = netatmo_device.parent_id self._id = netatmo_device.parent_id
self._attr_name = f"{self._module.name} {self.entity_description.name}" self._attr_name = f"{self._module.name} {self.entity_description.name}"
self._state_class_name = netatmo_device.state_class_name self._signal_name = netatmo_device.signal_name
self._room_id = self._module.room_id self._room_id = self._module.room_id
self._model = getattr(self._module.device_type, "value") self._model = getattr(self._module.device_type, "value")
@ -734,7 +731,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
self._data_classes.append( self._publishers.append(
{ {
"name": PUBLICDATA_DATA_CLASS_NAME, "name": PUBLICDATA_DATA_CLASS_NAME,
"lat_ne": area.lat_ne, "lat_ne": area.lat_ne,
@ -751,7 +748,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
self._area_name = area.area_name self._area_name = area.area_name
self._id = self._area_name self._id = self._area_name
self._device_name = f"{self._area_name}" self._device_name = f"{self._area_name}"
self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" self._attr_name = f"{self._device_name} {description.name}"
self._show_on_map = area.show_on_map self._show_on_map = area.show_on_map
self._attr_unique_id = ( self._attr_unique_id = (
f"{self._device_name.replace(' ', '-')}-{description.key}" f"{self._device_name.replace(' ', '-')}-{description.key}"
@ -788,13 +785,13 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
if self.area == area: if self.area == area:
return return
await self.data_handler.unregister_data_class( await self.data_handler.unsubscribe(
self._signal_name, self.async_update_callback self._signal_name, self.async_update_callback
) )
self.area = area self.area = area
self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
self._data_classes = [ self._publishers = [
{ {
"name": PUBLICDATA_DATA_CLASS_NAME, "name": PUBLICDATA_DATA_CLASS_NAME,
"lat_ne": area.lat_ne, "lat_ne": area.lat_ne,
@ -807,7 +804,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
] ]
self._mode = area.mode self._mode = area.mode
self._show_on_map = area.show_on_map self._show_on_map = area.show_on_map
await self.data_handler.register_data_class( await self.data_handler.subscribe(
PUBLICDATA_DATA_CLASS_NAME, PUBLICDATA_DATA_CLASS_NAME,
self._signal_name, self._signal_name,
self.async_update_callback, self.async_update_callback,

View file

@ -32,8 +32,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
webhook_id = config_entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
await hass.async_block_till_done() await hass.async_block_till_done()
camera_entity_indoor = "camera.netatmo_hall" camera_entity_indoor = "camera.hall"
camera_entity_outdoor = "camera.netatmo_garden" camera_entity_outdoor = "camera.garden"
assert hass.states.get(camera_entity_indoor).state == "streaming" assert hass.states.get(camera_entity_indoor).state == "streaming"
response = { response = {
"event_type": "off", "event_type": "off",
@ -95,7 +95,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call( await hass.services.async_call(
"camera", "turn_off", service_data={"entity_id": "camera.netatmo_hall"} "camera", "turn_off", service_data={"entity_id": "camera.hall"}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_set_state.assert_called_once_with( mock_set_state.assert_called_once_with(
@ -106,7 +106,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
await hass.services.async_call( await hass.services.async_call(
"camera", "turn_on", service_data={"entity_id": "camera.netatmo_hall"} "camera", "turn_on", service_data={"entity_id": "camera.hall"}
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_set_state.assert_called_once_with( mock_set_state.assert_called_once_with(
@ -130,7 +130,7 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut
uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d"
stream_uri = uri + "/live/files/high/index.m3u8" stream_uri = uri + "/live/files/high/index.m3u8"
camera_entity_indoor = "camera.netatmo_hall" camera_entity_indoor = "camera.hall"
cam = hass.states.get(camera_entity_indoor) cam = hass.states.get(camera_entity_indoor)
assert cam is not None assert cam is not None
@ -161,7 +161,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth)
"6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,," "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,"
) )
stream_uri = uri + "/live/files/high/index.m3u8" stream_uri = uri + "/live/files/high/index.m3u8"
camera_entity_indoor = "camera.netatmo_garden" camera_entity_indoor = "camera.garden"
cam = hass.states.get(camera_entity_indoor) cam = hass.states.get(camera_entity_indoor)
assert cam is not None assert cam is not None
@ -188,7 +188,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth):
await hass.async_block_till_done() await hass.async_block_till_done()
data = { data = {
"entity_id": "camera.netatmo_hall", "entity_id": "camera.hall",
"person": "Richard Doe", "person": "Richard Doe",
} }
@ -205,7 +205,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth):
) )
data = { data = {
"entity_id": "camera.netatmo_hall", "entity_id": "camera.hall",
} }
with patch( with patch(
@ -231,7 +231,7 @@ async def test_service_set_person_away_invalid_person(hass, config_entry, netatm
await hass.async_block_till_done() await hass.async_block_till_done()
data = { data = {
"entity_id": "camera.netatmo_hall", "entity_id": "camera.hall",
"person": "Batman", "person": "Batman",
} }
@ -259,7 +259,7 @@ async def test_service_set_persons_home_invalid_person(
await hass.async_block_till_done() await hass.async_block_till_done()
data = { data = {
"entity_id": "camera.netatmo_hall", "entity_id": "camera.hall",
"persons": "Batman", "persons": "Batman",
} }
@ -285,7 +285,7 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth):
await hass.async_block_till_done() await hass.async_block_till_done()
data = { data = {
"entity_id": "camera.netatmo_hall", "entity_id": "camera.hall",
"persons": "John Doe", "persons": "John Doe",
} }
@ -312,7 +312,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth):
await hass.async_block_till_done() await hass.async_block_till_done()
data = { data = {
"entity_id": "camera.netatmo_garden", "entity_id": "camera.garden",
"camera_light_mode": "on", "camera_light_mode": "on",
} }
@ -485,7 +485,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
camera_entity_indoor = "camera.netatmo_hall" camera_entity_indoor = "camera.hall"
with pytest.raises(Exception) as excinfo: with pytest.raises(Exception) as excinfo:
await camera.async_get_image(hass, camera_entity_indoor) await camera.async_get_image(hass, camera_entity_indoor)

View file

@ -27,7 +27,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
await hass.async_block_till_done() await hass.async_block_till_done()
light_entity = "light.netatmo_garden" light_entity = "light.garden"
assert hass.states.get(light_entity).state == "unavailable" assert hass.states.get(light_entity).state == "unavailable"
# Trigger light mode change # Trigger light mode change

View file

@ -17,7 +17,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth):
await hass.async_block_till_done() await hass.async_block_till_done()
prefix = "sensor.netatmo_mystation_" prefix = "sensor.mystation_"
assert hass.states.get(f"{prefix}temperature").state == "24.6" assert hass.states.get(f"{prefix}temperature").state == "24.6"
assert hass.states.get(f"{prefix}humidity").state == "36" assert hass.states.get(f"{prefix}humidity").state == "36"
@ -34,13 +34,13 @@ async def test_public_weather_sensor(hass, config_entry, netatmo_auth):
assert len(hass.states.async_all()) > 0 assert len(hass.states.async_all()) > 0
prefix = "sensor.netatmo_home_max_" prefix = "sensor.home_max_"
assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}temperature").state == "27.4"
assert hass.states.get(f"{prefix}humidity").state == "76" assert hass.states.get(f"{prefix}humidity").state == "76"
assert hass.states.get(f"{prefix}pressure").state == "1014.4" assert hass.states.get(f"{prefix}pressure").state == "1014.4"
prefix = "sensor.netatmo_home_avg_" prefix = "sensor.home_avg_"
assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}temperature").state == "22.7"
assert hass.states.get(f"{prefix}humidity").state == "63.2" assert hass.states.get(f"{prefix}humidity").state == "63.2"