Switch Netatmo integration to dispatcher for internal communication ()

* Switch to dispatcher for internal communication

* Fix method call

* Update homeassistant/components/netatmo/camera.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/netatmo/camera.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/netatmo/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/netatmo/climate.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Rename variables

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
cgtobi 2020-08-07 09:25:59 +02:00 committed by GitHub
parent 3546a82cfb
commit 6930aebea2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 170 additions and 165 deletions

View file

@ -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")

View file

@ -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),

View file

@ -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

View file

@ -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"

View file

@ -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."""

View file

@ -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

View file

@ -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},
)