From 9ba3abd1b73f80b17d7e1be552d91d5b0b67e124 Mon Sep 17 00:00:00 2001 From: ssenart <37013755+ssenart@users.noreply.github.com> Date: Wed, 6 Nov 2019 13:52:59 +0100 Subject: [PATCH] Add Netatmo camera services (#27970) * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement turn_on and turn_off methods. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Netatmo camera : Implement enable_motion_detection(), disable_motion_detection() operations. * Add Presence Netatmo Camera services (set_light_auto, set_light_on, set_light_off) to control its internal flood light status. * Add Presence Netatmo Camera services (set_light_auto, set_light_on, set_light_off) to control its internal flood light status. * Netatmo camera : Use new style string formatting. * Make the file compliant with flake8 linter. * Make the file compliant with flake8 linter. * Make it compliant with black formatter. * Make it compliant with black formatter. * Bug fix : Flood light control was not working with VPN url. --- homeassistant/components/netatmo/camera.py | 307 +++++++++++++++++- .../components/netatmo/services.yaml | 24 +- 2 files changed, 312 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ecc38add3b4..f3bf6a6784c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,11 +5,20 @@ from pyatmo import NoDevice import requests import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM -from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + SUPPORT_STREAM, + CAMERA_SERVICE_SCHEMA, +) +from homeassistant.const import CONF_VERIFY_SSL, STATE_ON, STATE_OFF from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + async_dispatcher_connect, +) -from .const import DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -33,6 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up access to Netatmo cameras.""" @@ -63,6 +74,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except NoDevice: return None + async def async_service_handler(call): + """Handle service call.""" + _LOGGER.debug( + "Service handler invoked with service=%s and data=%s", + call.service, + call.data, + ) + service = call.service + entity_id = call.data["entity_id"][0] + async_dispatcher_send(hass, f"{service}_{entity_id}") + + hass.services.async_register( + DOMAIN, "set_light_auto", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "set_light_on", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + hass.services.async_register( + DOMAIN, "set_light_off", async_service_handler, CAMERA_SERVICE_SCHEMA + ) + class NetatmoCamera(Camera): """Representation of the images published from a Netatmo camera.""" @@ -72,16 +104,39 @@ class NetatmoCamera(Camera): super().__init__() self._data = data self._camera_name = camera_name - self._verify_ssl = verify_ssl - self._quality = quality + self._home = home if home: self._name = home + " / " + camera_name else: self._name = camera_name - self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( - camera=camera_name - ) self._cameratype = camera_type + self._verify_ssl = verify_ssl + self._quality = quality + + # URLs. + self._vpnurl = None + self._localurl = None + + # Identifier + self._id = None + + # Monitoring status. + self._status = None + + # SD Card status + self._sd_status = None + + # Power status + self._alim_status = None + + # Is local + self._is_local = None + + # VPN URL + self._vpn_url = None + + # Light mode status + self._light_mode_status = None def camera_image(self): """Return a still image response from the camera.""" @@ -112,16 +167,79 @@ class NetatmoCamera(Camera): return None return response.content + # Entity property overrides + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return True + @property def name(self): """Return the name of this Netatmo camera device.""" return self._name + @property + def device_state_attributes(self): + """Return the Netatmo-specific camera state attributes.""" + + _LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name) + + attr = {} + attr["id"] = self._id + attr["status"] = self._status + attr["sd_status"] = self._sd_status + attr["alim_status"] = self._alim_status + attr["is_local"] = self._is_local + attr["vpn_url"] = self._vpn_url + + if self.model == "Presence": + attr["light_mode_status"] = self._light_mode_status + + _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) + + return attr + + @property + def available(self): + """Return True if entity is available.""" + return bool(self._alim_status == "on") + + @property + def supported_features(self): + """Return supported features.""" + return SUPPORT_STREAM + + @property + def is_recording(self): + """Return true if the device is recording.""" + return bool(self._status == "on") + @property def brand(self): """Return the camera brand.""" return "Netatmo" + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return bool(self._status == "on") + + @property + def is_on(self): + """Return true if on.""" + return self.is_streaming + + async def stream_source(self): + """Return the stream source.""" + url = "{0}/live/files/{1}/index.m3u8" + if self._localurl: + return url.format(self._localurl, self._quality) + return url.format(self._vpnurl, self._quality) + @property def model(self): """Return the camera model.""" @@ -131,14 +249,167 @@ class NetatmoCamera(Camera): return "Welcome" return None - @property - def supported_features(self): - """Return supported features.""" - return SUPPORT_STREAM + # Other Entity method overrides - async def stream_source(self): - """Return the stream source.""" - url = "{0}/live/files/{1}/index.m3u8" - if self._localurl: - return url.format(self._localurl, self._quality) - return url.format(self._vpnurl, self._quality) + async def async_added_to_hass(self): + """Subscribe to signals and add camera to list.""" + _LOGGER.debug("Registering services for entity_id=%s", self.entity_id) + async_dispatcher_connect( + self.hass, f"set_light_auto_{self.entity_id}", self.set_light_auto + ) + async_dispatcher_connect( + self.hass, f"set_light_on_{self.entity_id}", self.set_light_on + ) + async_dispatcher_connect( + self.hass, f"set_light_off_{self.entity_id}", self.set_light_off + ) + + def update(self): + """Update entity status.""" + + _LOGGER.debug("Updating camera netatmo '%s'", self._name) + + # Refresh camera data. + self._data.update() + + # URLs. + self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + + # Identifier + self._id = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["id"] + + # Monitoring status. + self._status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["status"] + + _LOGGER.debug("Status of '%s' = %s", self._name, self._status) + + # SD Card status + self._sd_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["sd_status"] + + # Power status + self._alim_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["alim_status"] + + # Is local + self._is_local = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["is_local"] + + # VPN URL + self._vpn_url = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["vpn_url"] + + self.is_streaming = self._alim_status == "on" + + if self.model == "Presence": + # Light mode status + self._light_mode_status = self._data.camera_data.cameraByName( + camera=self._camera_name, home=self._home + )["light_mode_status"] + + # Camera method overrides + + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) + self._enable_motion_detection(True) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + _LOGGER.debug("Disable motion detection of the camera '%s'", self._name) + self._enable_motion_detection(False) + + def _enable_motion_detection(self, enable): + """Enable or disable motion detection.""" + try: + if self._localurl: + requests.get( + f"{self._localurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Welcome/Presence VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Welcome/Presence URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + else: + self.async_schedule_update_ha_state(True) + + # Netatmo Presence specific camera method. + + def set_light_auto(self): + """Set flood light in automatic mode.""" + _LOGGER.debug( + "Set the flood light in automatic mode for the camera '%s'", self._name + ) + self._set_light_mode("auto") + + def set_light_on(self): + """Set flood light on.""" + _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) + self._set_light_mode("on") + + def set_light_off(self): + """Set flood light off.""" + _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) + self._set_light_mode("off") + + def _set_light_mode(self, mode): + """Set light mode ('auto', 'on', 'off').""" + if self.model == "Presence": + try: + config = '{"mode":"' + mode + '"}' + if self._localurl: + requests.get( + f"{self._localurl}/command/floodlight_set_config?config={config}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/floodlight_set_config?config={config}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Presence VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + except requests.exceptions.RequestException as error: + _LOGGER.error("Presence URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + camera=self._camera_name + ) + return None + else: + self.async_schedule_update_ha_state(True) + else: + _LOGGER.error("Unsupported camera model for light mode") diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 7bb990caf97..a928f4765e0 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -4,5 +4,27 @@ addwebhook: url: description: URL for which to add the webhook. example: https://yourdomain.com:443/api/webhook/webhook_id + dropwebhook: - description: Drop active webhooks. \ No newline at end of file + description: Drop active webhooks. + +set_light_auto: + description: Set the camera (Presence only) light in automatic mode. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +set_light_on: + description: Set the camera (Netatmo Presence only) light on. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room' + +set_light_off: + description: Set the camera (Netatmo Presence only) light off. + fields: + entity_id: + description: Entity id. + example: 'camera.living_room'