From 6930aebea277cd9965d8478475932d151e5b6c38 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 7 Aug 2020 09:25:59 +0200 Subject: [PATCH] Switch Netatmo integration to dispatcher for internal communication (#38590) * Switch to dispatcher for internal communication * Fix method call * Update homeassistant/components/netatmo/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/camera.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/climate.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/netatmo/climate.py Co-authored-by: Martin Hjelmare * Rename variables Co-authored-by: Martin Hjelmare --- homeassistant/components/netatmo/camera.py | 74 +++++-------- homeassistant/components/netatmo/climate.py | 101 +++++++++--------- .../components/netatmo/config_flow.py | 6 +- homeassistant/components/netatmo/const.py | 13 ++- .../components/netatmo/data_handler.py | 45 ++++---- homeassistant/components/netatmo/light.py | 63 +++++------ homeassistant/components/netatmo/webhook.py | 33 +++--- 7 files changed, 170 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 8fbff3225dd..39f6839d331 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,14 +5,10 @@ import pyatmo import requests import voluptuous as vol -from homeassistant.components.camera import ( - DOMAIN as CAMERA_DOMAIN, - SUPPORT_STREAM, - Camera, -) -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_PERSON, @@ -21,10 +17,12 @@ from .const import ( DATA_HANDLER, DATA_PERSONS, DOMAIN, + EVENT_TYPE_OFF, + EVENT_TYPE_ON, MANUFACTURER, MODELS, - SERVICE_SETPERSONAWAY, - SERVICE_SETPERSONSHOME, + SERVICE_SET_PERSON_AWAY, + SERVICE_SET_PERSONS_HOME, SIGNAL_NAME, ) from .data_handler import CAMERA_DATA_CLASS_NAME @@ -34,20 +32,6 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_QUALITY = "high" -SCHEMA_SERVICE_SETPERSONSHOME = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_domain(CAMERA_DOMAIN), - vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string]), - } -) - -SCHEMA_SERVICE_SETPERSONAWAY = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_domain(CAMERA_DOMAIN), - vol.Optional(ATTR_PERSON): cv.string, - } -) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera platform.""" @@ -108,22 +92,17 @@ async def async_setup_entry(hass, entry, async_add_entities): if data_handler.data[CAMERA_DATA_CLASS_NAME] is not None: platform.async_register_entity_service( - SERVICE_SETPERSONSHOME, - SCHEMA_SERVICE_SETPERSONSHOME, - "_service_setpersonshome", + SERVICE_SET_PERSONS_HOME, + {vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])}, + "_service_set_persons_home", ) platform.async_register_entity_service( - SERVICE_SETPERSONAWAY, - SCHEMA_SERVICE_SETPERSONAWAY, - "_service_setpersonaway", + SERVICE_SET_PERSON_AWAY, + {vol.Optional(ATTR_PERSON): cv.string}, + "_service_set_person_away", ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo camera platform.""" - return - - class NetatmoCamera(NetatmoBase, Camera): """Representation of a Netatmo camera.""" @@ -156,16 +135,19 @@ class NetatmoCamera(NetatmoBase, Camera): """Entity created.""" await super().async_added_to_hass() - self._listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) - ) + for event_type in (EVENT_TYPE_OFF, EVENT_TYPE_ON): + self._listeners.append( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{event_type}", + self.handle_event, + ) + ) - async def handle_event(self, event): + @callback + def handle_event(self, event): """Handle webhook events.""" - data = event.data["data"] - - if not data.get("event_type"): - return + data = event["data"] if not data.get("camera_id"): return @@ -278,7 +260,7 @@ class NetatmoCamera(NetatmoBase, Camera): self._is_local = camera.get("is_local") self.is_streaming = bool(self._status == "on") - def _service_setpersonshome(self, **kwargs): + def _service_set_persons_home(self, **kwargs): """Service to change current home schedule.""" persons = kwargs.get(ATTR_PERSONS) person_ids = [] @@ -288,9 +270,9 @@ class NetatmoCamera(NetatmoBase, Camera): person_ids.append(pid) self._data.set_persons_home(person_ids=person_ids, home_id=self._home_id) - _LOGGER.info("Set %s as at home", persons) + _LOGGER.debug("Set %s as at home", persons) - def _service_setpersonaway(self, **kwargs): + def _service_set_person_away(self, **kwargs): """Service to mark a person as away or set the home as empty.""" person = kwargs.get(ATTR_PERSON) person_id = None @@ -303,10 +285,10 @@ class NetatmoCamera(NetatmoBase, Camera): self._data.set_persons_away( person_id=person_id, home_id=self._home_id, ) - _LOGGER.info("Set %s as away", person) + _LOGGER.debug("Set %s as away", person) else: self._data.set_persons_away( person_id=person_id, home_id=self._home_id, ) - _LOGGER.info("Set home as empty") + _LOGGER.debug("Set home as empty") diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 459f005695b..acfcf4306fb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -4,7 +4,7 @@ from typing import List, Optional import voluptuous as vol -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN, ClimateEntity +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -19,7 +19,6 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_BATTERY_LEVEL, - ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_HALVES, STATE_OFF, @@ -27,6 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_HEATING_POWER_REQUEST, @@ -35,8 +35,11 @@ from .const import ( DATA_HOMES, DATA_SCHEDULES, DOMAIN, + EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SET_POINT, + EVENT_TYPE_THERM_MODE, MANUFACTURER, - SERVICE_SETSCHEDULE, + SERVICE_SET_SCHEDULE, SIGNAL_NAME, ) from .data_handler import HOMEDATA_DATA_CLASS_NAME, HOMESTATUS_DATA_CLASS_NAME @@ -95,13 +98,6 @@ DEFAULT_MAX_TEMP = 30 NA_THERM = "NATherm1" NA_VALVE = "NRV" -SCHEMA_SERVICE_SETSCHEDULE = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_domain(CLIMATE_DOMAIN), - vol.Required(ATTR_SCHEDULE_NAME): cv.string, - } -) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo energy platform.""" @@ -156,15 +152,12 @@ async def async_setup_entry(hass, entry, async_add_entities): if home_data is not None: platform.async_register_entity_service( - SERVICE_SETSCHEDULE, SCHEMA_SERVICE_SETSCHEDULE, "_service_setschedule", + SERVICE_SET_SCHEDULE, + {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, + "_service_set_schedule", ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo energy sensors.""" - return - - class NetatmoThermostat(NetatmoBase, ClimateEntity): """Representation a Netatmo thermostat.""" @@ -229,23 +222,29 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Entity created.""" await super().async_added_to_hass() - self._listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) - ) + for event_type in ( + EVENT_TYPE_SET_POINT, + EVENT_TYPE_THERM_MODE, + EVENT_TYPE_CANCEL_SET_POINT, + ): + self._listeners.append( + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{event_type}", + self.handle_event, + ) + ) async def handle_event(self, event): """Handle webhook events.""" - data = event.data["data"] - - if not data.get("event_type"): - return + data = event["data"] if not data.get("home"): return home = data["home"] - if self._home_id == home["id"] and data["event_type"] == "therm_mode": - self._preset = NETATMO_MAP_PRESET[home["therm_mode"]] + if self._home_id == home["id"] and data["event_type"] == EVENT_TYPE_THERM_MODE: + self._preset = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] if self._preset == PRESET_FROST_GUARD: self._target_temperature = self._hg_temperature @@ -260,7 +259,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return for room in home["rooms"]: - if data["event_type"] == "set_point": + if data["event_type"] == EVENT_TYPE_SET_POINT: if self._id == room["id"]: if room["therm_setpoint_mode"] == "off": self._hvac_mode = HVAC_MODE_OFF @@ -269,7 +268,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.async_write_ha_state() break - elif data["event_type"] == "cancel_set_point": + elif data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT: if self._id == room["id"]: self.async_update_callback() self.async_write_ha_state() @@ -411,10 +410,20 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def async_update_callback(self): """Update the entity's state.""" self._home_status = self.data_handler.data[self._home_status_class] - self._room_status = self._home_status.rooms[self._id] - self._room_data = self._data.rooms[self._home_id][self._id] + self._room_status = self._home_status.rooms.get(self._id) + self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id) - roomstatus = {"roomID": self._room_status["id"]} + if not self._room_status or not self._room_data: + if self._connected: + _LOGGER.info( + "The thermostat in room %s seems to be out of reach", + self._device_name, + ) + + self._connected = False + return + + roomstatus = {"roomID": self._room_status.get("id", {})} if self._room_status.get("reachable"): roomstatus.update(self._build_room_status()) @@ -422,25 +431,17 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._hg_temperature = self._data.get_hg_temp(self._home_id) self._setpoint_duration = self._data.setpoint_duration[self._home_id] - try: - if self._model is None: - self._model = roomstatus["module_type"] - self._current_temperature = roomstatus["current_temperature"] - self._target_temperature = roomstatus["target_temperature"] - self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] - self._hvac_mode = HVAC_MAP_NETATMO[self._preset] - self._battery_level = roomstatus.get("battery_level") - self._connected = True + if "current_temperature" not in roomstatus: + return - except KeyError as err: - if self._connected: - _LOGGER.debug( - "The thermostat in room %s seems to be out of reach. (%s)", - self._device_name, - err, - ) - - self._connected = False + if self._model is None: + self._model = roomstatus["module_type"] + self._current_temperature = roomstatus["current_temperature"] + self._target_temperature = roomstatus["target_temperature"] + self._preset = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] + self._hvac_mode = HVAC_MAP_NETATMO[self._preset] + self._battery_level = roomstatus.get("battery_level") + self._connected = True self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] @@ -503,7 +504,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return {} - def _service_setschedule(self, **kwargs): + def _service_set_schedule(self, **kwargs): schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): @@ -515,7 +516,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) - _LOGGER.info( + _LOGGER.debug( "Setting %s schedule to %s (%s)", self._home_id, kwargs.get(ATTR_SCHEDULE_NAME), diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index eedac3229c0..516f78e8019 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -69,7 +69,7 @@ class NetatmoFlowHandler( """Handle a flow start.""" await self.async_set_unique_id(DOMAIN) - if self.hass.config_entries.async_entries(DOMAIN): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) @@ -108,7 +108,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): user_input={CONF_NEW_AREA: new_client} ) - return self._update_options() + return self._create_options_entry() weather_areas = list(self.options[CONF_WEATHER_AREAS]) @@ -183,7 +183,7 @@ class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="public_weather", data_schema=data_schema) - def _update_options(self): + def _create_options_entry(self): """Update config entry options.""" return self.async_create_entry( title="Netatmo Public Weather", data=self.options diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index c23b934c541..30e40d358d9 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -73,6 +73,13 @@ ATTR_SCHEDULE_NAME = "schedule_name" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) -SERVICE_SETSCHEDULE = "set_schedule" -SERVICE_SETPERSONSHOME = "set_persons_home" -SERVICE_SETPERSONAWAY = "set_person_away" +SERVICE_SET_SCHEDULE = "set_schedule" +SERVICE_SET_PERSONS_HOME = "set_persons_home" +SERVICE_SET_PERSON_AWAY = "set_person_away" + +EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" +EVENT_TYPE_LIGHT_MODE = "light_mode" +EVENT_TYPE_OFF = "off" +EVENT_TYPE_ON = "on" +EVENT_TYPE_SET_POINT = "set_point" +EVENT_TYPE_THERM_MODE = "therm_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 414c89e13ec..8a299d0f072 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -11,6 +11,7 @@ import pyatmo from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval from .const import AUTH, DOMAIN, MANUFACTURER @@ -69,7 +70,9 @@ class NetatmoDataHandler: ) self.listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) + async_dispatcher_connect( + self.hass, f"signal-{DOMAIN}-webhook-None", self.handle_event, + ) ) async def async_update(self, event_time): @@ -99,11 +102,11 @@ class NetatmoDataHandler: async def handle_event(self, event): """Handle webhook events.""" - if event.data["data"]["push_type"] == "webhook_activation": + if event["data"]["push_type"] == "webhook_activation": _LOGGER.info("%s webhook successfully registered", MANUFACTURER) self._webhook = True - elif event.data["data"]["push_type"] == "NACamera-connection": + elif event["data"]["push_type"] == "NACamera-connection": _LOGGER.debug("%s camera reconnected", MANUFACTURER) self._data_classes[CAMERA_DATA_CLASS_NAME][NEXT_SCAN] = time() @@ -126,27 +129,27 @@ class NetatmoDataHandler: self, data_class_name, data_class_entry, update_callback, **kwargs ): """Register data class.""" - if data_class_entry not in self._data_classes: - self._data_classes[data_class_entry] = { - "class": DATA_CLASSES[data_class_name], - "name": data_class_entry, - "interval": DEFAULT_INTERVALS[data_class_name], - NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], - "kwargs": kwargs, - "subscriptions": [update_callback], - } - - await self.async_fetch_data( - DATA_CLASSES[data_class_name], data_class_entry, **kwargs - ) - - self._queue.append(self._data_classes[data_class_entry]) - _LOGGER.debug("Data class %s added", data_class_entry) - - else: + if data_class_entry in self._data_classes: self._data_classes[data_class_entry]["subscriptions"].append( update_callback ) + return + + self._data_classes[data_class_entry] = { + "class": DATA_CLASSES[data_class_name], + "name": data_class_entry, + "interval": DEFAULT_INTERVALS[data_class_name], + NEXT_SCAN: time() + DEFAULT_INTERVALS[data_class_name], + "kwargs": kwargs, + "subscriptions": [update_callback], + } + + await self.async_fetch_data( + DATA_CLASSES[data_class_name], data_class_entry, **kwargs + ) + + self._queue.append(self._data_classes[data_class_entry]) + _LOGGER.debug("Data class %s added", data_class_entry) async def unregister_data_class(self, data_class_entry, update_callback): """Unregister data class.""" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 56cf7945402..dea56e54c09 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -6,8 +6,15 @@ import pyatmo from homeassistant.components.light import LightEntity from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA_HANDLER, DOMAIN, MANUFACTURER, SIGNAL_NAME +from .const import ( + DATA_HANDLER, + DOMAIN, + EVENT_TYPE_LIGHT_MODE, + MANUFACTURER, + SIGNAL_NAME, +) from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler from .netatmo_entity_base import NetatmoBase @@ -31,42 +38,36 @@ async def async_setup_entry(hass, entry, async_add_entities): ) entities = [] + all_cameras = [] + + if CAMERA_DATA_CLASS_NAME not in data_handler.data: + raise PlatformNotReady + try: - all_cameras = [] for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values(): for camera in home.values(): all_cameras.append(camera) - for camera in all_cameras: - if camera["type"] == "NOC": - if not data_handler.webhook: - raise PlatformNotReady - - _LOGGER.debug( - "Adding camera light %s %s", camera["id"], camera["name"] - ) - entities.append( - NetatmoLight( - data_handler, - camera["id"], - camera["type"], - camera["home_id"], - ) - ) - except pyatmo.NoDevice: _LOGGER.debug("No cameras found") + for camera in all_cameras: + if camera["type"] == "NOC": + if not data_handler.webhook: + raise PlatformNotReady + + _LOGGER.debug("Adding camera light %s %s", camera["id"], camera["name"]) + entities.append( + NetatmoLight( + data_handler, camera["id"], camera["type"], camera["home_id"], + ) + ) + return entities async_add_entities(await get_entities(), True) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Netatmo camera platform.""" - return - - class NetatmoLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" @@ -97,15 +98,17 @@ class NetatmoLight(NetatmoBase, LightEntity): await super().async_added_to_hass() self._listeners.append( - self.hass.bus.async_listen("netatmo_event", self.handle_event) + async_dispatcher_connect( + self.hass, + f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}", + self.handle_event, + ) ) - async def handle_event(self, event): + @callback + def handle_event(self, event): """Handle webhook events.""" - data = event.data["data"] - - if not data.get("event_type"): - return + data = event["data"] if not data.get("camera_id"): return diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 7126551883a..582fce8985c 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -2,6 +2,7 @@ import logging from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( ATTR_EVENT_TYPE, @@ -36,10 +37,9 @@ async def handle_webhook(hass, webhook_id, request): event_type = data.get(ATTR_EVENT_TYPE) - if event_type in ["outdoor", "therm_mode"]: - hass.bus.async_fire( - event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} - ) + if event_type in EVENT_TYPE_MAP: + async_send_event(hass, event_type, data) + for event_data in data.get(EVENT_TYPE_MAP[event_type], []): async_evaluate_event(hass, event_data) @@ -61,13 +61,22 @@ def async_evaluate_event(hass, event_data): ) person_event_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) person_event_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) - hass.bus.async_fire( - event_type=NETATMO_EVENT, - event_data={"type": event_type, "data": person_event_data}, - ) + + async_send_event(hass, event_type, person_event_data) + else: _LOGGER.debug("%s: %s", event_type, event_data) - hass.bus.async_fire( - event_type=NETATMO_EVENT, - event_data={"type": event_type, "data": event_data}, - ) + async_send_event(hass, event_type, event_data) + + +@callback +def async_send_event(hass, event_type, data): + """Send events.""" + hass.bus.async_fire( + event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} + ) + async_dispatcher_send( + hass, + f"signal-{DOMAIN}-webhook-{event_type}", + {"type": event_type, "data": data}, + )