From ad9b859b42240a3de97f974f689b84830f94c854 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 23 Nov 2019 00:55:32 +0100 Subject: [PATCH 01/58] Refactor to use ids in data class --- homeassistant/components/netatmo/sensor.py | 45 ++++++++++------------ 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index d4d624061f5df5..6c80322c81f306 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -6,7 +6,6 @@ import pyatmo import requests -import urllib3 import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -177,7 +176,22 @@ def _retry(_data): for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: - data = NetatmoData(auth, data_class, config.get(CONF_STATION)) + station = config.get(CONF_STATION) + dc_data = data_class(auth) + _LOGGER.debug("%s detected!", NETATMO_DEVICE_TYPES[data_class.__name__]) + if station: + station_data = dc_data.stationByName(station) + if station_data: + station_id = station_data.get("_id") + else: + _LOGGER.debug( + 'No %s station "%s" found', + NETATMO_DEVICE_TYPES[data_class.__name__], + station, + ) + else: + station_id = None + data = NetatmoData(dc_data, station_id) except pyatmo.NoDevice: _LOGGER.info( "No %s devices found", NETATMO_DEVICE_TYPES[data_class.__name__] @@ -541,18 +555,11 @@ def update(self): class NetatmoData: """Get the latest data from Netatmo.""" - def __init__(self, auth, data_class, station): + def __init__(self, station_data, station_id): """Initialize the data object.""" - self.auth = auth - self.data_class = data_class self.data = {} - self.station_data = self.data_class(self.auth) - self.station = station - self.station_id = None - if station: - station_data = self.station_data.stationByName(self.station) - if station_data: - self.station_id = station_data.get("_id") + self.station_data = station_data + self.station_id = station_id self._next_update = time() self._update_in_progress = threading.Lock() @@ -572,18 +579,6 @@ def update(self): if time() < self._next_update or not self._update_in_progress.acquire(False): return try: - try: - self.station_data = self.data_class(self.auth) - _LOGGER.debug("%s detected!", str(self.data_class.__name__)) - except pyatmo.NoDevice: - _LOGGER.warning( - "No Weather or HomeCoach devices found for %s", str(self.station) - ) - return - except (requests.exceptions.Timeout, urllib3.exceptions.ReadTimeoutError): - _LOGGER.warning("Timed out when connecting to Netatmo server.") - return - data = self.station_data.lastData( station=self.station_id, exclude=3600, byId=True ) @@ -599,7 +594,7 @@ def update(self): newinterval = self.data[module]["When"] break except TypeError: - _LOGGER.debug("No %s modules found", self.data_class.__name__) + _LOGGER.debug("No %s modules found", self.station_data.__name__) if newinterval: # Try and estimate when fresh data will be available From ccaeee11cfa1a4b28b30fa2cd651a8d9013b3456 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Sat, 23 Nov 2019 00:59:56 +0100 Subject: [PATCH 02/58] Use station_id --- homeassistant/components/netatmo/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 6c80322c81f306..051ff00c2ed882 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -43,7 +43,7 @@ NETATMO_UPDATE_INTERVAL = 600 # NetAtmo Public Data is uploaded to server every 10 minutes -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=NETATMO_UPDATE_INTERVAL) SUPPORTED_PUBLIC_SENSOR_TYPES = [ "temperature", From 2e7a5c7632c53eb26eb7a6f9c727f7195ebf8314 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 Nov 2019 00:22:07 +0100 Subject: [PATCH 03/58] Refactor Netatmo to use oauth --- CODEOWNERS | 1 + homeassistant/components/netatmo/__init__.py | 289 ++++-------------- homeassistant/components/netatmo/api.py | 40 +++ .../components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 257 ++++++++++------ homeassistant/components/netatmo/climate.py | 116 +++---- .../components/netatmo/config_flow.py | 40 +++ homeassistant/components/netatmo/const.py | 49 ++- .../components/netatmo/manifest.json | 15 +- homeassistant/components/netatmo/sensor.py | 171 ++++------- .../components/netatmo/services.yaml | 38 +-- homeassistant/components/netatmo/strings.json | 18 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/netatmo/test_config_flow.py | 60 ++++ 15 files changed, 549 insertions(+), 551 deletions(-) create mode 100644 homeassistant/components/netatmo/api.py create mode 100644 homeassistant/components/netatmo/config_flow.py create mode 100644 homeassistant/components/netatmo/strings.json create mode 100644 tests/components/netatmo/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index b30e15d36cab41..0f9f3fd041ab67 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -222,6 +222,7 @@ homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan +homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/nextbus/* @vividboarder homeassistant/components/nilu/* @hfurubotten diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 6becedde61149c..3847e3c6a00fe3 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,164 +1,53 @@ -"""Support for the Netatmo devices.""" -from datetime import timedelta +"""The Netatmo integration.""" +import asyncio import logging -from urllib.error import HTTPError -import pyatmo import voluptuous as vol -from homeassistant.const import ( - CONF_API_KEY, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_URL, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow +from homeassistant.config_entries import ConfigEntry -from .const import DATA_NETATMO_AUTH, DOMAIN +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, AUTH, DATA_PERSONS +from . import api, config_flow _LOGGER = logging.getLogger(__name__) -DATA_PERSONS = "netatmo_persons" -DATA_WEBHOOK_URL = "netatmo_webhook_url" - -CONF_SECRET_KEY = "secret_key" -CONF_WEBHOOKS = "webhooks" - -SERVICE_ADDWEBHOOK = "addwebhook" -SERVICE_DROPWEBHOOK = "dropwebhook" -SERVICE_SETSCHEDULE = "set_schedule" - -NETATMO_AUTH = None -NETATMO_WEBHOOK_URL = None - -DEFAULT_PERSON = "Unknown" -DEFAULT_DISCOVERY = True -DEFAULT_WEBHOOKS = False - -EVENT_PERSON = "person" -EVENT_MOVEMENT = "movement" -EVENT_HUMAN = "human" -EVENT_ANIMAL = "animal" -EVENT_VEHICLE = "vehicle" - -EVENT_BUS_PERSON = "netatmo_person" -EVENT_BUS_MOVEMENT = "netatmo_movement" -EVENT_BUS_HUMAN = "netatmo_human" -EVENT_BUS_ANIMAL = "netatmo_animal" -EVENT_BUS_VEHICLE = "netatmo_vehicle" -EVENT_BUS_OTHER = "netatmo_other" - -ATTR_ID = "id" -ATTR_PSEUDO = "pseudo" -ATTR_NAME = "name" -ATTR_EVENT_TYPE = "event_type" -ATTR_MESSAGE = "message" -ATTR_CAMERA_ID = "camera_id" -ATTR_HOME_NAME = "home_name" -ATTR_PERSONS = "persons" -ATTR_IS_KNOWN = "is_known" -ATTR_FACE_URL = "face_url" -ATTR_SNAPSHOT_URL = "snapshot_url" -ATTR_VIGNETTE_URL = "vignette_url" -ATTR_SCHEDULE = "schedule" - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_SECRET_KEY): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_WEBHOOKS, default=DEFAULT_WEBHOOKS): cv.boolean, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, } ) }, extra=vol.ALLOW_EXTRA, ) -SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({vol.Optional(CONF_URL): cv.string}) - -SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) - -SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string}) - +# PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor", "switch"] +PLATFORMS = ["camera", "climate", "sensor"] -def setup(hass, config): - """Set up the Netatmo devices.""" - hass.data[DATA_PERSONS] = {} - try: - auth = pyatmo.ClientAuth( - config[DOMAIN][CONF_API_KEY], - config[DOMAIN][CONF_SECRET_KEY], - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - "read_station read_camera access_camera " - "read_thermostat write_thermostat " - "read_presence access_presence read_homecoach", - ) - except HTTPError: - _LOGGER.error("Unable to connect to Netatmo API") - return False - - try: - home_data = pyatmo.HomeData(auth) - except pyatmo.NoDevice: - home_data = None - _LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE) +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Netatmo component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_PERSONS] = {} - # Store config to be used during entry setup - hass.data[DATA_NETATMO_AUTH] = auth + if DOMAIN not in config: + return True - if config[DOMAIN][CONF_DISCOVERY]: - for component in "camera", "sensor", "binary_sensor", "climate": - discovery.load_platform(hass, component, DOMAIN, {}, config) - - if config[DOMAIN][CONF_WEBHOOKS]: - webhook_id = hass.components.webhook.async_generate_id() - hass.data[DATA_WEBHOOK_URL] = hass.components.webhook.async_generate_url( - webhook_id - ) - hass.components.webhook.async_register( - DOMAIN, "Netatmo", webhook_id, handle_webhook - ) - auth.addwebhook(hass.data[DATA_WEBHOOK_URL]) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, dropwebhook) - - def _service_addwebhook(service): - """Service to (re)add webhooks during runtime.""" - url = service.data.get(CONF_URL) - if url is None: - url = hass.data[DATA_WEBHOOK_URL] - _LOGGER.info("Adding webhook for URL: %s", url) - auth.addwebhook(url) - - hass.services.register( - DOMAIN, - SERVICE_ADDWEBHOOK, - _service_addwebhook, - schema=SCHEMA_SERVICE_ADDWEBHOOK, - ) - - def _service_dropwebhook(service): - """Service to drop webhooks during runtime.""" - _LOGGER.info("Dropping webhook") - auth.dropwebhook() - - hass.services.register( - DOMAIN, - SERVICE_DROPWEBHOOK, - _service_dropwebhook, - schema=SCHEMA_SERVICE_DROPWEBHOOK, + config_flow.NetatmoFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), ) def _service_setschedule(service): @@ -178,109 +67,35 @@ def _service_setschedule(service): return True -def dropwebhook(hass): - """Drop the webhook subscription.""" - auth = hass.data[DATA_NETATMO_AUTH] - auth.dropwebhook() - - -async def handle_webhook(hass, webhook_id, request): - """Handle webhook callback.""" - try: - data = await request.json() - except ValueError: - return None - - _LOGGER.debug("Got webhook data: %s", data) - published_data = { - ATTR_EVENT_TYPE: data.get(ATTR_EVENT_TYPE), - ATTR_HOME_NAME: data.get(ATTR_HOME_NAME), - ATTR_CAMERA_ID: data.get(ATTR_CAMERA_ID), - ATTR_MESSAGE: data.get(ATTR_MESSAGE), - } - if data.get(ATTR_EVENT_TYPE) == EVENT_PERSON: - for person in data[ATTR_PERSONS]: - published_data[ATTR_ID] = person.get(ATTR_ID) - published_data[ATTR_NAME] = hass.data[DATA_PERSONS].get( - published_data[ATTR_ID], DEFAULT_PERSON - ) - published_data[ATTR_IS_KNOWN] = person.get(ATTR_IS_KNOWN) - published_data[ATTR_FACE_URL] = person.get(ATTR_FACE_URL) - hass.bus.async_fire(EVENT_BUS_PERSON, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_MOVEMENT: - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - hass.bus.async_fire(EVENT_BUS_MOVEMENT, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_HUMAN: - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - hass.bus.async_fire(EVENT_BUS_HUMAN, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_ANIMAL: - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - hass.bus.async_fire(EVENT_BUS_ANIMAL, published_data) - elif data.get(ATTR_EVENT_TYPE) == EVENT_VEHICLE: - hass.bus.async_fire(EVENT_BUS_VEHICLE, published_data) - published_data[ATTR_VIGNETTE_URL] = data.get(ATTR_VIGNETTE_URL) - published_data[ATTR_SNAPSHOT_URL] = data.get(ATTR_SNAPSHOT_URL) - else: - hass.bus.async_fire(EVENT_BUS_OTHER, data) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Netatmo from a config entry.""" + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) -class CameraData: - """Get the latest data from Netatmo.""" + hass.data[DOMAIN][AUTH] = api.ConfigEntryNetatmoAuth(hass, entry, session) - def __init__(self, hass, auth, home=None): - """Initialize the data object.""" - self._hass = hass - self.auth = auth - self.camera_data = None - self.camera_names = [] - self.module_names = [] - self.home = home - self.camera_type = None + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) - def get_camera_names(self): - """Return all camera available on the API as a list.""" - self.camera_names = [] - self.update() - if not self.home: - for home in self.camera_data.cameras: - for camera in self.camera_data.cameras[home].values(): - self.camera_names.append(camera["name"]) - else: - for camera in self.camera_data.cameras[self.home].values(): - self.camera_names.append(camera["name"]) - return self.camera_names + return True - def get_module_names(self, camera_name): - """Return all module available on the API as a list.""" - self.module_names = [] - self.update() - cam_id = self.camera_data.cameraByName(camera=camera_name, home=self.home)["id"] - for module in self.camera_data.modules.values(): - if cam_id == module["cam_id"]: - self.module_names.append(module["name"]) - return self.module_names - def get_camera_type(self, camera=None, home=None, cid=None): - """Return camera type for a camera, cid has preference over camera.""" - self.camera_type = self.camera_data.cameraType( - camera=camera, home=home, cid=cid +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] ) - return self.camera_type - - def get_persons(self): - """Gather person data for webhooks.""" - for person_id, person_data in self.camera_data.persons.items(): - self._hass.data[DATA_PERSONS][person_id] = person_data.get(ATTR_PSEUDO) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Call the Netatmo API to update the data.""" - self.camera_data = pyatmo.CameraData(self.auth, size=100) + ) + if unload_ok: + hass.data[DOMAIN].pop(AUTH) - @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) - def update_event(self): - """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent(home=self.home, devicetype=self.camera_type) + return unload_ok diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py new file mode 100644 index 00000000000000..6dbf9d63db2b94 --- /dev/null +++ b/homeassistant/components/netatmo/api.py @@ -0,0 +1,40 @@ +"""API for Netatmo bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe +import logging + +import pyatmo + +from homeassistant import core, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryNetatmoAuth(pyatmo.auth.NetatmOAuth2): + """Provide Netatmo authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Netatmo Auth.""" + _LOGGER.debug("Netatmo ConfigEntryNetatmoAuth") + self.hass = hass + self.config_entry = config_entry + _LOGGER.debug("Netatmo ConfigEntryAuth 2") + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + _LOGGER.debug("Netatmo ConfigEntryAuth 3") + super().__init__(token=self.session.token) + + def refresh_tokens(self) -> dict: + """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" + _LOGGER.debug("Netatmo ConfigEntryAuth.refresh_tokens") + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index a449b7bb43daef..b6babf5bc32399 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,8 +8,8 @@ from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv -from . import CameraData from .const import DATA_NETATMO_AUTH +from .camera import CameraData _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 546a5da3c152e8..f3a220c996c328 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,25 +1,35 @@ """Support for the Netatmo cameras.""" import logging -from pyatmo import NoDevice +import pyatmo import requests -import voluptuous as vol + +# import voluptuous as vol from homeassistant.components.camera import ( - CAMERA_SERVICE_SCHEMA, - PLATFORM_SCHEMA, + # PLATFORM_SCHEMA, + Camera, SUPPORT_STREAM, Camera, ) -from homeassistant.const import CONF_VERIFY_SSL, STATE_OFF, STATE_ON -from homeassistant.helpers import config_validation as cv +from homeassistant.const import STATE_ON, STATE_OFF + +# from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) - -from . import CameraData -from .const import DATA_NETATMO_AUTH, DOMAIN +from homeassistant.util import Throttle + +from .const import ( + AUTH, + DOMAIN, + MANUFAKTURER, + DATA_PERSONS, + ATTR_PSEUDO, + MIN_TIME_BETWEEN_UPDATES, + MIN_TIME_BETWEEN_EVENT_UPDATES, +) _LOGGER = logging.getLogger(__name__) @@ -31,48 +41,40 @@ VALID_QUALITIES = ["high", "medium", "low", "poor"] -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_HOME): cv.string, - vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_QUALITY, default=DEFAULT_QUALITY): vol.All( - cv.string, vol.In(VALID_QUALITIES) - ), - } -) +# PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +# { +# vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, +# vol.Optional(CONF_HOME): cv.string, +# vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), +# vol.Optional(CONF_QUALITY, default=DEFAULT_QUALITY): vol.All( +# cv.string, vol.In(VALID_QUALITIES) +# ), +# } +# ) _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.""" - home = config.get(CONF_HOME) - verify_ssl = config.get(CONF_VERIFY_SSL, True) - quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) - - auth = hass.data[DATA_NETATMO_AUTH] - - try: - data = CameraData(hass, auth, home) - for camera_name in data.get_camera_names(): - camera_type = data.get_camera_type(camera=camera_name, home=home) - if CONF_CAMERAS in config: - if ( - config[CONF_CAMERAS] != [] - and camera_name not in config[CONF_CAMERAS] - ): - continue - add_entities( - [ - NetatmoCamera( - data, camera_name, home, camera_type, verify_ssl, quality - ) - ] - ) - data.get_persons() - except NoDevice: - return None +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Netatmo weather and homecoach platform.""" + auth = hass.data[DOMAIN][AUTH] + + def get_devices(): + """Retrieve Netatmo devices.""" + devices = [] + try: + data = CameraData(hass, auth) + for camera_id in data.get_camera_ids(): + camera_type = data.get_camera_type(cid=camera_id) + devices.append( + NetatmoCamera(data, camera_id, camera_type, True, DEFAULT_QUALITY) + ) + data.get_persons() + except pyatmo.NoDevice: + _LOGGER.debug("No cameras found") + return devices + + async_add_entities(await hass.async_add_executor_job(get_devices), True) async def async_service_handler(call): """Handle service call.""" @@ -96,20 +98,25 @@ async def async_service_handler(call): ) +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up access to Netatmo cameras.""" + return + + class NetatmoCamera(Camera): """Representation of the images published from a Netatmo camera.""" - def __init__(self, data, camera_name, home, camera_type, verify_ssl, quality): + def __init__(self, data, camera_id, camera_type, verify_ssl, quality): """Set up for access to the Netatmo camera images.""" super().__init__() self._data = data - self._camera_name = camera_name - self._home = home - if home: - self._name = f"{home} / {camera_name}" - else: - self._name = camera_name - self._cameratype = camera_type + self._camera_id = camera_id + self._name = ( + f"{MANUFAKTURER} {self._data.camera_data.cameraById(camera_id).get('name')}" + ) + self._camera_type = camera_type + self._unique_id = f"{self._camera_id}-{self._camera_type}" + _LOGGER.debug("Setting up camera %s", self._unique_id) self._verify_ssl = verify_ssl self._quality = quality @@ -155,14 +162,14 @@ def camera_image(self): _LOGGER.error("Welcome VPN URL is None") self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + cid=self._camera_id ) return None except requests.exceptions.RequestException as error: _LOGGER.error("Welcome URL changed: %s", error) self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + cid=self._camera_id ) return None return response.content @@ -182,11 +189,21 @@ def name(self): """Return the name of this Netatmo camera device.""" return self._name + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._camera_id)}, + "name": self._name, + "manufacturer": MANUFAKTURER, + "model": self._camera_type, + } + @property def device_state_attributes(self): """Return the Netatmo-specific camera state attributes.""" - _LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name) + # _LOGGER.debug("Getting new attributes from camera netatmo '%s'", self._name) attr = {} attr["id"] = self._id @@ -199,7 +216,7 @@ def device_state_attributes(self): if self.model == "Presence": attr["light_mode_status"] = self._light_mode_status - _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) + # _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) return attr @@ -221,7 +238,7 @@ def is_recording(self): @property def brand(self): """Return the camera brand.""" - return "Netatmo" + return MANUFAKTURER @property def motion_detection_enabled(self): @@ -243,9 +260,9 @@ async def stream_source(self): @property def model(self): """Return the camera model.""" - if self._cameratype == "NOC": + if self._camera_type == "NOC": return "Presence" - if self._cameratype == "NACamera": + if self._camera_type == "NACamera": return "Welcome" return None @@ -267,55 +284,40 @@ async def async_added_to_hass(self): def update(self): """Update entity status.""" - _LOGGER.debug("Updating camera netatmo '%s'", self._name) + # _LOGGER.debug("Updating camera '%s'", self._name) - # Refresh camera data. + # Refresh camera data self._data.update() - # URLs. + camera = self._data.camera_data.cameraById(cid=self._camera_id) + + # URLs self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( - camera=self._camera_name + cid=self._camera_id ) - # Identifier - self._id = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["id"] + # Monitoring status + self._status = camera["status"] - # 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) + # _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"] + self._sd_status = camera["sd_status"] # Power status - self._alim_status = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["alim_status"] + self._alim_status = camera["alim_status"] # Is local - self._is_local = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["is_local"] + self._is_local = camera["is_local"] # VPN URL - self._vpn_url = self._data.camera_data.cameraByName( - camera=self._camera_name, home=self._home - )["vpn_url"] + self._vpn_url = camera["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"] + self._light_mode_status = camera["light_mode_status"] # Camera method overrides @@ -334,12 +336,14 @@ def _enable_motion_detection(self, enable): try: if self._localurl: requests.get( - f"{self._localurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + f"{self._localurl}/command/changestatus?status=" + f"{_BOOL_TO_STATE.get(enable)}", timeout=10, ) elif self._vpnurl: requests.get( - f"{self._vpnurl}/command/changestatus?status={_BOOL_TO_STATE.get(enable)}", + f"{self._vpnurl}/command/changestatus?status=" + f"{_BOOL_TO_STATE.get(enable)}", timeout=10, verify=self._verify_ssl, ) @@ -347,14 +351,14 @@ def _enable_motion_detection(self, enable): _LOGGER.error("Welcome/Presence VPN URL is None") self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + cid=self._camera_id ) 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 + cid=self._camera_id ) return None else: @@ -386,12 +390,14 @@ def _set_light_mode(self, mode): config = f'{{"mode":"{mode}"}}' if self._localurl: requests.get( - f"{self._localurl}/command/floodlight_set_config?config={config}", + f"{self._localurl}/command/floodlight_set_config?config=" + f"{config}", timeout=10, ) elif self._vpnurl: requests.get( - f"{self._vpnurl}/command/floodlight_set_config?config={config}", + f"{self._vpnurl}/command/floodlight_set_config?config=" + f"{config}", timeout=10, verify=self._verify_ssl, ) @@ -399,17 +405,70 @@ def _set_light_mode(self, mode): _LOGGER.error("Presence VPN URL is None") self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( - camera=self._camera_name + cid=self._camera_id ) 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 + cid=self._camera_id ) return None else: self.async_schedule_update_ha_state(True) else: _LOGGER.error("Unsupported camera model for light mode") + + +class CameraData: + """Get the latest data from Netatmo.""" + + def __init__(self, hass, auth): + """Initialize the data object.""" + self._hass = hass + self.auth = auth + self.camera_data = None + self.camera_ids = [] + self.module_names = [] + self.camera_type = None + + def get_camera_ids(self): + """Return all camera available on the API as a list.""" + self.camera_ids = [] + self.update() + for home_id in self.camera_data.cameras: + for camera in self.camera_data.cameras[home_id].values(): + self.camera_ids.append(camera["id"]) + return self.camera_ids + + def get_module_names(self, camera_id): + """Return all module available on the API as a list.""" + self.module_names = [] + self.update() + for module in self.camera_data.modules.values(): + if camera_id == module["cam_id"]: + self.module_names.append(module["name"]) + return self.module_names + + def get_camera_type(self, cid=None): + """Return camera type for a camera, cid has preference over camera.""" + self.camera_type = self.camera_data.cameraType(cid=cid) + return self.camera_type + + def get_persons(self): + """Gather person data for webhooks.""" + for person_id, person_data in self.camera_data.persons.items(): + self._hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get( + ATTR_PSEUDO + ) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Call the Netatmo API to update the data.""" + self.camera_data = pyatmo.CameraData(self.auth, size=100) + + @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) + def update_event(self): + """Call the Netatmo API to update the events.""" + self.camera_data.updateEvent(devicetype=self.camera_type) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9e320c303c89b9..60682bea61339e 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -5,9 +5,11 @@ import pyatmo import requests -import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +# import voluptuous as vol + +# import homeassistant.helpers.config_validation as cv +from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -23,7 +25,7 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, - CONF_NAME, + # CONF_NAME, PRECISION_HALVES, STATE_OFF, TEMP_CELSIUS, @@ -31,7 +33,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from .const import DATA_NETATMO_AUTH +from .const import AUTH, DOMAIN, MANUFAKTURER _LOGGER = logging.getLogger(__name__) @@ -85,16 +87,16 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) -HOME_CONFIG_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) +# HOME_CONFIG_SCHEMA = vol.Schema( +# { +# vol.Required(CONF_NAME): cv.string, +# vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), +# } +# ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_HOMES): vol.All(cv.ensure_list, [HOME_CONFIG_SCHEMA])} -) +# PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +# {vol.Optional(CONF_HOMES): vol.All(cv.ensure_list, [HOME_CONFIG_SCHEMA])} +# ) DEFAULT_MAX_TEMP = 30 @@ -102,46 +104,39 @@ NA_VALVE = "NRV" -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NetAtmo Thermostat.""" - homes_conf = config.get(CONF_HOMES) - - auth = hass.data[DATA_NETATMO_AUTH] - - home_data = HomeData(auth) - try: - home_data.setup() - except pyatmo.NoDevice: - return - - home_ids = [] - rooms = {} - if homes_conf is not None: - for home_conf in homes_conf: - home = home_conf[CONF_NAME] - home_id = home_data.homedata.gethomeId(home) - if home_conf[CONF_ROOMS] != []: - rooms[home_id] = home_conf[CONF_ROOMS] - home_ids.append(home_id) - else: - home_ids = home_data.get_home_ids() +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Netatmo energy platform.""" + auth = hass.data[DOMAIN][AUTH] + + def get_devices(): + """Retrieve Netatmo devices.""" + devices = [] - devices = [] - for home_id in home_ids: - _LOGGER.debug("Setting up %s ...", home_id) + home_data = HomeData(auth) try: - room_data = ThermostatData(auth, home_id) + home_data.setup() except pyatmo.NoDevice: - continue - for room_id in room_data.get_room_ids(): - room_name = room_data.homedata.rooms[home_id][room_id]["name"] - _LOGGER.debug("Setting up %s (%s) ...", room_name, room_id) - if home_id in rooms and room_name not in rooms[home_id]: - _LOGGER.debug("Excluding %s ...", room_name) + return + home_ids = home_data.get_home_ids() + + for home_id in home_ids: + _LOGGER.debug("Setting up home %s ...", home_id) + try: + room_data = ThermostatData(auth, home_id) + except pyatmo.NoDevice: continue - _LOGGER.debug("Adding devices for room %s (%s) ...", room_name, room_id) - devices.append(NetatmoThermostat(room_data, room_id)) - add_entities(devices, True) + for room_id in room_data.get_room_ids(): + room_name = room_data.homedata.rooms[home_id][room_id]["name"] + _LOGGER.debug("Setting up room %s (%s) ...", room_name, room_id) + devices.append(NetatmoThermostat(room_data, room_id)) + return devices + + async_add_entities(await hass.async_add_executor_job(get_devices), True) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the NetAtmo Thermostat.""" + return class NetatmoThermostat(ClimateDevice): @@ -153,7 +148,7 @@ def __init__(self, data, room_id): self._state = None self._room_id = room_id self._room_name = self._data.homedata.rooms[self._data.home_id][room_id]["name"] - self._name = f"netatmo_{self._room_name}" + self._name = f"{MANUFAKTURER} {self._room_name}" self._current_temperature = None self._target_temperature = None self._preset = None @@ -168,6 +163,23 @@ def __init__(self, data, room_id): if self._module_type == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) + self._unique_id = f"{self._room_id}-{self._module_type}" + + @property + def device_info(self): + """Return the device info for the thermostat/valve.""" + return { + "identifiers": {(DOMAIN, self._room_id)}, + "name": self._room_name, + "manufacturer": MANUFAKTURER, + "model": self._module_type, + } + + @property + def unique_id(self): + """Return a unique ID.""" + return self._room_id + @property def supported_features(self): """Return the list of supported features.""" @@ -426,8 +438,8 @@ def update(self): except requests.exceptions.Timeout: _LOGGER.warning("Timed out when connecting to Netatmo server") return - _LOGGER.debug("Following is the debugging output for homestatus:") - _LOGGER.debug(self.homestatus.rawData) + # _LOGGER.debug("Following is the debugging output for homestatus:") + # _LOGGER.debug(self.homestatus.rawData) for room in self.homestatus.rooms: try: roomstatus = {} diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py new file mode 100644 index 00000000000000..df2deaf0d21762 --- /dev/null +++ b/homeassistant/components/netatmo/config_flow.py @@ -0,0 +1,40 @@ +"""Config flow for Netatmo.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class NetatmoFlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Netatmo OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": ( + "read_station read_camera access_camera read_thermostat " + "write_thermostat read_presence access_presence " + "read_homecoach read_smokedetector" + ) + } + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if self.hass.config_entries.async_entries(DOMAIN): + return self.async_abort(reason="already_setup") + + return await super().async_step_user(user_input) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index c036a52991bee0..5f7fb0b3ee2bb8 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -1,5 +1,50 @@ """Constants used by the Netatmo component.""" +from datetime import timedelta + +API = "api" + DOMAIN = "netatmo" +MANUFAKTURER = "Netatmo" + +# DATA_NETATMO = "netatmo" +AUTH = "netatmo_auth" + +OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" +OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" + +DATA_PERSONS = "netatmo_persons" + +NETATMO_WEBHOOK_URL = None + +DEFAULT_PERSON = "Unknown" +DEFAULT_DISCOVERY = True +DEFAULT_WEBHOOKS = False + +EVENT_PERSON = "person" +EVENT_MOVEMENT = "movement" +EVENT_HUMAN = "human" +EVENT_ANIMAL = "animal" +EVENT_VEHICLE = "vehicle" + +EVENT_BUS_PERSON = "netatmo_person" +EVENT_BUS_MOVEMENT = "netatmo_movement" +EVENT_BUS_HUMAN = "netatmo_human" +EVENT_BUS_ANIMAL = "netatmo_animal" +EVENT_BUS_VEHICLE = "netatmo_vehicle" +EVENT_BUS_OTHER = "netatmo_other" + +ATTR_ID = "id" +ATTR_PSEUDO = "pseudo" +ATTR_NAME = "name" +ATTR_EVENT_TYPE = "event_type" +ATTR_MESSAGE = "message" +ATTR_CAMERA_ID = "camera_id" +ATTR_HOME_NAME = "home_name" +ATTR_PERSONS = "persons" +ATTR_IS_KNOWN = "is_known" +ATTR_FACE_URL = "face_url" +ATTR_SNAPSHOT_URL = "snapshot_url" +ATTR_VIGNETTE_URL = "vignette_url" -DATA_NETATMO = "netatmo" -DATA_NETATMO_AUTH = "netatmo_auth" +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) +MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index ff42136350605c..7cd0311e791afd 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,7 +2,14 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==3.1.0"], - "dependencies": ["webhook"], - "codeowners": [] -} + "requirements": [ + "pyatmo==3.1.0" + ], + "dependencies": [ + "webhook" + ], + "codeowners": [ + "@cgtobi" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 051ff00c2ed882..9bbaaf08a16fa1 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -5,7 +5,8 @@ from time import time import pyatmo -import requests + +# import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -19,10 +20,8 @@ ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import call_later from homeassistant.util import Throttle - -from .const import DATA_NETATMO_AUTH, DOMAIN +from .const import AUTH, DOMAIN, MANUFAKTURER _LOGGER = logging.getLogger(__name__) @@ -121,90 +120,46 @@ } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the available Netatmo weather sensors.""" - dev = [] - auth = hass.data[DATA_NETATMO_AUTH] - - if config.get(CONF_AREAS) is not None: - for area in config[CONF_AREAS]: - data = NetatmoPublicData( - auth, - lat_ne=area[CONF_LAT_NE], - lon_ne=area[CONF_LON_NE], - lat_sw=area[CONF_LAT_SW], - lon_sw=area[CONF_LON_SW], - ) - for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: - dev.append( - NetatmoPublicSensor( - area[CONF_NAME], data, sensor_type, area[CONF_MODE] - ) - ) - else: - - def find_devices(data): - """Find all devices.""" - all_module_infos = data.get_module_infos() - all_module_names = [e["module_name"] for e in all_module_infos.values()] - module_names = config.get(CONF_MODULES, all_module_names) - entities = [] - for module_name in module_names: - if module_name not in all_module_names: - _LOGGER.info("Module %s not found", module_name) - for module in all_module_infos.values(): - if module["module_name"] not in module_names: - continue - _LOGGER.debug( - "Adding module %s %s", module["module_name"], module["id"] - ) - for condition in data.station_data.monitoredConditions( - moduleId=module["id"] - ): - entities.append(NetatmoSensor(data, module, condition.lower())) - return entities - - def _retry(_data): - try: - entities = find_devices(_data) - except requests.exceptions.Timeout: - return call_later( - hass, NETATMO_UPDATE_INTERVAL, lambda _: _retry(_data) - ) - if entities: - add_entities(entities, True) - +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Netatmo weather and homecoach platform.""" + auth = hass.data[DOMAIN][AUTH] + + def find_devices(data): + """Find all devices.""" + all_module_infos = data.get_module_infos() + entities = [] + for module in all_module_infos.values(): + _LOGGER.debug("Adding module %s %s", module["module_name"], module["id"]) + for condition in data.station_data.monitoredConditions( + moduleId=module["id"] + ): + entities.append(NetatmoSensor(data, module, condition.lower())) + return entities + + def get_devices(): + """Retrieve Netatmo devices.""" + devices = [] for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: - station = config.get(CONF_STATION) dc_data = data_class(auth) _LOGGER.debug("%s detected!", NETATMO_DEVICE_TYPES[data_class.__name__]) - if station: - station_data = dc_data.stationByName(station) - if station_data: - station_id = station_data.get("_id") - else: - _LOGGER.debug( - 'No %s station "%s" found', - NETATMO_DEVICE_TYPES[data_class.__name__], - station, - ) - else: - station_id = None - data = NetatmoData(dc_data, station_id) + data = NetatmoData(dc_data) except pyatmo.NoDevice: _LOGGER.info( "No %s devices found", NETATMO_DEVICE_TYPES[data_class.__name__] ) continue - try: - dev.extend(find_devices(data)) - except requests.exceptions.Timeout: - call_later(hass, NETATMO_UPDATE_INTERVAL, lambda _: _retry(data)) + devices.extend(find_devices(data)) - if dev: - add_entities(dev, True) + return devices + + async_add_entities(await hass.async_add_executor_job(get_devices), True) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the available Netatmo weather sensors.""" + return class NetatmoSensor(Entity): @@ -226,7 +181,7 @@ def __init__(self, netatmo_data, module_info, sensor_type): f"{module_info['station_name']} {module_info['module_name']}" ) - self._name = f"{DOMAIN} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" + self._name = f"{MANUFAKTURER} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type self._state = None self._device_class = SENSOR_TYPES[self.type][3] @@ -251,6 +206,16 @@ def device_class(self): """Return the device class of the sensor.""" return self._device_class + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._module_id)}, + "name": self.module_name, + "manufacturer": MANUFAKTURER, + "model": self._module_type, + } + @property def state(self): """Return the state of the device.""" @@ -272,7 +237,7 @@ def update(self): if self.netatmo_data.data is None: if self._state is None: return - _LOGGER.warning("No data found for %s", self.module_name) + _LOGGER.warning("No data from update") self._state = None return @@ -555,7 +520,7 @@ def update(self): class NetatmoData: """Get the latest data from Netatmo.""" - def __init__(self, station_data, station_id): + def __init__(self, station_data, station_id=None): """Initialize the data object.""" self.data = {} self.station_data = station_data @@ -569,6 +534,7 @@ def get_module_infos(self): return self.station_data.getModules(station_id=self.station_id) return self.station_data.getModules() + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data. @@ -576,43 +542,10 @@ def update(self): but with a custom logic, which takes into account the time of the last update from the cloud. """ - if time() < self._next_update or not self._update_in_progress.acquire(False): + data = self.station_data.lastData( + station=self.station_id, exclude=3600, byId=True + ) + if not data: + _LOGGER.debug("No data received when updating station %s", self.station_id) return - try: - data = self.station_data.lastData( - station=self.station_id, exclude=3600, byId=True - ) - if not data: - self._next_update = time() + NETATMO_UPDATE_INTERVAL - return - self.data = data - - newinterval = 0 - try: - for module in self.data: - if "When" in self.data[module]: - newinterval = self.data[module]["When"] - break - except TypeError: - _LOGGER.debug("No %s modules found", self.station_data.__name__) - - if newinterval: - # Try and estimate when fresh data will be available - newinterval += NETATMO_UPDATE_INTERVAL - time() - if newinterval > NETATMO_UPDATE_INTERVAL - 30: - newinterval = NETATMO_UPDATE_INTERVAL - else: - if newinterval < NETATMO_UPDATE_INTERVAL / 2: - # Never hammer the Netatmo API more than - # twice per update interval - newinterval = NETATMO_UPDATE_INTERVAL / 2 - _LOGGER.info( - "Netatmo refresh interval reset to %d seconds", newinterval - ) - else: - # Last update time not found, fall back to default value - newinterval = NETATMO_UPDATE_INTERVAL - - self._next_update = time() + newinterval - finally: - self._update_in_progress.release() + self.data = data diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index d8fa223780a187..9829b336538b12 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,37 +1 @@ -addwebhook: - description: Add webhook during runtime (e.g. if it has been banned). - fields: - url: - description: URL for which to add the webhook. - example: https://yourdomain.com:443/api/webhook/webhook_id - -dropwebhook: - 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' - -set_schedule: - description: Set the home heating schedule - fields: - schedule: - description: Schedule name - example: Standard \ No newline at end of file +# Describes the format for available Netatmo services diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json new file mode 100644 index 00000000000000..8cd4f51aee2660 --- /dev/null +++ b/homeassistant/components/netatmo/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Netatmo", + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_setup": "You can only configure one Netatmo account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Netatmo." + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6f3f0e714f6ee5..2782a975d67c77 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -57,6 +57,7 @@ "mqtt", "neato", "nest", + "netatmo", "notion", "opentherm_gw", "openuv", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 59001b5e1cb191..75fc04fd736244 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -398,6 +398,9 @@ pyalmond==0.0.2 # homeassistant.components.arlo pyarlo==0.2.3 +# homeassistant.components.netatmo +pyatmo==3.1.0 + # homeassistant.components.blackbird pyblackbird==0.5 diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py new file mode 100644 index 00000000000000..a2e0b4f515d788 --- /dev/null +++ b/tests/components/netatmo/test_config_flow.py @@ -0,0 +1,60 @@ +"""Test the Netatmo config flow.""" +from homeassistant import config_entries, setup, data_entry_flow +from homeassistant.components.netatmo.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.helpers import config_entry_oauth2_flow + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "netatmo", + { + "netatmo": { + "type": "oauth2", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "netatmo", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data["type"] == "oauth2" From 22dc31159ddb39c8c0edf93a033b7dcaed3b746e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 Nov 2019 00:27:10 +0100 Subject: [PATCH 04/58] Remove old code --- homeassistant/components/netatmo/camera.py | 25 ---------------------- 1 file changed, 25 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index f3a220c996c328..8c7113ba8dc9e5 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -4,17 +4,13 @@ import pyatmo import requests -# import voluptuous as vol - from homeassistant.components.camera import ( - # PLATFORM_SCHEMA, Camera, SUPPORT_STREAM, Camera, ) from homeassistant.const import STATE_ON, STATE_OFF -# from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -41,17 +37,6 @@ VALID_QUALITIES = ["high", "medium", "low", "poor"] -# PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( -# { -# vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, -# vol.Optional(CONF_HOME): cv.string, -# vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), -# vol.Optional(CONF_QUALITY, default=DEFAULT_QUALITY): vol.All( -# cv.string, vol.In(VALID_QUALITIES) -# ), -# } -# ) - _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} @@ -116,7 +101,6 @@ def __init__(self, data, camera_id, camera_type, verify_ssl, quality): ) self._camera_type = camera_type self._unique_id = f"{self._camera_id}-{self._camera_type}" - _LOGGER.debug("Setting up camera %s", self._unique_id) self._verify_ssl = verify_ssl self._quality = quality @@ -202,9 +186,6 @@ def device_info(self): @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 @@ -216,8 +197,6 @@ def device_state_attributes(self): if self.model == "Presence": attr["light_mode_status"] = self._light_mode_status - # _LOGGER.debug("Attributes of '%s' = %s", self._name, attr) - return attr @property @@ -284,8 +263,6 @@ async def async_added_to_hass(self): def update(self): """Update entity status.""" - # _LOGGER.debug("Updating camera '%s'", self._name) - # Refresh camera data self._data.update() @@ -299,8 +276,6 @@ def update(self): # Monitoring status self._status = camera["status"] - # _LOGGER.debug("Status of '%s' = %s", self._name, self._status) - # SD Card status self._sd_status = camera["sd_status"] From 0139896ae1ffa0c4a3d33638248333ac49ebb9a4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 Nov 2019 00:38:52 +0100 Subject: [PATCH 05/58] Clean up --- homeassistant/components/netatmo/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 6dbf9d63db2b94..a1e3e7cf0516a2 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -20,19 +20,15 @@ def __init__( implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, ): """Initialize Netatmo Auth.""" - _LOGGER.debug("Netatmo ConfigEntryNetatmoAuth") self.hass = hass self.config_entry = config_entry - _LOGGER.debug("Netatmo ConfigEntryAuth 2") self.session = config_entry_oauth2_flow.OAuth2Session( hass, config_entry, implementation ) - _LOGGER.debug("Netatmo ConfigEntryAuth 3") super().__init__(token=self.session.token) def refresh_tokens(self) -> dict: """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" - _LOGGER.debug("Netatmo ConfigEntryAuth.refresh_tokens") run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self.hass.loop ).result() From f5ea0226ee8d89b216b43d0079184dc7ce778986 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 Nov 2019 00:39:10 +0100 Subject: [PATCH 06/58] Clean up --- homeassistant/components/netatmo/climate.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 60682bea61339e..5a2b114b968756 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -6,9 +6,6 @@ import pyatmo import requests -# import voluptuous as vol - -# import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -25,7 +22,6 @@ from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, - # CONF_NAME, PRECISION_HALVES, STATE_OFF, TEMP_CELSIUS, @@ -87,17 +83,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) -# HOME_CONFIG_SCHEMA = vol.Schema( -# { -# vol.Required(CONF_NAME): cv.string, -# vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), -# } -# ) - -# PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( -# {vol.Optional(CONF_HOMES): vol.All(cv.ensure_list, [HOME_CONFIG_SCHEMA])} -# ) - DEFAULT_MAX_TEMP = 30 NA_THERM = "NATherm1" From 451d727f4bc2528dd6ce79d96dd0b45e8205d29e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 29 Nov 2019 00:39:42 +0100 Subject: [PATCH 07/58] Clean up --- homeassistant/components/netatmo/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 8c7113ba8dc9e5..684d763b282660 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -104,14 +104,14 @@ def __init__(self, data, camera_id, camera_type, verify_ssl, quality): self._verify_ssl = verify_ssl self._quality = quality - # URLs. + # URLs self._vpnurl = None self._localurl = None # Identifier self._id = None - # Monitoring status. + # Monitoring status self._status = None # SD Card status From 70b5a6e1f27e9e9ff03e804b1e9d23e875e9f457 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 9 Dec 2019 16:07:43 +0100 Subject: [PATCH 08/58] Refactor binary sensor --- homeassistant/components/netatmo/__init__.py | 12 +- homeassistant/components/netatmo/api.py | 2 +- .../components/netatmo/binary_sensor.py | 234 ++++++------------ homeassistant/components/netatmo/camera.py | 43 ++-- homeassistant/components/netatmo/climate.py | 5 + .../components/netatmo/config_flow.py | 17 +- homeassistant/components/netatmo/const.py | 3 +- homeassistant/components/netatmo/sensor.py | 104 +++++--- 8 files changed, 215 insertions(+), 205 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 3847e3c6a00fe3..076fc1fe0fd334 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -27,7 +27,7 @@ ) # PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor", "switch"] -PLATFORMS = ["camera", "climate", "sensor"] +PLATFORMS = ["binary_sensor", "camera", "climate", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): @@ -69,13 +69,17 @@ def _service_setschedule(service): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Netatmo from a config entry.""" + # Backwards compat + if "auth_implementation" not in entry.data: + hass.config_entries.async_update_entry( + entry, data={**entry.data, "auth_implementation": DOMAIN} + ) + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry ) - session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) - - hass.data[DOMAIN][AUTH] = api.ConfigEntryNetatmoAuth(hass, entry, session) + hass.data[DOMAIN][AUTH] = api.ConfigEntryNetatmoAuth(hass, entry, implementation) for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index a1e3e7cf0516a2..2b49d038c3852e 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -27,7 +27,7 @@ def __init__( ) super().__init__(token=self.session.token) - def refresh_tokens(self) -> dict: + def refresh_tokens(self,) -> dict: """Refresh and return new Netatmo tokens using Home Assistant OAuth2 session.""" run_coroutine_threadsafe( self.session.async_ensure_token_valid(), self.hass.loop diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index b6babf5bc32399..c6353d46a083bc 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -2,13 +2,10 @@ import logging from pyatmo import NoDevice -import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import CONF_TIMEOUT -from homeassistant.helpers import config_validation as cv +from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import DATA_NETATMO_AUTH +from .const import AUTH, DOMAIN, MANUFAKTURER from .camera import CameraData _LOGGER = logging.getLogger(__name__) @@ -27,6 +24,8 @@ } TAG_SENSOR_TYPES = {"Tag Vibration": "vibration", "Tag Open": "opening"} +SENSOR_TYPES = {"NACamera": WELCOME_SENSOR_TYPES, "NOC": PRESENCE_SENSOR_TYPES} + CONF_HOME = "home" CONF_CAMERAS = "cameras" CONF_WELCOME_SENSORS = "welcome_sensors" @@ -35,130 +34,61 @@ DEFAULT_TIMEOUT = 90 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_HOME): cv.string, - vol.Optional( - CONF_PRESENCE_SENSORS, default=list(PRESENCE_SENSOR_TYPES) - ): vol.All(cv.ensure_list, [vol.In(PRESENCE_SENSOR_TYPES)]), - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_WELCOME_SENSORS, default=list(WELCOME_SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(WELCOME_SENSOR_TYPES)] - ), - } -) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Netatmo energy platform.""" + auth = hass.data[DOMAIN][AUTH] -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the access to Netatmo binary sensor.""" - home = config.get(CONF_HOME) - timeout = config.get(CONF_TIMEOUT) - if timeout is None: - timeout = DEFAULT_TIMEOUT - - module_name = None - - auth = hass.data[DATA_NETATMO_AUTH] - - try: - data = CameraData(hass, auth, home) - if not data.get_camera_names(): - return None - except NoDevice: - return None - - welcome_sensors = config.get(CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) - presence_sensors = config.get(CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES) - tag_sensors = config.get(CONF_TAG_SENSORS, TAG_SENSOR_TYPES) - - for camera_name in data.get_camera_names(): - camera_type = data.get_camera_type(camera=camera_name, home=home) - if camera_type == "NACamera": - if CONF_CAMERAS in config: - if ( - config[CONF_CAMERAS] != [] - and camera_name not in config[CONF_CAMERAS] - ): - continue - for variable in welcome_sensors: - add_entities( - [ - NetatmoBinarySensor( - data, - camera_name, - module_name, - home, - timeout, - camera_type, - variable, - ) - ], - True, - ) - if camera_type == "NOC": - if CONF_CAMERAS in config: - if ( - config[CONF_CAMERAS] != [] - and camera_name not in config[CONF_CAMERAS] - ): - continue - for variable in presence_sensors: - add_entities( - [ - NetatmoBinarySensor( - data, - camera_name, - module_name, - home, - timeout, - camera_type, - variable, - ) - ], - True, - ) + def get_devices(): + """Retrieve Netatmo devices.""" + devices = [] + + try: + data = CameraData(hass, auth) + except NoDevice: + _LOGGER.debug("No camera devices to add") + + for camera_id in data.get_camera_ids(): + camera_type = data.get_camera_type(camera_id=camera_id) + home_id = data.get_camera_home_id(camera_id=camera_id) - for module_name in data.get_module_names(camera_name): - for variable in tag_sensors: - camera_type = None - add_entities( - [ - NetatmoBinarySensor( - data, - camera_name, - module_name, - home, - timeout, - camera_type, - variable, - ) - ], - True, + for sensor_name in SENSOR_TYPES[camera_type]: + devices.append( + NetatmoBinarySensor(data, camera_id, home_id, sensor_name) ) + return devices + + async_add_entities(await hass.async_add_executor_job(get_devices), True) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the access to Netatmo binary sensor.""" + return + class NetatmoBinarySensor(BinarySensorDevice): """Represent a single binary sensor in a Netatmo Camera device.""" - def __init__( - self, data, camera_name, module_name, home, timeout, camera_type, sensor - ): + def __init__(self, data, camera_id, home_id, sensor_type, module_id=None): """Set up for access to the Netatmo camera events.""" self._data = data - self._camera_name = camera_name - self._module_name = module_name - self._home = home - self._timeout = timeout - if home: - self._name = f"{home} / {camera_name}" - else: - self._name = camera_name - if module_name: - self._name += f" / {module_name}" - self._sensor_name = sensor - self._name += f" {sensor}" - self._cameratype = camera_type + self._camera_id = camera_id + self._module_id = module_id + self._sensor_type = sensor_type + camera_info = data.camera_data.cameraById(cid=camera_id) + self._camera_name = camera_info["name"] + self._camera_type = camera_info["type"] + if module_id: + self._module_name = data.camera_data.moduleById(mid=module_id)["name"] + self._home_id = home_id + self._home_name = self._data.camera_data.getHomeName(home_id=home_id) + self._timeout = DEFAULT_TIMEOUT + self._name = ( + f"{MANUFAKTURER}{self._camera_name} {self._module_name} {sensor_type}" + if module_id is not None + else f"{MANUFAKTURER}{self._camera_name} {sensor_type}" + ) self._state = None @property @@ -169,11 +99,11 @@ def name(self): @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" - if self._cameratype == "NACamera": - return WELCOME_SENSOR_TYPES.get(self._sensor_name) - if self._cameratype == "NOC": - return PRESENCE_SENSOR_TYPES.get(self._sensor_name) - return TAG_SENSOR_TYPES.get(self._sensor_name) + if self._camera_type == "NACamera": + return WELCOME_SENSOR_TYPES.get(self._sensor_type) + if self._camera_type == "NOC": + return PRESENCE_SENSOR_TYPES.get(self._sensor_type) + return TAG_SENSOR_TYPES.get(self._sensor_type) @property def is_on(self): @@ -185,41 +115,41 @@ def update(self): self._data.update() self._data.update_event() - if self._cameratype == "NACamera": - if self._sensor_name == "Someone known": - self._state = self._data.camera_data.someoneKnownSeen( - self._home, self._camera_name, self._timeout + if self._camera_type == "NACamera": + if self._sensor_type == "Someone known": + self._state = self._data.camera_data.someone_known_seen( + cid=self._camera_id, exclude=self._timeout ) - elif self._sensor_name == "Someone unknown": - self._state = self._data.camera_data.someoneUnknownSeen( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Someone unknown": + self._state = self._data.camera_data.someone_unknown_seen( + cid=self._camera_id, exclude=self._timeout ) - elif self._sensor_name == "Motion": - self._state = self._data.camera_data.motionDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Motion": + self._state = self._data.camera_data.motion_detected( + cid=self._camera_id, exclude=self._timeout ) - elif self._cameratype == "NOC": - if self._sensor_name == "Outdoor motion": - self._state = self._data.camera_data.outdoormotionDetected( - self._home, self._camera_name, self._timeout + elif self._camera_type == "NOC": + if self._sensor_type == "Outdoor motion": + self._state = self._data.camera_data.outdoor_motion_detected( + cid=self._camera_id, offset=self._timeout ) - elif self._sensor_name == "Outdoor human": - self._state = self._data.camera_data.humanDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Outdoor human": + self._state = self._data.camera_data.human_detected( + cid=self._camera_id, offset=self._timeout ) - elif self._sensor_name == "Outdoor animal": - self._state = self._data.camera_data.animalDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Outdoor animal": + self._state = self._data.camera_data.animal_detected( + cid=self._camera_id, offset=self._timeout ) - elif self._sensor_name == "Outdoor vehicle": - self._state = self._data.camera_data.carDetected( - self._home, self._camera_name, self._timeout + elif self._sensor_type == "Outdoor vehicle": + self._state = self._data.camera_data.car_detected( + cid=self._camera_id, offset=self._timeout ) - if self._sensor_name == "Tag Vibration": - self._state = self._data.camera_data.moduleMotionDetected( - self._home, self._module_name, self._camera_name, self._timeout + if self._sensor_type == "Tag Vibration": + self._state = self._data.camera_data.module_motion_detected( + mid=self._module_id, cid=self._camera_id, exclude=self._timeout ) - elif self._sensor_name == "Tag Open": - self._state = self._data.camera_data.moduleOpened( - self._home, self._module_name, self._camera_name, self._timeout + elif self._sensor_type == "Tag Open": + self._state = self._data.camera_data.module_opened( + mid=self._module_id, cid=self._camera_id, exclude=self._timeout ) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 684d763b282660..bf1537c14e8ef6 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -42,19 +42,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" - auth = hass.data[DOMAIN][AUTH] def get_devices(): """Retrieve Netatmo devices.""" devices = [] try: - data = CameraData(hass, auth) - for camera_id in data.get_camera_ids(): - camera_type = data.get_camera_type(cid=camera_id) + camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) + for camera_id in camera_data.get_camera_ids(): + _LOGGER.debug("Setting up camera %s", camera_id) + camera_type = camera_data.get_camera_type(camera_id=camera_id) devices.append( - NetatmoCamera(data, camera_id, camera_type, True, DEFAULT_QUALITY) + NetatmoCamera( + camera_data, camera_id, camera_type, True, DEFAULT_QUALITY + ) ) - data.get_persons() + camera_data.get_persons() except pyatmo.NoDevice: _LOGGER.debug("No cameras found") return devices @@ -260,6 +262,11 @@ async def async_added_to_hass(self): self.hass, f"set_light_off_{self.entity_id}", self.set_light_off ) + @property + def unique_id(self): + """Return the unique ID for this sensor.""" + return self._unique_id + def update(self): """Update entity status.""" @@ -274,25 +281,25 @@ def update(self): ) # Monitoring status - self._status = camera["status"] + self._status = camera.get("status") # SD Card status - self._sd_status = camera["sd_status"] + self._sd_status = camera.get("sd_status") # Power status - self._alim_status = camera["alim_status"] + self._alim_status = camera.get("alim_status") # Is local - self._is_local = camera["is_local"] + self._is_local = camera.get("is_local") # VPN URL - self._vpn_url = camera["vpn_url"] + self._vpn_url = camera.get("vpn_url") self.is_streaming = self._alim_status == "on" if self.model == "Presence": # Light mode status - self._light_mode_status = camera["light_mode_status"] + self._light_mode_status = camera.get("light_mode_status") # Camera method overrides @@ -408,6 +415,14 @@ def __init__(self, hass, auth): self.module_names = [] self.camera_type = None + def get_camera_home_id(self, camera_id): + """Return the home id for a given camera id.""" + for home_id in self.camera_data.cameras: + for camera in self.camera_data.cameras[home_id].values(): + if camera["id"] == camera_id: + return home_id + return None + def get_camera_ids(self): """Return all camera available on the API as a list.""" self.camera_ids = [] @@ -426,9 +441,9 @@ def get_module_names(self, camera_id): self.module_names.append(module["name"]) return self.module_names - def get_camera_type(self, cid=None): + def get_camera_type(self, camera_id=None): """Return camera type for a camera, cid has preference over camera.""" - self.camera_type = self.camera_data.cameraType(cid=cid) + self.camera_type = self.camera_data.cameraType(cid=camera_id) return self.camera_type def get_persons(self): diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 5a2b114b968756..d7742851997e65 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,4 +1,9 @@ """Support for Netatmo Smart thermostats.""" + +# TODO +# * expose schedule switching +# * + from datetime import timedelta import logging from typing import List, Optional diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index df2deaf0d21762..1da60462c0717f 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -26,9 +26,20 @@ def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" return { "scope": ( - "read_station read_camera access_camera read_thermostat " - "write_thermostat read_presence access_presence " - "read_homecoach read_smokedetector" + " ".join( + [ + "read_station", + "read_camera", + "access_camera", + "write_camera", + "read_presence", + "access_presence", + "read_homecoach", + "read_smokedetector", + "read_thermostat", + "write_thermostat", + ] + ) ) } diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 5f7fb0b3ee2bb8..77a8053cc09a98 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -6,8 +6,9 @@ DOMAIN = "netatmo" MANUFAKTURER = "Netatmo" -# DATA_NETATMO = "netatmo" AUTH = "netatmo_auth" +CAMERA_DATA = "netatmo_camera" +HOME_DATA = "netatmo_home_data" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 9bbaaf08a16fa1..41db1585c865ad 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,6 +1,7 @@ """Support for the Netatmo Weather Service.""" from datetime import timedelta import logging + import threading from time import time @@ -36,7 +37,7 @@ DEFAULT_MODE = "avg" MODE_TYPES = {"max", "avg"} -DEFAULT_NAME_PUBLIC = "Netatmo Public Data" +DEFAULT_NAME_PUBLIC = "Public Data" # This is the Netatmo data upload interval in seconds NETATMO_UPDATE_INTERVAL = 600 @@ -90,8 +91,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_STATION): cv.string, - vol.Optional(CONF_MODULES): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_AREAS): vol.All( cv.ensure_list, [ @@ -104,7 +103,7 @@ vol.Optional(CONF_NAME, default=DEFAULT_NAME_PUBLIC): cv.string, } ], - ), + ) } ) @@ -139,13 +138,14 @@ def find_devices(data): def get_devices(): """Retrieve Netatmo devices.""" devices = [] + for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: dc_data = data_class(auth) _LOGGER.debug("%s detected!", NETATMO_DEVICE_TYPES[data_class.__name__]) - data = NetatmoData(dc_data) + data = NetatmoData(auth, dc_data) except pyatmo.NoDevice: - _LOGGER.info( + _LOGGER.debug( "No %s devices found", NETATMO_DEVICE_TYPES[data_class.__name__] ) continue @@ -157,9 +157,31 @@ def get_devices(): async_add_entities(await hass.async_add_executor_job(get_devices), True) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" - return + + def get_devices(): + devices = [] + + if config.get(CONF_AREAS) is not None: + for area in config[CONF_AREAS]: + _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) + data = NetatmoPublicData( + hass.data[DOMAIN][AUTH], + lat_ne=area[CONF_LAT_NE], + lon_ne=area[CONF_LON_NE], + lat_sw=area[CONF_LAT_SW], + lon_sw=area[CONF_LON_SW], + ) + for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: + devices.append( + NetatmoPublicSensor( + area[CONF_NAME], data, sensor_type, area[CONF_MODE] + ) + ) + return devices + + async_add_entities(await hass.async_add_executor_job(get_devices), True) class NetatmoSensor(Entity): @@ -244,7 +266,8 @@ def update(self): data = self.netatmo_data.data.get(self._module_id) if data is None: - _LOGGER.warning("No data found for %s", self.module_name) + _LOGGER.info("No data found for %s (%s)", self.module_name, self._module_id) + _LOGGER.error("data: %s", self.netatmo_data.data) self._state = None return @@ -399,7 +422,7 @@ def update(self): elif data["health_idx"] == 4: self._state = "Unhealthy" except KeyError: - _LOGGER.error("No %s data found for %s", self.type, self.module_name) + _LOGGER.info("No %s data found for %s", self.type, self.module_name) self._state = None return @@ -412,7 +435,7 @@ def __init__(self, area_name, data, sensor_type, mode): self.netatmo_data = data self.type = sensor_type self._mode = mode - self._name = "{} {}".format(area_name, SENSOR_TYPES[self.type][0]) + self._name = f"{MANUFAKTURER} {area_name} {SENSOR_TYPES[self.type][0]}" self._area_name = area_name self._state = None self._device_class = SENSOR_TYPES[self.type][3] @@ -434,6 +457,16 @@ def device_class(self): """Return the device class of the sensor.""" return self._device_class + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._area_name)}, + "name": self._area_name, + "manufacturer": MANUFAKTURER, + "model": "public", + } + @property def state(self): """Return the state of the device.""" @@ -449,7 +482,7 @@ def update(self): self.netatmo_data.update() if self.netatmo_data.data is None: - _LOGGER.warning("No data found for %s", self._name) + _LOGGER.info("No data found for %s", self._name) self._state = None return @@ -501,14 +534,21 @@ def __init__(self, auth, lat_ne, lon_ne, lat_sw, lon_sw): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" - data = pyatmo.PublicData( - self.auth, - LAT_NE=self.lat_ne, - LON_NE=self.lon_ne, - LAT_SW=self.lat_sw, - LON_SW=self.lon_sw, - filtering=True, - ) + try: + data = pyatmo.PublicData( + self.auth, + LAT_NE=self.lat_ne, + LON_NE=self.lon_ne, + LAT_SW=self.lat_sw, + LON_SW=self.lon_sw, + filtering=True, + ) + except pyatmo.NoDevice: + data = None + + if not data: + _LOGGER.debug("No data received when updating public station data") + return if data.CountStationInArea() == 0: _LOGGER.warning("No Stations available in this area.") @@ -520,18 +560,16 @@ def update(self): class NetatmoData: """Get the latest data from Netatmo.""" - def __init__(self, station_data, station_id=None): + def __init__(self, auth, station_data): """Initialize the data object.""" self.data = {} self.station_data = station_data - self.station_id = station_id self._next_update = time() + self.auth = auth self._update_in_progress = threading.Lock() def get_module_infos(self): """Return all modules available on the API as a dict.""" - if self.station_id is not None: - return self.station_data.getModules(station_id=self.station_id) return self.station_data.getModules() @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -542,10 +580,16 @@ def update(self): but with a custom logic, which takes into account the time of the last update from the cloud. """ - data = self.station_data.lastData( - station=self.station_id, exclude=3600, byId=True - ) - if not data: - _LOGGER.debug("No data received when updating station %s", self.station_id) + if not self._update_in_progress.acquire(False): return - self.data = data + + try: + self.station_data = self.station_data.__class__(self.auth) + + data = self.station_data.lastData(exclude=3600, byId=True) + if not data: + _LOGGER.debug("No data received when updating station data") + return + self.data = data + finally: + self._update_in_progress.release() From 23d44ab86131c48e5ef5312a2e1ae0d8770f48f7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Mon, 9 Dec 2019 16:34:34 +0100 Subject: [PATCH 09/58] Add initial light implementation --- homeassistant/components/netatmo/camera.py | 4 +- homeassistant/components/netatmo/light.py | 108 ++++++++++++++++++ .../components/netatmo/services.yaml | 30 +++++ 3 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/netatmo/light.py diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index bf1537c14e8ef6..b79705bca7372a 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Netatmo weather and homecoach platform.""" + """Set up the Netatmo camera platform.""" def get_devices(): """Retrieve Netatmo devices.""" @@ -91,7 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class NetatmoCamera(Camera): - """Representation of the images published from a Netatmo camera.""" + """Representation of a Netatmo camera.""" def __init__(self, data, camera_id, camera_type, verify_ssl, quality): """Set up for access to the Netatmo camera images.""" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py new file mode 100644 index 00000000000000..62dd9c17858982 --- /dev/null +++ b/homeassistant/components/netatmo/light.py @@ -0,0 +1,108 @@ +"""Support for the Netatmo camera lights.""" +import logging + +import pyatmo + +# import requests + +from homeassistant.components.light import Light + +# from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + # async_dispatcher_connect, +) + +from .const import AUTH, DOMAIN, MANUFAKTURER +from .camera import CameraData + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Netatmo camera platform.""" + + def get_devices(): + """Retrieve Netatmo devices.""" + devices = [] + try: + camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) + for camera_id in camera_data.get_camera_ids(): + _LOGGER.debug("Setting up camera %s", camera_id) + camera_type = camera_data.get_camera_type(camera_id=camera_id) + if camera_type == "NOC": + devices.append(NetatmoLight(camera_id)) + camera_data.get_persons() + except pyatmo.NoDevice: + _LOGGER.debug("No cameras found") + return devices + + async_add_entities(await hass.async_add_executor_job(get_devices), True) + + 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 NetatmoLight(Light): + """Representation of a Netatmo Presence camera light.""" + + def __init__(self, camera_id): + """Initialize a Netatmo Presence camera light.""" + self._camera_id = camera_id + self._name = f"{MANUFAKTURER} {camera_id}" + self._is_on = False + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def supported_features(self): + """Flag supported features.""" + return 0 + + @property + def should_poll(self): + """Return if we should poll this device.""" + return True + + @property + def assumed_state(self) -> bool: + """Return False if unable to access real state of the entity.""" + return False + + @property + def is_on(self): + """Return true if light is on.""" + return self._is_on + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + # TODO + self._is_on = True + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + # TODO + self._is_on = False + self.schedule_update_ha_state() diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 9829b336538b12..7fc9ac400ce9ee 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1 +1,31 @@ # Describes the format for available Netatmo services +addwebhook: + description: Add webhook during runtime (e.g. if it has been banned). + fields: + url: + description: URL for which to add the webhook. + example: https://yourdomain.com:443/api/webhook/webhook_id + +dropwebhook: + 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" From 17f42c8e1422868d25824921e884b18c465612ee Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 10 Dec 2019 00:08:15 +0100 Subject: [PATCH 10/58] Add discovery --- homeassistant/components/netatmo/__init__.py | 10 ++- homeassistant/components/netatmo/const.py | 1 + .../components/netatmo/manifest.json | 7 +- homeassistant/components/netatmo/sensor.py | 74 +++++++++++++------ 4 files changed, 66 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 076fc1fe0fd334..0a67721b280bb4 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -9,7 +9,14 @@ from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.config_entries import ConfigEntry -from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, AUTH, DATA_PERSONS +from .const import ( + AUTH, + CONF_PUBLIC, + DATA_PERSONS, + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) from . import api, config_flow _LOGGER = logging.getLogger(__name__) @@ -34,6 +41,7 @@ async def async_setup(hass: HomeAssistant, config: dict): """Set up the Netatmo component.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_PERSONS] = {} + hass.data[DOMAIN][CONF_PUBLIC] = config.get("sensor", {}) if DOMAIN not in config: return True diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 77a8053cc09a98..2a2a4e767de2c4 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -7,6 +7,7 @@ MANUFAKTURER = "Netatmo" AUTH = "netatmo_auth" +CONF_PUBLIC = "public_sensor_config" CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 7cd0311e791afd..a5b4d5552756d4 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -11,5 +11,10 @@ "codeowners": [ "@cgtobi" ], - "config_flow": true + "config_flow": true, + "homekit": { + "models": [ + "Relay" + ] + } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 41db1585c865ad..6f8efbe0b67ca6 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import AUTH, DOMAIN, MANUFAKTURER +from .const import AUTH, CONF_PUBLIC, DOMAIN, MANUFAKTURER _LOGGER = logging.getLogger(__name__) @@ -122,6 +122,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" auth = hass.data[DOMAIN][AUTH] + config_public = hass.data[DOMAIN][CONF_PUBLIC] def find_devices(data): """Find all devices.""" @@ -135,6 +136,28 @@ def find_devices(data): entities.append(NetatmoSensor(data, module, condition.lower())) return entities + def setup_public(): + devices = [] + _LOGGER.debug("Adding public weather sensor %s", config_public) + + if config_public.get(CONF_AREAS) is not None: + for area in config_public[CONF_AREAS]: + _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) + data = NetatmoPublicData( + hass.data[DOMAIN][AUTH], + lat_ne=area[CONF_LAT_NE], + lon_ne=area[CONF_LON_NE], + lat_sw=area[CONF_LAT_SW], + lon_sw=area[CONF_LON_SW], + ) + for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: + devices.append( + NetatmoPublicSensor( + area[CONF_NAME], data, sensor_type, area[CONF_MODE] + ) + ) + return devices + def get_devices(): """Retrieve Netatmo devices.""" devices = [] @@ -155,33 +178,36 @@ def get_devices(): return devices async_add_entities(await hass.async_add_executor_job(get_devices), True) + async_add_entities(await hass.async_add_executor_job(setup_public), True) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" - - def get_devices(): - devices = [] - - if config.get(CONF_AREAS) is not None: - for area in config[CONF_AREAS]: - _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) - data = NetatmoPublicData( - hass.data[DOMAIN][AUTH], - lat_ne=area[CONF_LAT_NE], - lon_ne=area[CONF_LON_NE], - lat_sw=area[CONF_LAT_SW], - lon_sw=area[CONF_LON_SW], - ) - for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: - devices.append( - NetatmoPublicSensor( - area[CONF_NAME], data, sensor_type, area[CONF_MODE] - ) - ) - return devices - - async_add_entities(await hass.async_add_executor_job(get_devices), True) + return + + +# def get_devices(): +# devices = [] + +# if config.get(CONF_AREAS) is not None: +# for area in config[CONF_AREAS]: +# _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) +# data = NetatmoPublicData( +# hass.data[DOMAIN][AUTH], +# lat_ne=area[CONF_LAT_NE], +# lon_ne=area[CONF_LON_NE], +# lat_sw=area[CONF_LAT_SW], +# lon_sw=area[CONF_LON_SW], +# ) +# for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: +# devices.append( +# NetatmoPublicSensor( +# area[CONF_NAME], data, sensor_type, area[CONF_MODE] +# ) +# ) +# return devices + +# async_add_entities(await hass.async_add_executor_job(get_devices), True) class NetatmoSensor(Entity): From 583c0b688bfaf831a4a54ed11f48549cfd033eca Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 10 Dec 2019 00:28:40 +0100 Subject: [PATCH 11/58] Add set schedule service back in --- homeassistant/components/netatmo/__init__.py | 14 -------------- homeassistant/components/netatmo/climate.py | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 0a67721b280bb4..157416a344d0b8 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -58,20 +58,6 @@ async def async_setup(hass: HomeAssistant, config: dict): ), ) - def _service_setschedule(service): - """Service to change current home schedule.""" - schedule_name = service.data.get(ATTR_SCHEDULE) - home_data.switchHomeSchedule(schedule=schedule_name) - _LOGGER.info("Set home schedule to %s", schedule_name) - - if home_data is not None: - hass.services.register( - DOMAIN, - SERVICE_SETSCHEDULE, - _service_setschedule, - schema=SCHEMA_SERVICE_SETSCHEDULE, - ) - return True diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index d7742851997e65..c018beedb6f196 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,7 +2,6 @@ # TODO # * expose schedule switching -# * from datetime import timedelta import logging @@ -31,7 +30,6 @@ STATE_OFF, TEMP_CELSIUS, ) -import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from .const import AUTH, DOMAIN, MANUFAKTURER @@ -123,6 +121,20 @@ def get_devices(): async_add_entities(await hass.async_add_executor_job(get_devices), True) + # def _service_setschedule(service): + # """Service to change current home schedule.""" + # schedule_name = service.data.get(ATTR_SCHEDULE) + # home_data.switchHomeSchedule(schedule=schedule_name) + # _LOGGER.info("Set home schedule to %s", schedule_name) + + # if home_data is not None: + # hass.services.register( + # DOMAIN, + # SERVICE_SETSCHEDULE, + # _service_setschedule, + # schema=SCHEMA_SERVICE_SETSCHEDULE, + # ) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" From 43ba3094b5de518bed232b74ece6d62095e96a88 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 10 Dec 2019 09:53:31 +0100 Subject: [PATCH 12/58] Add discovery via homekit --- homeassistant/components/netatmo/config_flow.py | 5 +++++ homeassistant/components/netatmo/manifest.json | 4 +++- homeassistant/generated/zeroconf.py | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 1da60462c0717f..8f59382dd46923 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -3,6 +3,7 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -49,3 +50,7 @@ async def async_step_user(self, user_input=None): return self.async_abort(reason="already_setup") return await super().async_step_user(user_input) + + async def async_step_homekit(self, homekit_info): + """Handle HomeKit discovery.""" + return await self.async_step_user() diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index a5b4d5552756d4..ebfe7ca4ca31c2 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -14,7 +14,9 @@ "config_flow": true, "homekit": { "models": [ - "Relay" + "Netatmo Relay", + "Presence", + "Welcome" ] } } \ No newline at end of file diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 306b3850a1b709..eceb2ee3fd59a0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -32,6 +32,9 @@ HOMEKIT = { "BSB002": "hue", "LIFX": "lifx", + "Netatmo Relay": "netatmo", + "Presence": "netatmo", "TRADFRI": "tradfri", + "Welcome": "netatmo", "Wemo": "wemo" } From 724bc98e27c82a019f374b61230923c2a344771c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 10 Dec 2019 13:44:05 +0100 Subject: [PATCH 13/58] More work on the light --- homeassistant/components/netatmo/__init__.py | 6 +- homeassistant/components/netatmo/camera.py | 103 +------------ homeassistant/components/netatmo/light.py | 148 +++++++++++++++---- 3 files changed, 124 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 157416a344d0b8..46072bc70e7d60 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,4 +1,8 @@ """The Netatmo integration.""" + +# TODO: +# * fix webhooks + import asyncio import logging @@ -34,7 +38,7 @@ ) # PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor", "switch"] -PLATFORMS = ["binary_sensor", "camera", "climate", "sensor"] +PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index b79705bca7372a..e31b8b0f62d8f9 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -4,17 +4,8 @@ import pyatmo import requests -from homeassistant.components.camera import ( - Camera, - SUPPORT_STREAM, - Camera, -) +from homeassistant.components.camera import Camera, SUPPORT_STREAM from homeassistant.const import STATE_ON, STATE_OFF - -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.util import Throttle from .const import ( @@ -63,27 +54,6 @@ def get_devices(): async_add_entities(await hass.async_add_executor_job(get_devices), True) - 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 - ) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up access to Netatmo cameras.""" @@ -247,21 +217,6 @@ def model(self): return "Welcome" return None - # Other Entity method overrides - - 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 - ) - @property def unique_id(self): """Return the unique ID for this sensor.""" @@ -346,62 +301,6 @@ def _enable_motion_detection(self, enable): 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 = f'{{"mode":"{mode}"}}' - if self._localurl: - requests.get( - f"{self._localurl}/command/floodlight_set_config?config=" - f"{config}", - timeout=10, - ) - elif self._vpnurl: - requests.get( - f"{self._vpnurl}/command/floodlight_set_config?config=" - f"{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( - cid=self._camera_id - ) - 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( - cid=self._camera_id - ) - return None - else: - self.async_schedule_update_ha_state(True) - else: - _LOGGER.error("Unsupported camera model for light mode") - class CameraData: """Get the latest data from Netatmo.""" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 62dd9c17858982..2edc3a8c6afe71 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -8,10 +8,11 @@ from homeassistant.components.light import Light # from homeassistant.const import STATE_ON, STATE_OFF -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, - # async_dispatcher_connect, -) + +# from homeassistant.helpers.dispatcher import ( +# async_dispatcher_send, +# async_dispatcher_connect, +# ) from .const import AUTH, DOMAIN, MANUFAKTURER from .camera import CameraData @@ -20,7 +21,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the Netatmo camera platform.""" + """Set up the Netatmo camera light platform.""" def get_devices(): """Retrieve Netatmo devices.""" @@ -28,10 +29,10 @@ def get_devices(): try: camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) for camera_id in camera_data.get_camera_ids(): - _LOGGER.debug("Setting up camera %s", camera_id) camera_type = camera_data.get_camera_type(camera_id=camera_id) if camera_type == "NOC": - devices.append(NetatmoLight(camera_id)) + _LOGGER.debug("Setting up camera %s", camera_id) + devices.append(NetatmoLight(camera_id, camera_data)) camera_data.get_persons() except pyatmo.NoDevice: _LOGGER.debug("No cameras found") @@ -39,42 +40,39 @@ def get_devices(): async_add_entities(await hass.async_add_executor_job(get_devices), True) - 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 NetatmoLight(Light): """Representation of a Netatmo Presence camera light.""" - def __init__(self, camera_id): + def __init__(self, camera_id: str, camera_data: CameraData): """Initialize a Netatmo Presence camera light.""" self._camera_id = camera_id - self._name = f"{MANUFAKTURER} {camera_id}" + self._camera_data = camera_data + self._module_type = camera_data.camera_data[camera_id].get("type") + self._name = f"{MANUFAKTURER} {camera_data.camera_data[camera_id].get('name')}" self._is_on = False + self._unique_id = f"{self._camera_id}-{self._name}-light" @property def name(self): """Return the display name of this light.""" return self._name + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._camera_id)}, + "name": self._name, + "manufacturer": MANUFAKTURER, + "model": self._module_type, + } + + @property + def unique_id(self): + """Return the unique ID for this light.""" + return self._unique_id + @property def supported_features(self): """Flag supported features.""" @@ -106,3 +104,93 @@ def turn_off(self, **kwargs): # TODO self._is_on = False self.schedule_update_ha_state() + + # 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 + # ) + + # 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 + # ) + + # # 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=" + # f"{config}", + # timeout=10, + # ) + # elif self._vpnurl: + # requests.get( + # f"{self._vpnurl}/command/floodlight_set_config?config=" + # f"{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( + # cid=self._camera_id + # ) + # 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( + # cid=self._camera_id + # ) + # return None + # else: + # self.async_schedule_update_ha_state(True) + # else: + # _LOGGER.error("Unsupported camera model for light mode") From ba1960082f01d4ab9a65ce8afb110c33e162546a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 11 Dec 2019 23:35:39 +0100 Subject: [PATCH 14/58] Fix set schedule service --- homeassistant/components/netatmo/__init__.py | 3 +- .../components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 98 ++++++++---- homeassistant/components/netatmo/climate.py | 53 ++++--- homeassistant/components/netatmo/const.py | 5 + homeassistant/components/netatmo/light.py | 146 +++++++----------- homeassistant/components/netatmo/sensor.py | 51 +++--- .../components/netatmo/services.yaml | 10 ++ 8 files changed, 198 insertions(+), 170 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 46072bc70e7d60..1d314a8fda9c57 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,7 +1,7 @@ """The Netatmo integration.""" # TODO: -# * fix webhooks +# * fix/add webhooks import asyncio import logging @@ -37,7 +37,6 @@ extra=vol.ALLOW_EXTRA, ) -# PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor", "switch"] PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor"] diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index c6353d46a083bc..20f0d85505899f 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -48,7 +48,7 @@ def get_devices(): except NoDevice: _LOGGER.debug("No camera devices to add") - for camera_id in data.get_camera_ids(): + for camera_id in data.get_all_camera_ids(): camera_type = data.get_camera_type(camera_id=camera_id) home_id = data.get_camera_home_id(camera_id=camera_id) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index e31b8b0f62d8f9..2ea170f70bcc7c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -39,7 +39,7 @@ def get_devices(): devices = [] try: camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) - for camera_id in camera_data.get_camera_ids(): + for camera_id in camera_data.get_all_camera_ids(): _LOGGER.debug("Setting up camera %s", camera_id) camera_type = camera_data.get_camera_type(camera_id=camera_id) devices.append( @@ -68,9 +68,8 @@ def __init__(self, data, camera_id, camera_type, verify_ssl, quality): super().__init__() self._data = data self._camera_id = camera_id - self._name = ( - f"{MANUFAKTURER} {self._data.camera_data.cameraById(camera_id).get('name')}" - ) + self._camera_name = self._data.camera_data.get_camera(cid=camera_id).get("name") + self._name = f"{MANUFAKTURER} {self._camera_name}" self._camera_type = camera_type self._unique_id = f"{self._camera_id}-{self._camera_type}" self._verify_ssl = verify_ssl @@ -80,9 +79,6 @@ def __init__(self, data, camera_id, camera_type, verify_ssl, quality): self._vpnurl = None self._localurl = None - # Identifier - self._id = None - # Monitoring status self._status = None @@ -95,12 +91,6 @@ def __init__(self, data, camera_id, camera_type, verify_ssl, quality): # 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.""" try: @@ -117,14 +107,14 @@ def camera_image(self): else: _LOGGER.error("Welcome VPN URL is None") self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) return None except requests.exceptions.RequestException as error: _LOGGER.error("Welcome URL changed: %s", error) self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) return None @@ -150,7 +140,7 @@ def device_info(self): """Return the device info for the sensor.""" return { "identifiers": {(DOMAIN, self._camera_id)}, - "name": self._name, + "name": self._camera_name, "manufacturer": MANUFAKTURER, "model": self._camera_type, } @@ -159,15 +149,12 @@ def device_info(self): def device_state_attributes(self): """Return the Netatmo-specific camera state attributes.""" attr = {} - attr["id"] = self._id + attr["id"] = self._camera_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 + attr["vpn_url"] = self._vpnurl return attr @@ -228,10 +215,10 @@ def update(self): # Refresh camera data self._data.update() - camera = self._data.camera_data.cameraById(cid=self._camera_id) + camera = self._data.camera_data.get_camera(cid=self._camera_id) # URLs - self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( + self._vpnurl, self._localurl = self._data.camera_data.camera_urls( cid=self._camera_id ) @@ -247,16 +234,61 @@ def update(self): # Is local self._is_local = camera.get("is_local") - # VPN URL - self._vpn_url = camera.get("vpn_url") - self.is_streaming = self._alim_status == "on" - if self.model == "Presence": - # Light mode status - self._light_mode_status = camera.get("light_mode_status") + def turn_on(self): + """Instruct the light to turn on.""" + _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) + if self._set_mode("on"): + self.is_streaming = True + self.schedule_update_ha_state() + + def turn_off(self): + """Instruct the light to turn off.""" + _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) + if self._set_mode("off"): + self.is_streaming = False + self.schedule_update_ha_state() + + def _set_mode(self, mode: str): + """Set camera mode ('on', 'off').""" + try: + config = f'{{"mode":"{mode}"}}' + if self._localurl: + resp = requests.get( + f"{self._localurl}/command/changestatus?status=" f"{config}", + timeout=10, + ) + elif self._vpnurl: + resp = requests.get( + f"{self._vpnurl}/command/changestatus?status=" f"{config}", + timeout=10, + verify=self._verify_ssl, + ) + else: + _LOGGER.error("Camera VPN URL is None") + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( + cid=self._camera_id + ) + + if resp.status_code == 200: + return True + _LOGGER.debug( + "Turning camera %s %s failed (%s)", + self._camera_id, + mode, + resp.status_code, + ) + return None - # Camera method overrides + except requests.exceptions.RequestException as error: + _LOGGER.error("Camera URL changed: %s", error) + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( + cid=self._camera_id + ) + return None def enable_motion_detection(self): """Enable motion detection in the camera.""" @@ -287,14 +319,14 @@ def _enable_motion_detection(self, enable): else: _LOGGER.error("Welcome/Presence VPN URL is None") self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) 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( + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) return None @@ -322,7 +354,7 @@ def get_camera_home_id(self, camera_id): return home_id return None - def get_camera_ids(self): + def get_all_camera_ids(self): """Return all camera available on the API as a list.""" self.camera_ids = [] self.update() diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index c018beedb6f196..7e22a459b119a8 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging from typing import List, Optional +import voluptuous as vol import pyatmo import requests @@ -30,9 +31,17 @@ STATE_OFF, TEMP_CELSIUS, ) +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle -from .const import AUTH, DOMAIN, MANUFAKTURER +from .const import ( + ATTR_HOME_NAME, + ATTR_SCHEDULE_NAME, + AUTH, + DOMAIN, + MANUFAKTURER, + SERVICE_SETSCHEDULE, +) _LOGGER = logging.getLogger(__name__) @@ -91,21 +100,28 @@ NA_THERM = "NATherm1" NA_VALVE = "NRV" +SCHEMA_SERVICE_SETSCHEDULE = vol.Schema( + { + vol.Required(ATTR_SCHEDULE_NAME): cv.string, + vol.Required(ATTR_HOME_NAME): cv.string, + } +) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Netatmo energy platform.""" auth = hass.data[DOMAIN][AUTH] + home_data = HomeData(auth) + def get_devices(): """Retrieve Netatmo devices.""" devices = [] - - home_data = HomeData(auth) try: home_data.setup() except pyatmo.NoDevice: return - home_ids = home_data.get_home_ids() + home_ids = home_data.get_all_home_ids() for home_id in home_ids: _LOGGER.debug("Setting up home %s ...", home_id) @@ -121,19 +137,20 @@ def get_devices(): async_add_entities(await hass.async_add_executor_job(get_devices), True) - # def _service_setschedule(service): - # """Service to change current home schedule.""" - # schedule_name = service.data.get(ATTR_SCHEDULE) - # home_data.switchHomeSchedule(schedule=schedule_name) - # _LOGGER.info("Set home schedule to %s", schedule_name) - - # if home_data is not None: - # hass.services.register( - # DOMAIN, - # SERVICE_SETSCHEDULE, - # _service_setschedule, - # schema=SCHEMA_SERVICE_SETSCHEDULE, - # ) + def _service_setschedule(service): + """Service to change current home schedule.""" + home_name = service.data.get(ATTR_HOME_NAME) + schedule_name = service.data.get(ATTR_SCHEDULE_NAME) + home_data.homedata.switchHomeSchedule(schedule=schedule_name, home=home_name) + _LOGGER.info("Set home (%s) schedule to %s", home_name, schedule_name) + + if home_data.homedata is not None: + hass.services.async_register( + DOMAIN, + SERVICE_SETSCHEDULE, + _service_setschedule, + schema=SCHEMA_SERVICE_SETSCHEDULE, + ) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -364,7 +381,7 @@ def __init__(self, auth, home=None): self.home = home self.home_id = None - def get_home_ids(self): + def get_all_home_ids(self): """Get all the home ids returned by NetAtmo API.""" if self.homedata is None: return [] diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 2a2a4e767de2c4..60a89779ecffdf 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -41,12 +41,17 @@ ATTR_EVENT_TYPE = "event_type" ATTR_MESSAGE = "message" ATTR_CAMERA_ID = "camera_id" +ATTR_HOME_ID = "home_id" ATTR_HOME_NAME = "home_name" ATTR_PERSONS = "persons" ATTR_IS_KNOWN = "is_known" ATTR_FACE_URL = "face_url" ATTR_SNAPSHOT_URL = "snapshot_url" ATTR_VIGNETTE_URL = "vignette_url" +ATTR_SCHEDULE_ID = "schedule_id" +ATTR_SCHEDULE_NAME = "schedule_name" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) + +SERVICE_SETSCHEDULE = "set_schedule" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 2edc3a8c6afe71..829d8e4259d033 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -3,7 +3,7 @@ import pyatmo -# import requests +import requests from homeassistant.components.light import Light @@ -28,12 +28,11 @@ def get_devices(): devices = [] try: camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) - for camera_id in camera_data.get_camera_ids(): + for camera_id in camera_data.get_all_camera_ids(): camera_type = camera_data.get_camera_type(camera_id=camera_id) if camera_type == "NOC": _LOGGER.debug("Setting up camera %s", camera_id) devices.append(NetatmoLight(camera_id, camera_data)) - camera_data.get_persons() except pyatmo.NoDevice: _LOGGER.debug("No cameras found") return devices @@ -47,11 +46,15 @@ class NetatmoLight(Light): def __init__(self, camera_id: str, camera_data: CameraData): """Initialize a Netatmo Presence camera light.""" self._camera_id = camera_id - self._camera_data = camera_data - self._module_type = camera_data.camera_data[camera_id].get("type") - self._name = f"{MANUFAKTURER} {camera_data.camera_data[camera_id].get('name')}" + self._data = camera_data + self._camera_type = self._data.camera_data.cameraById(camera_id).get("type") + self._name = ( + f"{MANUFAKTURER} {self._data.camera_data.cameraById(camera_id).get('name')}" + ) self._is_on = False - self._unique_id = f"{self._camera_id}-{self._name}-light" + self._unique_id = f"{self._camera_id}-{self._camera_type}-light" + self._vpnurl = self._localurl = None + self._verify_ssl = True @property def name(self): @@ -65,7 +68,7 @@ def device_info(self): "identifiers": {(DOMAIN, self._camera_id)}, "name": self._name, "manufacturer": MANUFAKTURER, - "model": self._module_type, + "model": self._camera_type, } @property @@ -95,51 +98,59 @@ def is_on(self): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - # TODO + _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) + self._set_light_mode("on") self._is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Instruct the light to turn off.""" - # TODO + _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) + self._set_light_mode("off") self._is_on = False self.schedule_update_ha_state() - # 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 - # ) - - # 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 - # ) + # Netatmo Presence specific camera methods. + + def update(self): + """Update the camera data.""" + self._data.update() + (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + cid=self._camera_id + ) - # # Netatmo Presence specific camera method. + def _set_light_mode(self, mode: str): + """Set light mode ('auto', 'on', 'off').""" + try: + config = f'{{"mode":"{mode}"}}' + if self._localurl: + requests.get( + f"{self._localurl}/command/floodlight_set_config?config=" + f"{config}", + timeout=10, + ) + elif self._vpnurl: + requests.get( + f"{self._vpnurl}/command/floodlight_set_config?config=" f"{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( + cid=self._camera_id + ) + 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( + cid=self._camera_id + ) + return None + else: + self.async_schedule_update_ha_state(True) # def set_light_auto(self): # """Set flood light in automatic mode.""" @@ -147,50 +158,3 @@ def turn_off(self, **kwargs): # "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=" - # f"{config}", - # timeout=10, - # ) - # elif self._vpnurl: - # requests.get( - # f"{self._vpnurl}/command/floodlight_set_config?config=" - # f"{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( - # cid=self._camera_id - # ) - # 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( - # cid=self._camera_id - # ) - # 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/sensor.py b/homeassistant/components/netatmo/sensor.py index 6f8efbe0b67ca6..c9ca946178b159 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import AUTH, CONF_PUBLIC, DOMAIN, MANUFAKTURER +from .const import AUTH, DOMAIN, MANUFAKTURER _LOGGER = logging.getLogger(__name__) @@ -122,7 +122,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" auth = hass.data[DOMAIN][AUTH] - config_public = hass.data[DOMAIN][CONF_PUBLIC] + # config_public = hass.data[DOMAIN][CONF_PUBLIC] def find_devices(data): """Find all devices.""" @@ -136,28 +136,6 @@ def find_devices(data): entities.append(NetatmoSensor(data, module, condition.lower())) return entities - def setup_public(): - devices = [] - _LOGGER.debug("Adding public weather sensor %s", config_public) - - if config_public.get(CONF_AREAS) is not None: - for area in config_public[CONF_AREAS]: - _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) - data = NetatmoPublicData( - hass.data[DOMAIN][AUTH], - lat_ne=area[CONF_LAT_NE], - lon_ne=area[CONF_LON_NE], - lat_sw=area[CONF_LAT_SW], - lon_sw=area[CONF_LON_SW], - ) - for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: - devices.append( - NetatmoPublicSensor( - area[CONF_NAME], data, sensor_type, area[CONF_MODE] - ) - ) - return devices - def get_devices(): """Retrieve Netatmo devices.""" devices = [] @@ -178,7 +156,30 @@ def get_devices(): return devices async_add_entities(await hass.async_add_executor_job(get_devices), True) - async_add_entities(await hass.async_add_executor_job(setup_public), True) + + # def setup_public(): + # devices = [] + # _LOGGER.debug("Adding public weather sensor %s", config_public) + + # if config_public.get(CONF_AREAS) is not None: + # for area in config_public[CONF_AREAS]: + # _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) + # data = NetatmoPublicData( + # hass.data[DOMAIN][AUTH], + # lat_ne=area[CONF_LAT_NE], + # lon_ne=area[CONF_LON_NE], + # lat_sw=area[CONF_LAT_SW], + # lon_sw=area[CONF_LON_SW], + # ) + # for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: + # devices.append( + # NetatmoPublicSensor( + # area[CONF_NAME], data, sensor_type, area[CONF_MODE] + # ) + # ) + # return devices + + # async_add_entities(await hass.async_add_executor_job(setup_public), True) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 7fc9ac400ce9ee..6cbc1d5a5c0d8b 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -29,3 +29,13 @@ set_light_off: entity_id: description: Entity id. example: "camera.living_room" + +set_schedule: + description: Set the home heating schedule + fields: + schedule_name: + description: Schedule name + example: Standard + home_name: + description: Home name + example: MyHome \ No newline at end of file From e6d27d1b222c617a10ab5677dac0c04648aa4fb4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 11 Dec 2019 23:58:50 +0100 Subject: [PATCH 15/58] Clean up --- homeassistant/components/netatmo/climate.py | 4 --- .../components/netatmo/services.yaml | 32 +++---------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 7e22a459b119a8..1924e4bdd036d7 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,8 +1,4 @@ """Support for Netatmo Smart thermostats.""" - -# TODO -# * expose schedule switching - from datetime import timedelta import logging from typing import List, Optional diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index 6cbc1d5a5c0d8b..bc1e307d3a45df 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,41 +1,17 @@ # Describes the format for available Netatmo services -addwebhook: - description: Add webhook during runtime (e.g. if it has been banned). - fields: - url: - description: URL for which to add the webhook. - example: https://yourdomain.com:443/api/webhook/webhook_id - -dropwebhook: - 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" + example: "light.living_room" set_schedule: - description: Set the home heating schedule + description: Set the heating schedule. fields: schedule_name: - description: Schedule name + description: Schedule name. example: Standard home_name: - description: Home name + description: Home name. example: MyHome \ No newline at end of file From f2e5b94fb0d883828377b63ac66aba7a762b7f0a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 11:15:28 +0100 Subject: [PATCH 16/58] Remove unnecessary code --- homeassistant/components/netatmo/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 1d314a8fda9c57..bbd725dfd0bb69 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -66,12 +66,6 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Netatmo from a config entry.""" - # Backwards compat - if "auth_implementation" not in entry.data: - hass.config_entries.async_update_entry( - entry, data={**entry.data, "auth_implementation": DOMAIN} - ) - implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry ) From d5e057799d5dc85a0a8b096ffa84836399d92706 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 11:39:02 +0100 Subject: [PATCH 17/58] Add support for multiple entities/accounts --- homeassistant/components/netatmo/__init__.py | 16 +++++----------- .../components/netatmo/binary_sensor.py | 4 ++-- homeassistant/components/netatmo/camera.py | 4 ++-- homeassistant/components/netatmo/climate.py | 4 ++-- homeassistant/components/netatmo/light.py | 4 ++-- homeassistant/components/netatmo/sensor.py | 4 ++-- 6 files changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index bbd725dfd0bb69..bde8fca41f27fe 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -13,14 +13,7 @@ from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.config_entries import ConfigEntry -from .const import ( - AUTH, - CONF_PUBLIC, - DATA_PERSONS, - DOMAIN, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, -) +from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from . import api, config_flow _LOGGER = logging.getLogger(__name__) @@ -44,7 +37,6 @@ async def async_setup(hass: HomeAssistant, config: dict): """Set up the Netatmo component.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_PERSONS] = {} - hass.data[DOMAIN][CONF_PUBLIC] = config.get("sensor", {}) if DOMAIN not in config: return True @@ -70,7 +62,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass, entry ) - hass.data[DOMAIN][AUTH] = api.ConfigEntryNetatmoAuth(hass, entry, implementation) + hass.data[DOMAIN][entry.entry_id] = { + AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) + } for component in PLATFORMS: hass.async_create_task( @@ -91,6 +85,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) if unload_ok: - hass.data[DOMAIN].pop(AUTH) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 20f0d85505899f..760a368f5740fb 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -35,9 +35,9 @@ DEFAULT_TIMEOUT = 90 -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo energy platform.""" - auth = hass.data[DOMAIN][AUTH] + auth = hass.data[DOMAIN][entry.entry_id][AUTH] def get_devices(): """Retrieve Netatmo devices.""" diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 2ea170f70bcc7c..8f88bd2988711b 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -31,14 +31,14 @@ _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera platform.""" def get_devices(): """Retrieve Netatmo devices.""" devices = [] try: - camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) + camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) for camera_id in camera_data.get_all_camera_ids(): _LOGGER.debug("Setting up camera %s", camera_id) camera_type = camera_data.get_camera_type(camera_id=camera_id) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1924e4bdd036d7..99e5cecf440326 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -104,9 +104,9 @@ ) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo energy platform.""" - auth = hass.data[DOMAIN][AUTH] + auth = hass.data[DOMAIN][entry.entry_id][AUTH] home_data = HomeData(auth) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 829d8e4259d033..20ec8c351295ad 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -20,14 +20,14 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera light platform.""" def get_devices(): """Retrieve Netatmo devices.""" devices = [] try: - camera_data = CameraData(hass, hass.data[DOMAIN][AUTH]) + camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) for camera_id in camera_data.get_all_camera_ids(): camera_type = camera_data.get_camera_type(camera_id=camera_id) if camera_type == "NOC": diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index c9ca946178b159..e22fe1bc7c8532 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -119,9 +119,9 @@ } -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" - auth = hass.data[DOMAIN][AUTH] + auth = hass.data[DOMAIN][entry.entry_id][AUTH] # config_public = hass.data[DOMAIN][CONF_PUBLIC] def find_devices(data): From 04489499b2ea54f4bc24913d75cfbf65ba6de746 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 12:05:50 +0100 Subject: [PATCH 18/58] Fix MANUFACTURER typo --- .../components/netatmo/binary_sensor.py | 31 +++++++++++++------ homeassistant/components/netatmo/camera.py | 8 ++--- homeassistant/components/netatmo/climate.py | 6 ++-- homeassistant/components/netatmo/const.py | 2 +- homeassistant/components/netatmo/light.py | 13 ++------ homeassistant/components/netatmo/sensor.py | 10 +++--- 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 760a368f5740fb..f41133c0091cbf 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import AUTH, DOMAIN, MANUFAKTURER +from .const import AUTH, DOMAIN, MANUFACTURER from .camera import CameraData _LOGGER = logging.getLogger(__name__) @@ -85,11 +85,16 @@ def __init__(self, data, camera_id, home_id, sensor_type, module_id=None): self._home_name = self._data.camera_data.getHomeName(home_id=home_id) self._timeout = DEFAULT_TIMEOUT self._name = ( - f"{MANUFAKTURER}{self._camera_name} {self._module_name} {sensor_type}" + f"{MANUFACTURER} {self._camera_name} {self._module_name} {sensor_type}" if module_id is not None - else f"{MANUFAKTURER}{self._camera_name} {sensor_type}" + else f"{MANUFACTURER} {self._camera_name} {sensor_type}" ) self._state = None + self._unique_id = ( + f"{self._camera_id}-{self._module_name}-{self._camera_type}-{sensor_type}" + if module_id is not None + else f"{self._camera_id}-{self._camera_type}-{sensor_type}" + ) @property def name(self): @@ -97,13 +102,19 @@ def name(self): return self._name @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - if self._camera_type == "NACamera": - return WELCOME_SENSOR_TYPES.get(self._sensor_type) - if self._camera_type == "NOC": - return PRESENCE_SENSOR_TYPES.get(self._sensor_type) - return TAG_SENSOR_TYPES.get(self._sensor_type) + def unique_id(self): + """Return the unique ID for this sensor.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info for the sensor.""" + return { + "identifiers": {(DOMAIN, self._camera_id)}, + "name": self._camera_name, + "manufacturer": MANUFACTURER, + "model": self._camera_type, + } @property def is_on(self): diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 8f88bd2988711b..2e686af2fb71fc 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -11,7 +11,7 @@ from .const import ( AUTH, DOMAIN, - MANUFAKTURER, + MANUFACTURER, DATA_PERSONS, ATTR_PSEUDO, MIN_TIME_BETWEEN_UPDATES, @@ -69,7 +69,7 @@ def __init__(self, data, camera_id, camera_type, verify_ssl, quality): self._data = data self._camera_id = camera_id self._camera_name = self._data.camera_data.get_camera(cid=camera_id).get("name") - self._name = f"{MANUFAKTURER} {self._camera_name}" + self._name = f"{MANUFACTURER} {self._camera_name}" self._camera_type = camera_type self._unique_id = f"{self._camera_id}-{self._camera_type}" self._verify_ssl = verify_ssl @@ -141,7 +141,7 @@ def device_info(self): return { "identifiers": {(DOMAIN, self._camera_id)}, "name": self._camera_name, - "manufacturer": MANUFAKTURER, + "manufacturer": MANUFACTURER, "model": self._camera_type, } @@ -176,7 +176,7 @@ def is_recording(self): @property def brand(self): """Return the camera brand.""" - return MANUFAKTURER + return MANUFACTURER @property def motion_detection_enabled(self): diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 99e5cecf440326..b9a130a4ee9f5b 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -35,7 +35,7 @@ ATTR_SCHEDULE_NAME, AUTH, DOMAIN, - MANUFAKTURER, + MANUFACTURER, SERVICE_SETSCHEDULE, ) @@ -163,7 +163,7 @@ def __init__(self, data, room_id): self._state = None self._room_id = room_id self._room_name = self._data.homedata.rooms[self._data.home_id][room_id]["name"] - self._name = f"{MANUFAKTURER} {self._room_name}" + self._name = f"{MANUFACTURER} {self._room_name}" self._current_temperature = None self._target_temperature = None self._preset = None @@ -186,7 +186,7 @@ def device_info(self): return { "identifiers": {(DOMAIN, self._room_id)}, "name": self._room_name, - "manufacturer": MANUFAKTURER, + "manufacturer": MANUFACTURER, "model": self._module_type, } diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 60a89779ecffdf..5d981dc23b4586 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -4,7 +4,7 @@ API = "api" DOMAIN = "netatmo" -MANUFAKTURER = "Netatmo" +MANUFACTURER = "Netatmo" AUTH = "netatmo_auth" CONF_PUBLIC = "public_sensor_config" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 20ec8c351295ad..b818d94f0ed805 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -7,14 +7,7 @@ from homeassistant.components.light import Light -# from homeassistant.const import STATE_ON, STATE_OFF - -# from homeassistant.helpers.dispatcher import ( -# async_dispatcher_send, -# async_dispatcher_connect, -# ) - -from .const import AUTH, DOMAIN, MANUFAKTURER +from .const import AUTH, DOMAIN, MANUFACTURER from .camera import CameraData _LOGGER = logging.getLogger(__name__) @@ -49,7 +42,7 @@ def __init__(self, camera_id: str, camera_data: CameraData): self._data = camera_data self._camera_type = self._data.camera_data.cameraById(camera_id).get("type") self._name = ( - f"{MANUFAKTURER} {self._data.camera_data.cameraById(camera_id).get('name')}" + f"{MANUFACTURER} {self._data.camera_data.cameraById(camera_id).get('name')}" ) self._is_on = False self._unique_id = f"{self._camera_id}-{self._camera_type}-light" @@ -67,7 +60,7 @@ def device_info(self): return { "identifiers": {(DOMAIN, self._camera_id)}, "name": self._name, - "manufacturer": MANUFAKTURER, + "manufacturer": MANUFACTURER, "model": self._camera_type, } diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index e22fe1bc7c8532..ff45b4c6d92671 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import AUTH, DOMAIN, MANUFAKTURER +from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -230,7 +230,7 @@ def __init__(self, netatmo_data, module_info, sensor_type): f"{module_info['station_name']} {module_info['module_name']}" ) - self._name = f"{MANUFAKTURER} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" + self._name = f"{MANUFACTURER} {self.module_name} {SENSOR_TYPES[sensor_type][0]}" self.type = sensor_type self._state = None self._device_class = SENSOR_TYPES[self.type][3] @@ -261,7 +261,7 @@ def device_info(self): return { "identifiers": {(DOMAIN, self._module_id)}, "name": self.module_name, - "manufacturer": MANUFAKTURER, + "manufacturer": MANUFACTURER, "model": self._module_type, } @@ -462,7 +462,7 @@ def __init__(self, area_name, data, sensor_type, mode): self.netatmo_data = data self.type = sensor_type self._mode = mode - self._name = f"{MANUFAKTURER} {area_name} {SENSOR_TYPES[self.type][0]}" + self._name = f"{MANUFACTURER} {area_name} {SENSOR_TYPES[self.type][0]}" self._area_name = area_name self._state = None self._device_class = SENSOR_TYPES[self.type][3] @@ -490,7 +490,7 @@ def device_info(self): return { "identifiers": {(DOMAIN, self._area_name)}, "name": self._area_name, - "manufacturer": MANUFAKTURER, + "manufacturer": MANUFACTURER, "model": "public", } From 7e0f2b718d1902065696c42181c191461edfc99e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 12:14:22 +0100 Subject: [PATCH 19/58] Remove multiline inline if statement --- .../components/netatmo/binary_sensor.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index f41133c0091cbf..0dcdbdf6b8fe61 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -79,22 +79,22 @@ def __init__(self, data, camera_id, home_id, sensor_type, module_id=None): camera_info = data.camera_data.cameraById(cid=camera_id) self._camera_name = camera_info["name"] self._camera_type = camera_info["type"] - if module_id: - self._module_name = data.camera_data.moduleById(mid=module_id)["name"] self._home_id = home_id self._home_name = self._data.camera_data.getHomeName(home_id=home_id) self._timeout = DEFAULT_TIMEOUT - self._name = ( - f"{MANUFACTURER} {self._camera_name} {self._module_name} {sensor_type}" - if module_id is not None - else f"{MANUFACTURER} {self._camera_name} {sensor_type}" - ) + if module_id: + self._module_name = data.camera_data.moduleById(mid=module_id)["name"] + self._name = ( + f"{MANUFACTURER} {self._camera_name} {self._module_name} {sensor_type}" + ) + self._unique_id = ( + f"{self._camera_id}-{self._module_name}-" + f"{self._camera_type}-{sensor_type}" + ) + else: + self._name = f"{MANUFACTURER} {self._camera_name} {sensor_type}" + self._unique_id = f"{self._camera_id}-{self._camera_type}-{sensor_type}" self._state = None - self._unique_id = ( - f"{self._camera_id}-{self._module_name}-{self._camera_type}-{sensor_type}" - if module_id is not None - else f"{self._camera_id}-{self._camera_type}-{sensor_type}" - ) @property def name(self): From 10d30c7c2641ee2ada5f1fe8db51498ae50cc099 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 13:26:37 +0100 Subject: [PATCH 20/58] Only add tags when camera type is welcome --- homeassistant/components/netatmo/binary_sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 0dcdbdf6b8fe61..85a1776d076f66 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -52,7 +52,14 @@ def get_devices(): camera_type = data.get_camera_type(camera_id=camera_id) home_id = data.get_camera_home_id(camera_id=camera_id) - for sensor_name in SENSOR_TYPES[camera_type]: + sensor_types = {} + sensor_types.update(SENSOR_TYPES[camera_type]) + + # Tags are only supported with Netatmo Welcome indoor cameras + if camera_type == "NACamera": + sensor_types.update(TAG_SENSOR_TYPES) + + for sensor_name in sensor_types: devices.append( NetatmoBinarySensor(data, camera_id, home_id, sensor_name) ) From 750d17f4ece978b05754f7f815eb2d1cb537e949 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 13:26:54 +0100 Subject: [PATCH 21/58] Remove on/off as it's currently broken --- homeassistant/components/netatmo/camera.py | 54 ---------------------- 1 file changed, 54 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 2e686af2fb71fc..2514a344d43982 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -236,60 +236,6 @@ def update(self): self.is_streaming = self._alim_status == "on" - def turn_on(self): - """Instruct the light to turn on.""" - _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) - if self._set_mode("on"): - self.is_streaming = True - self.schedule_update_ha_state() - - def turn_off(self): - """Instruct the light to turn off.""" - _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) - if self._set_mode("off"): - self.is_streaming = False - self.schedule_update_ha_state() - - def _set_mode(self, mode: str): - """Set camera mode ('on', 'off').""" - try: - config = f'{{"mode":"{mode}"}}' - if self._localurl: - resp = requests.get( - f"{self._localurl}/command/changestatus?status=" f"{config}", - timeout=10, - ) - elif self._vpnurl: - resp = requests.get( - f"{self._vpnurl}/command/changestatus?status=" f"{config}", - timeout=10, - verify=self._verify_ssl, - ) - else: - _LOGGER.error("Camera VPN URL is None") - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( - cid=self._camera_id - ) - - if resp.status_code == 200: - return True - _LOGGER.debug( - "Turning camera %s %s failed (%s)", - self._camera_id, - mode, - resp.status_code, - ) - return None - - except requests.exceptions.RequestException as error: - _LOGGER.error("Camera URL changed: %s", error) - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( - cid=self._camera_id - ) - return None - def enable_motion_detection(self): """Enable motion detection in the camera.""" _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) From 2038846c656aa73f6b386ce46fb4a5728b2af71a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 14:03:44 +0100 Subject: [PATCH 22/58] Fix camera turn_on/off --- homeassistant/components/netatmo/camera.py | 43 +++++++--------------- 1 file changed, 13 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 2514a344d43982..571a7e3adafab2 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -236,46 +236,29 @@ def update(self): self.is_streaming = self._alim_status == "on" - def enable_motion_detection(self): + def turn_on(self): """Enable motion detection in the camera.""" _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) - self._enable_motion_detection(True) + self._toggle_camera_operation(True) - def disable_motion_detection(self): + def turn_off(self): """Disable motion detection in camera.""" _LOGGER.debug("Disable motion detection of the camera '%s'", self._name) - self._enable_motion_detection(False) + self._toggle_camera_operation(False) - def _enable_motion_detection(self, enable): + def _toggle_camera_operation(self, enable): """Enable or disable motion detection.""" try: - if self._localurl: - requests.get( - f"{self._localurl}/command/changestatus?status=" - f"{_BOOL_TO_STATE.get(enable)}", - timeout=10, - ) - elif self._vpnurl: - requests.get( - f"{self._vpnurl}/command/changestatus?status=" - f"{_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.camera_urls( - cid=self._camera_id - ) - return None + camera_url = self._localurl if self._localurl else self._vpnurl + + requests.get( + url=f"{camera_url}/command/changestatus", + params={"status": _BOOL_TO_STATE.get(enable)}, + timeout=10, + verify=self._verify_ssl, + ) 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.camera_urls( - cid=self._camera_id - ) - return None else: self.async_schedule_update_ha_state(True) From 756f31faea1ce9d5b07926f43d6e2436dacf68fc Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 14:07:53 +0100 Subject: [PATCH 23/58] Fix debug message --- homeassistant/components/netatmo/camera.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 571a7e3adafab2..594e222967d50d 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -238,16 +238,16 @@ def update(self): def turn_on(self): """Enable motion detection in the camera.""" - _LOGGER.debug("Enable motion detection of the camera '%s'", self._name) + _LOGGER.debug("Turn the camera '%s' on", self._name) self._toggle_camera_operation(True) def turn_off(self): """Disable motion detection in camera.""" - _LOGGER.debug("Disable motion detection of the camera '%s'", self._name) + _LOGGER.debug("Turn the camera '%s' off", self._name) self._toggle_camera_operation(False) def _toggle_camera_operation(self, enable): - """Enable or disable motion detection.""" + """Enable or disable the camera.""" try: camera_url = self._localurl if self._localurl else self._vpnurl @@ -288,8 +288,7 @@ def get_all_camera_ids(self): self.camera_ids = [] self.update() for home_id in self.camera_data.cameras: - for camera in self.camera_data.cameras[home_id].values(): - self.camera_ids.append(camera["id"]) + self.camera_ids.extend(self.camera_data.cameras[home_id]) return self.camera_ids def get_module_names(self, camera_id): From cc182035e60ad191efe9dc3bc392f84302393f99 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 15:36:34 +0100 Subject: [PATCH 24/58] Refactor some camera code --- .../components/netatmo/binary_sensor.py | 12 +++++-- homeassistant/components/netatmo/camera.py | 32 ++++++------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 85a1776d076f66..0e6fa003c48967 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -48,9 +48,17 @@ def get_devices(): except NoDevice: _LOGGER.debug("No camera devices to add") + def get_camera_home_id(data, camera_id): + """Return the home id for a given camera id.""" + for home_id in data.camera_data.cameras: + for camera in data.camera_data.cameras[home_id].values(): + if camera["id"] == camera_id: + return home_id + return None + for camera_id in data.get_all_camera_ids(): camera_type = data.get_camera_type(camera_id=camera_id) - home_id = data.get_camera_home_id(camera_id=camera_id) + home_id = get_camera_home_id(data, camera_id=camera_id) sensor_types = {} sensor_types.update(SENSOR_TYPES[camera_type]) @@ -131,7 +139,7 @@ def is_on(self): def update(self): """Request an update from the Netatmo API.""" self._data.update() - self._data.update_event() + self._data.update_event(camera_type=self._camera_type) if self._camera_type == "NACamera": if self._sensor_type == "Someone known": diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 594e222967d50d..a9a7ab94796226 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -271,39 +271,27 @@ def __init__(self, hass, auth): self._hass = hass self.auth = auth self.camera_data = None - self.camera_ids = [] - self.module_names = [] - self.camera_type = None - - def get_camera_home_id(self, camera_id): - """Return the home id for a given camera id.""" - for home_id in self.camera_data.cameras: - for camera in self.camera_data.cameras[home_id].values(): - if camera["id"] == camera_id: - return home_id - return None def get_all_camera_ids(self): """Return all camera available on the API as a list.""" - self.camera_ids = [] self.update() + camera_ids = [] for home_id in self.camera_data.cameras: - self.camera_ids.extend(self.camera_data.cameras[home_id]) - return self.camera_ids + camera_ids.extend(self.camera_data.cameras[home_id]) + return camera_ids def get_module_names(self, camera_id): """Return all module available on the API as a list.""" - self.module_names = [] + module_names = [] self.update() for module in self.camera_data.modules.values(): if camera_id == module["cam_id"]: - self.module_names.append(module["name"]) - return self.module_names + module_names.append(module["name"]) + return module_names - def get_camera_type(self, camera_id=None): + def get_camera_type(self, camera_id): """Return camera type for a camera, cid has preference over camera.""" - self.camera_type = self.camera_data.cameraType(cid=camera_id) - return self.camera_type + return self.camera_data.cameraType(cid=camera_id) def get_persons(self): """Gather person data for webhooks.""" @@ -318,6 +306,6 @@ def update(self): self.camera_data = pyatmo.CameraData(self.auth, size=100) @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) - def update_event(self): + def update_event(self, camera_type): """Call the Netatmo API to update the events.""" - self.camera_data.updateEvent(devicetype=self.camera_type) + self.camera_data.updateEvent(devicetype=camera_type) From 5a40241e98f98b52e1ad4c5abb948b1cb9673a9e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 15:43:56 +0100 Subject: [PATCH 25/58] Refactor camera methods --- homeassistant/components/netatmo/binary_sensor.py | 11 +++++------ homeassistant/components/netatmo/camera.py | 15 +++++++-------- homeassistant/components/netatmo/light.py | 11 +++++------ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 0e6fa003c48967..8e75a8ee0d233d 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -56,20 +56,19 @@ def get_camera_home_id(data, camera_id): return home_id return None - for camera_id in data.get_all_camera_ids(): - camera_type = data.get_camera_type(camera_id=camera_id) - home_id = get_camera_home_id(data, camera_id=camera_id) + for camera in data.get_all_cameras(): + home_id = get_camera_home_id(data, camera_id=camera["id"]) sensor_types = {} - sensor_types.update(SENSOR_TYPES[camera_type]) + sensor_types.update(SENSOR_TYPES[camera["type"]]) # Tags are only supported with Netatmo Welcome indoor cameras - if camera_type == "NACamera": + if camera["type"] == "NACamera": sensor_types.update(TAG_SENSOR_TYPES) for sensor_name in sensor_types: devices.append( - NetatmoBinarySensor(data, camera_id, home_id, sensor_name) + NetatmoBinarySensor(data, camera["id"], home_id, sensor_name) ) return devices diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index a9a7ab94796226..5f017cd348f6f9 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -39,12 +39,11 @@ def get_devices(): devices = [] try: camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) - for camera_id in camera_data.get_all_camera_ids(): - _LOGGER.debug("Setting up camera %s", camera_id) - camera_type = camera_data.get_camera_type(camera_id=camera_id) + for camera in camera_data.get_all_cameras(): + _LOGGER.debug("Setting up camera %s", camera["id"]) devices.append( NetatmoCamera( - camera_data, camera_id, camera_type, True, DEFAULT_QUALITY + camera_data, camera["id"], camera["type"], True, DEFAULT_QUALITY ) ) camera_data.get_persons() @@ -272,13 +271,13 @@ def __init__(self, hass, auth): self.auth = auth self.camera_data = None - def get_all_camera_ids(self): + def get_all_cameras(self): """Return all camera available on the API as a list.""" self.update() - camera_ids = [] + cameras = [] for home_id in self.camera_data.cameras: - camera_ids.extend(self.camera_data.cameras[home_id]) - return camera_ids + cameras.extend(self.camera_data.cameras[home_id].values()) + return cameras def get_module_names(self, camera_id): """Return all module available on the API as a list.""" diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index b818d94f0ed805..5b9c8948c049fe 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -21,11 +21,10 @@ def get_devices(): devices = [] try: camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) - for camera_id in camera_data.get_all_camera_ids(): - camera_type = camera_data.get_camera_type(camera_id=camera_id) - if camera_type == "NOC": - _LOGGER.debug("Setting up camera %s", camera_id) - devices.append(NetatmoLight(camera_id, camera_data)) + for camera in camera_data.get_all_cameras(): + if camera["type"] == "NOC": + _LOGGER.debug("Setting up camera %s", camera["id"]) + devices.append(NetatmoLight(camera_data, camera["id"])) except pyatmo.NoDevice: _LOGGER.debug("No cameras found") return devices @@ -36,7 +35,7 @@ def get_devices(): class NetatmoLight(Light): """Representation of a Netatmo Presence camera light.""" - def __init__(self, camera_id: str, camera_data: CameraData): + def __init__(self, camera_data: CameraData, camera_id: str): """Initialize a Netatmo Presence camera light.""" self._camera_id = camera_id self._data = camera_data From 622eedb3882c581bfd17727703332784a37eafc8 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 15:45:44 +0100 Subject: [PATCH 26/58] Remove old code --- homeassistant/components/netatmo/camera.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 5f017cd348f6f9..17fed8a48bb770 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -279,15 +279,6 @@ def get_all_cameras(self): cameras.extend(self.camera_data.cameras[home_id].values()) return cameras - def get_module_names(self, camera_id): - """Return all module available on the API as a list.""" - module_names = [] - self.update() - for module in self.camera_data.modules.values(): - if camera_id == module["cam_id"]: - module_names.append(module["name"]) - return module_names - def get_camera_type(self, camera_id): """Return camera type for a camera, cid has preference over camera.""" return self.camera_data.cameraType(cid=camera_id) From 1b59e19fb66515b7975dddfe8ab2baed46621e3e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 15:46:58 +0100 Subject: [PATCH 27/58] Rename method --- homeassistant/components/netatmo/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 17fed8a48bb770..a08d15bf4058cb 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -46,7 +46,7 @@ def get_devices(): camera_data, camera["id"], camera["type"], True, DEFAULT_QUALITY ) ) - camera_data.get_persons() + camera_data.update_persons() except pyatmo.NoDevice: _LOGGER.debug("No cameras found") return devices @@ -283,7 +283,7 @@ def get_camera_type(self, camera_id): """Return camera type for a camera, cid has preference over camera.""" return self.camera_data.cameraType(cid=camera_id) - def get_persons(self): + def update_persons(self): """Gather person data for webhooks.""" for person_id, person_data in self.camera_data.persons.items(): self._hass.data[DOMAIN][DATA_PERSONS][person_id] = person_data.get( From c2536fab540e7320c043da83d81a47da58954fba Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 15:48:46 +0100 Subject: [PATCH 28/58] Update persons regularly --- homeassistant/components/netatmo/camera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index a08d15bf4058cb..25dca9051d036f 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -294,6 +294,7 @@ def update_persons(self): def update(self): """Call the Netatmo API to update the data.""" self.camera_data = pyatmo.CameraData(self.auth, size=100) + self.update_persons() @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) def update_event(self, camera_type): From e80e457ecd6319796822b8dd121b150d235d8997 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 15:54:41 +0100 Subject: [PATCH 29/58] Remove unused code --- homeassistant/components/netatmo/climate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index b9a130a4ee9f5b..a681707fef779f 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -453,8 +453,6 @@ def update(self): except requests.exceptions.Timeout: _LOGGER.warning("Timed out when connecting to Netatmo server") return - # _LOGGER.debug("Following is the debugging output for homestatus:") - # _LOGGER.debug(self.homestatus.rawData) for room in self.homestatus.rooms: try: roomstatus = {} From f3c743c231b64fac6cebf8e2f4d6c63ca9a2ed3e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 16:06:38 +0100 Subject: [PATCH 30/58] Refactor method --- homeassistant/components/netatmo/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 25dca9051d036f..ab00a08d4caa96 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -275,8 +275,8 @@ def get_all_cameras(self): """Return all camera available on the API as a list.""" self.update() cameras = [] - for home_id in self.camera_data.cameras: - cameras.extend(self.camera_data.cameras[home_id].values()) + for camera in self.camera_data.cameras.values(): + cameras.extend(camera.values()) return cameras def get_camera_type(self, camera_id): From 7f721772f39c75920742c4658e773e0fcbcb511e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 12 Dec 2019 16:51:56 +0100 Subject: [PATCH 31/58] Fix isort --- homeassistant/components/netatmo/__init__.py | 8 +++--- homeassistant/components/netatmo/api.py | 2 +- .../components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 10 +++---- homeassistant/components/netatmo/climate.py | 2 +- homeassistant/components/netatmo/light.py | 3 +- homeassistant/components/netatmo/sensor.py | 28 +------------------ tests/components/netatmo/test_config_flow.py | 2 +- 8 files changed, 15 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index bde8fca41f27fe..8b041233a3857f 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -8,13 +8,13 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv -from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from . import api, config_flow +from .const import AUTH, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 2b49d038c3852e..20924dbab00402 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -4,7 +4,7 @@ import pyatmo -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 8e75a8ee0d233d..6dfba7d2382a6e 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -5,8 +5,8 @@ from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import AUTH, DOMAIN, MANUFACTURER from .camera import CameraData +from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ab00a08d4caa96..a5c6cb4fde3c23 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -4,18 +4,18 @@ import pyatmo import requests -from homeassistant.components.camera import Camera, SUPPORT_STREAM -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util import Throttle from .const import ( + ATTR_PSEUDO, AUTH, + DATA_PERSONS, DOMAIN, MANUFACTURER, - DATA_PERSONS, - ATTR_PSEUDO, - MIN_TIME_BETWEEN_UPDATES, MIN_TIME_BETWEEN_EVENT_UPDATES, + MIN_TIME_BETWEEN_UPDATES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index a681707fef779f..407280769b1b01 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,10 +2,10 @@ from datetime import timedelta import logging from typing import List, Optional -import voluptuous as vol import pyatmo import requests +import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 5b9c8948c049fe..fc90adb63f5c3b 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -2,13 +2,12 @@ import logging import pyatmo - import requests from homeassistant.components.light import Light -from .const import AUTH, DOMAIN, MANUFACTURER from .camera import CameraData +from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index ff45b4c6d92671..3cdef8805e337a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,13 +1,10 @@ """Support for the Netatmo Weather Service.""" from datetime import timedelta import logging - import threading from time import time import pyatmo - -# import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -22,6 +19,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle + from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -187,30 +185,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return -# def get_devices(): -# devices = [] - -# if config.get(CONF_AREAS) is not None: -# for area in config[CONF_AREAS]: -# _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) -# data = NetatmoPublicData( -# hass.data[DOMAIN][AUTH], -# lat_ne=area[CONF_LAT_NE], -# lon_ne=area[CONF_LON_NE], -# lat_sw=area[CONF_LAT_SW], -# lon_sw=area[CONF_LON_SW], -# ) -# for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: -# devices.append( -# NetatmoPublicSensor( -# area[CONF_NAME], data, sensor_type, area[CONF_MODE] -# ) -# ) -# return devices - -# async_add_entities(await hass.async_add_executor_job(get_devices), True) - - class NetatmoSensor(Entity): """Implementation of a Netatmo sensor.""" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index a2e0b4f515d788..51cf75444c66f2 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Netatmo config flow.""" -from homeassistant import config_entries, setup, data_entry_flow +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.netatmo.const import ( DOMAIN, OAUTH2_AUTHORIZE, From 4216de98427896ba8172e0d9e2f9453a61cf35a2 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 13 Dec 2019 11:07:01 +0100 Subject: [PATCH 32/58] Add english strings --- .../components/netatmo/.translations/en.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 homeassistant/components/netatmo/.translations/en.json diff --git a/homeassistant/components/netatmo/.translations/en.json b/homeassistant/components/netatmo/.translations/en.json new file mode 100644 index 00000000000000..8cd4f51aee2660 --- /dev/null +++ b/homeassistant/components/netatmo/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Netatmo", + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "already_setup": "You can only configure one Netatmo account.", + "authorize_url_timeout": "Timeout generating authorize url.", + "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Netatmo." + } + } +} \ No newline at end of file From c560a74980c679cbfcb8ec92407843b0bd02dac6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 13 Dec 2019 11:24:28 +0100 Subject: [PATCH 33/58] Catch NoDevice exception --- .../components/netatmo/binary_sensor.py | 34 +++++++++---------- homeassistant/components/netatmo/light.py | 6 +++- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 6dfba7d2382a6e..ebcb8be7e52c15 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Netatmo binary sensors.""" import logging -from pyatmo import NoDevice +import pyatmo from homeassistant.components.binary_sensor import BinarySensorDevice @@ -43,11 +43,6 @@ def get_devices(): """Retrieve Netatmo devices.""" devices = [] - try: - data = CameraData(hass, auth) - except NoDevice: - _LOGGER.debug("No camera devices to add") - def get_camera_home_id(data, camera_id): """Return the home id for a given camera id.""" for home_id in data.camera_data.cameras: @@ -56,20 +51,25 @@ def get_camera_home_id(data, camera_id): return home_id return None - for camera in data.get_all_cameras(): - home_id = get_camera_home_id(data, camera_id=camera["id"]) + try: + data = CameraData(hass, auth) - sensor_types = {} - sensor_types.update(SENSOR_TYPES[camera["type"]]) + for camera in data.get_all_cameras(): + home_id = get_camera_home_id(data, camera_id=camera["id"]) - # Tags are only supported with Netatmo Welcome indoor cameras - if camera["type"] == "NACamera": - sensor_types.update(TAG_SENSOR_TYPES) + sensor_types = {} + sensor_types.update(SENSOR_TYPES[camera["type"]]) - for sensor_name in sensor_types: - devices.append( - NetatmoBinarySensor(data, camera["id"], home_id, sensor_name) - ) + # Tags are only supported with Netatmo Welcome indoor cameras + if camera["type"] == "NACamera": + sensor_types.update(TAG_SENSOR_TYPES) + + for sensor_name in sensor_types: + devices.append( + NetatmoBinarySensor(data, camera["id"], home_id, sensor_name) + ) + except pyatmo.NoDevice: + _LOGGER.debug("No camera devices to add") return devices diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index fc90adb63f5c3b..84ab2f8de05134 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -14,18 +14,22 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera light platform.""" + auth = hass.data[DOMAIN][entry.entry_id][AUTH] def get_devices(): """Retrieve Netatmo devices.""" devices = [] + try: - camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) + camera_data = CameraData(hass, auth) + for camera in camera_data.get_all_cameras(): if camera["type"] == "NOC": _LOGGER.debug("Setting up camera %s", camera["id"]) devices.append(NetatmoLight(camera_data, camera["id"])) except pyatmo.NoDevice: _LOGGER.debug("No cameras found") + return devices async_add_entities(await hass.async_add_executor_job(get_devices), True) From 159be981debf6c9d87bc1b0c42504b08aa897385 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 13 Dec 2019 12:13:36 +0100 Subject: [PATCH 34/58] Fix unique id and only add sensors for tags if present --- homeassistant/components/netatmo/binary_sensor.py | 4 ++-- homeassistant/components/netatmo/camera.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index ebcb8be7e52c15..423d04e9850783 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -61,7 +61,7 @@ def get_camera_home_id(data, camera_id): sensor_types.update(SENSOR_TYPES[camera["type"]]) # Tags are only supported with Netatmo Welcome indoor cameras - if camera["type"] == "NACamera": + if camera["type"] == "NACamera" and data.get_modules(camera["id"]): sensor_types.update(TAG_SENSOR_TYPES) for sensor_name in sensor_types: @@ -102,7 +102,7 @@ def __init__(self, data, camera_id, home_id, sensor_type, module_id=None): f"{MANUFACTURER} {self._camera_name} {self._module_name} {sensor_type}" ) self._unique_id = ( - f"{self._camera_id}-{self._module_name}-" + f"{self._camera_id}-{self._module_id}-" f"{self._camera_type}-{sensor_type}" ) else: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index a5c6cb4fde3c23..d2efe4a6594b05 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -279,6 +279,13 @@ def get_all_cameras(self): cameras.extend(camera.values()) return cameras + def get_modules(self, camera_id): + """Return all modules for a given camera.""" + return [ + module + for module in self.camera_data.get_camera(camera_id).get("modules", []) + ] + def get_camera_type(self, camera_id): """Return camera type for a camera, cid has preference over camera.""" return self.camera_data.cameraType(cid=camera_id) From 85f767ef35caefd3ae9b4c808a67c0221884205d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 13 Dec 2019 13:20:43 +0100 Subject: [PATCH 35/58] Address comments --- homeassistant/components/netatmo/api.py | 1 - .../components/netatmo/binary_sensor.py | 18 +++++++++--------- homeassistant/components/netatmo/camera.py | 12 ++++++------ homeassistant/components/netatmo/light.py | 12 ++++++------ 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py index 20924dbab00402..9a34888fd72b6e 100644 --- a/homeassistant/components/netatmo/api.py +++ b/homeassistant/components/netatmo/api.py @@ -21,7 +21,6 @@ def __init__( ): """Initialize Netatmo Auth.""" self.hass = hass - self.config_entry = config_entry self.session = config_entry_oauth2_flow.OAuth2Session( hass, config_entry, implementation ) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 423d04e9850783..0ed5243e6736bc 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -39,9 +39,9 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo energy platform.""" auth = hass.data[DOMAIN][entry.entry_id][AUTH] - def get_devices(): - """Retrieve Netatmo devices.""" - devices = [] + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] def get_camera_home_id(data, camera_id): """Return the home id for a given camera id.""" @@ -65,20 +65,20 @@ def get_camera_home_id(data, camera_id): sensor_types.update(TAG_SENSOR_TYPES) for sensor_name in sensor_types: - devices.append( + entities.append( NetatmoBinarySensor(data, camera["id"], home_id, sensor_name) ) except pyatmo.NoDevice: - _LOGGER.debug("No camera devices to add") + _LOGGER.debug("No camera entities to add") - return devices + return entities - async_add_entities(await hass.async_add_executor_job(get_devices), True) + async_add_entities(await hass.async_add_executor_job(get_entities), True) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the access to Netatmo binary sensor.""" - return + pass class NetatmoBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index d2efe4a6594b05..3bd8af795d0bd9 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -34,14 +34,14 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera platform.""" - def get_devices(): - """Retrieve Netatmo devices.""" - devices = [] + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] try: camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) for camera in camera_data.get_all_cameras(): _LOGGER.debug("Setting up camera %s", camera["id"]) - devices.append( + entities.append( NetatmoCamera( camera_data, camera["id"], camera["type"], True, DEFAULT_QUALITY ) @@ -49,9 +49,9 @@ def get_devices(): camera_data.update_persons() except pyatmo.NoDevice: _LOGGER.debug("No cameras found") - return devices + return entities - async_add_entities(await hass.async_add_executor_job(get_devices), True) + async_add_entities(await hass.async_add_executor_job(get_entities), True) def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 84ab2f8de05134..ed91894f9507a4 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -16,9 +16,9 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera light platform.""" auth = hass.data[DOMAIN][entry.entry_id][AUTH] - def get_devices(): - """Retrieve Netatmo devices.""" - devices = [] + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] try: camera_data = CameraData(hass, auth) @@ -26,13 +26,13 @@ def get_devices(): for camera in camera_data.get_all_cameras(): if camera["type"] == "NOC": _LOGGER.debug("Setting up camera %s", camera["id"]) - devices.append(NetatmoLight(camera_data, camera["id"])) + entities.append(NetatmoLight(camera_data, camera["id"])) except pyatmo.NoDevice: _LOGGER.debug("No cameras found") - return devices + return entities - async_add_entities(await hass.async_add_executor_job(get_devices), True) + async_add_entities(await hass.async_add_executor_job(get_entities), True) class NetatmoLight(Light): From fe412859242af7e11b395aad565c97f0ebe0c149 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 13 Dec 2019 13:44:32 +0100 Subject: [PATCH 36/58] Remove ToDo comment --- homeassistant/components/netatmo/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 8b041233a3857f..e2d9cc8adcd0cc 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,8 +1,4 @@ """The Netatmo integration.""" - -# TODO: -# * fix/add webhooks - import asyncio import logging From e37f2e01d8693ddb7b631ca118a33eb3e93b03b7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Tue, 17 Dec 2019 22:45:11 +0100 Subject: [PATCH 37/58] Add set_light_auto back in --- homeassistant/components/netatmo/camera.py | 4 ++-- homeassistant/components/netatmo/climate.py | 2 +- homeassistant/components/netatmo/light.py | 23 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 3bd8af795d0bd9..9a36488b4d53b5 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -104,14 +104,14 @@ def camera_image(self): verify=self._verify_ssl, ) else: - _LOGGER.error("Welcome VPN URL is None") + _LOGGER.error("Welcome/Presence VPN URL is None") self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) return None except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome URL changed: %s", error) + _LOGGER.info("Welcome/Presence URL changed: %s", error) self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 407280769b1b01..7f57120bfd06de 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -357,7 +357,7 @@ def update(self): except KeyError as err: _LOGGER.error( "The thermostat in room %s seems to be out of reach. (%s)", - self._room_id, + self._room_name, err, ) self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index ed91894f9507a4..1fea82b2534625 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -3,14 +3,22 @@ import pyatmo import requests +import voluptuous as vol from homeassistant.components.light import Light +from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from .camera import CameraData from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) +SCHEMA_SERVICE_SETLIGHTAUTO = vol.Schema( + {vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids} +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera light platform.""" @@ -34,6 +42,21 @@ def get_entities(): async_add_entities(await hass.async_add_executor_job(get_entities), True) + 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, SCHEMA_SERVICE_SETLIGHTAUTO + ) + class NetatmoLight(Light): """Representation of a Netatmo Presence camera light.""" From cb5068bc4241d1dc786a820a668257cc95219d05 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 18 Dec 2019 00:35:52 +0100 Subject: [PATCH 38/58] Add debug info --- homeassistant/components/netatmo/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 9a36488b4d53b5..f96cd1e36c81e8 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -40,7 +40,7 @@ def get_entities(): try: camera_data = CameraData(hass, hass.data[DOMAIN][entry.entry_id][AUTH]) for camera in camera_data.get_all_cameras(): - _LOGGER.debug("Setting up camera %s", camera["id"]) + _LOGGER.debug("Setting up camera %s %s", camera["id"], camera["name"]) entities.append( NetatmoCamera( camera_data, camera["id"], camera["type"], True, DEFAULT_QUALITY From 21eb950fceb3b33c8c83bb0e793b60bb41bf2f05 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 18 Dec 2019 00:41:00 +0100 Subject: [PATCH 39/58] Fix multiple camera issue --- homeassistant/components/netatmo/light.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 1fea82b2534625..1c82cd98dee271 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -133,7 +133,7 @@ def turn_off(self, **kwargs): def update(self): """Update the camera data.""" self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.cameraUrls( + (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) @@ -143,13 +143,14 @@ def _set_light_mode(self, mode: str): config = f'{{"mode":"{mode}"}}' if self._localurl: requests.get( - f"{self._localurl}/command/floodlight_set_config?config=" - f"{config}", + f"{self._localurl}/command/floodlight_set_config", + params={"config": config}, timeout=10, ) elif self._vpnurl: requests.get( - f"{self._vpnurl}/command/floodlight_set_config?config=" f"{config}", + f"{self._vpnurl}/command/floodlight_set_config", + params={"config": config}, timeout=10, verify=self._verify_ssl, ) From 90fbf8f93f580b3ec5ddb55b15d41b80ae69c82a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 18 Dec 2019 13:40:18 +0100 Subject: [PATCH 40/58] Move camera light service to camera --- homeassistant/components/netatmo/camera.py | 68 ++++++++++++++-- homeassistant/components/netatmo/const.py | 3 + homeassistant/components/netatmo/light.py | 94 +++++++--------------- 3 files changed, 94 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index f96cd1e36c81e8..15c7e19f28adfe 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -3,14 +3,22 @@ import pyatmo import requests +import voluptuous as vol from homeassistant.components.camera import SUPPORT_STREAM, Camera -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.util import Throttle from .const import ( ATTR_PSEUDO, AUTH, + CMD_CAMERA_LIGHT_URL, + CMD_CAMERA_STATUS_URL, DATA_PERSONS, DOMAIN, MANUFACTURER, @@ -30,6 +38,10 @@ _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} +SCHEMA_SERVICE_SETLIGHTAUTO = vol.Schema( + {vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids} +) + async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera platform.""" @@ -53,6 +65,21 @@ def get_entities(): async_add_entities(await hass.async_add_executor_job(get_entities), True) + 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, SCHEMA_SERVICE_SETLIGHTAUTO + ) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up access to Netatmo cameras.""" @@ -119,8 +146,6 @@ def camera_image(self): return None return response.content - # Entity property overrides - @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -249,10 +274,36 @@ def _toggle_camera_operation(self, enable): """Enable or disable the camera.""" try: camera_url = self._localurl if self._localurl else self._vpnurl + params = {"status": _BOOL_TO_STATE.get(enable)} + + requests.get( + url=f"{camera_url}{CMD_CAMERA_STATUS_URL}", + params=params, + timeout=10, + verify=self._verify_ssl, + ) + except requests.exceptions.RequestException as error: + _LOGGER.error("Welcome/Presence URL changed: %s", error) + else: + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Subscribe to signals and add camera to list.""" + if self._camera_type == "NOC": + _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 + ) + + def _set_light_mode(self, mode: str): + """Set light mode ('auto', 'on', 'off').""" + try: + camera_url = self._localurl if self._localurl else self._vpnurl + params = {"config": f'{{"mode":"{mode}"}}'} requests.get( - url=f"{camera_url}/command/changestatus", - params={"status": _BOOL_TO_STATE.get(enable)}, + url=f"{camera_url}{CMD_CAMERA_LIGHT_URL}", + params=params, timeout=10, verify=self._verify_ssl, ) @@ -261,6 +312,13 @@ def _toggle_camera_operation(self, enable): else: self.async_schedule_update_ha_state(True) + 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") + class CameraData: """Get the latest data from Netatmo.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 5d981dc23b4586..467004deec708f 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -14,6 +14,9 @@ OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" +CMD_CAMERA_LIGHT_URL = "/command/floodlight_set_config" +CMD_CAMERA_STATUS_URL = "/command/changestatus" + DATA_PERSONS = "netatmo_persons" NETATMO_WEBHOOK_URL = None diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 1c82cd98dee271..0f5d6f9df66858 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -3,22 +3,14 @@ import pyatmo import requests -import voluptuous as vol from homeassistant.components.light import Light -from homeassistant.const import ATTR_ENTITY_ID -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send from .camera import CameraData -from .const import AUTH, DOMAIN, MANUFACTURER +from .const import AUTH, CMD_CAMERA_LIGHT_URL, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) -SCHEMA_SERVICE_SETLIGHTAUTO = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids} -) - async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo camera light platform.""" @@ -42,21 +34,6 @@ def get_entities(): async_add_entities(await hass.async_add_executor_job(get_entities), True) - 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, SCHEMA_SERVICE_SETLIGHTAUTO - ) - class NetatmoLight(Light): """Representation of a Netatmo Presence camera light.""" @@ -65,9 +42,9 @@ def __init__(self, camera_data: CameraData, camera_id: str): """Initialize a Netatmo Presence camera light.""" self._camera_id = camera_id self._data = camera_data - self._camera_type = self._data.camera_data.cameraById(camera_id).get("type") + self._camera_type = self._data.camera_data.get_camera(camera_id).get("type") self._name = ( - f"{MANUFACTURER} {self._data.camera_data.cameraById(camera_id).get('name')}" + f"{MANUFACTURER} {self._data.camera_data.get_camera(camera_id).get('name')}" ) self._is_on = False self._unique_id = f"{self._camera_id}-{self._camera_type}-light" @@ -117,63 +94,48 @@ def is_on(self): def turn_on(self, **kwargs): """Instruct the light to turn on.""" _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) - self._set_light_mode("on") - self._is_on = True + if self._set_light_mode("on"): + self._data.camera_data.get_camera(self._camera_id)[ + "light_mode_status" + ] = "on" self.schedule_update_ha_state() def turn_off(self, **kwargs): """Instruct the light to turn off.""" _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) - self._set_light_mode("off") - self._is_on = False + if self._set_light_mode("off"): + self._data.camera_data.get_camera(self._camera_id)[ + "light_mode_status" + ] = "off" self.schedule_update_ha_state() - # Netatmo Presence specific camera methods. - def update(self): """Update the camera data.""" self._data.update() (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( cid=self._camera_id ) + if self._data.camera_data.get_light_state(self._camera_id) == "on": + self._is_on = True + else: + self._is_on = False def _set_light_mode(self, mode: str): """Set light mode ('auto', 'on', 'off').""" try: - config = f'{{"mode":"{mode}"}}' - if self._localurl: - requests.get( - f"{self._localurl}/command/floodlight_set_config", - params={"config": config}, - timeout=10, - ) - elif self._vpnurl: - requests.get( - f"{self._vpnurl}/command/floodlight_set_config", - params={"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( - cid=self._camera_id - ) - 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( - cid=self._camera_id + camera_url = self._localurl if self._localurl else self._vpnurl + params = {"config": f'{{"mode":"{mode}"}}'} + + resp = requests.get( + url=f"{camera_url}{CMD_CAMERA_LIGHT_URL}", + params=params, + timeout=10, + verify=self._verify_ssl, ) - return None + if resp.ok: + return True + return False + except requests.exceptions.RequestException as error: + _LOGGER.error("Welcome/Presence URL changed: %s", error) else: self.async_schedule_update_ha_state(True) - - # 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") From 03b7a54a98de080b5a054d01f67152e65d7cd20b Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 19 Dec 2019 07:42:29 +0100 Subject: [PATCH 41/58] Only allow camera entities --- homeassistant/components/netatmo/camera.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 15c7e19f28adfe..752acfa87cbd6c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -5,7 +5,11 @@ import requests import voluptuous as vol -from homeassistant.components.camera import SUPPORT_STREAM, Camera +from homeassistant.components.camera import ( + DOMAIN as CAMERA_DOMAIN, + SUPPORT_STREAM, + Camera, +) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -39,7 +43,7 @@ _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} SCHEMA_SERVICE_SETLIGHTAUTO = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids} + {vol.Optional(ATTR_ENTITY_ID): cv.entity_domain(CAMERA_DOMAIN)} ) From f18a227995fff37e8b6797d7b3b2b35d7463680a Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 20 Dec 2019 08:44:16 +0100 Subject: [PATCH 42/58] Make test pass --- tests/components/netatmo/test_config_flow.py | 28 +++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 51cf75444c66f2..593948c8e6efe9 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Netatmo config flow.""" -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, setup from homeassistant.components.netatmo.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -17,11 +17,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): hass, "netatmo", { - "netatmo": { - "type": "oauth2", - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, - }, + "netatmo": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, "http": {"base_url": "https://example.com"}, }, ) @@ -31,11 +27,25 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): ) state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + scope = "+".join( + [ + "read_station", + "read_camera", + "access_camera", + "write_camera", + "read_presence", + "access_presence", + "read_homecoach", + "read_smokedetector", + "read_thermostat", + "write_thermostat", + ] + ) + assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}" + f"&state={state}&scope={scope}" ) client = await aiohttp_client(hass.http.app) @@ -56,5 +66,3 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "oauth2" From eb50f2b9fee3bba311f9449624afe2d73fa5e03e Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Jan 2020 08:45:21 +0100 Subject: [PATCH 43/58] Upgrade pyatmo module to 3.2.0 --- homeassistant/components/netatmo/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index ebfe7ca4ca31c2..75824a9ebda443 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==3.1.0" + "pyatmo==3.2.0" ], "dependencies": [ "webhook" From 038dbc564e2cb913a3967a023f6ef0f5fde1e3c7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Jan 2020 08:53:38 +0100 Subject: [PATCH 44/58] Update requirements --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_all.txt b/requirements_all.txt index fe14f075618e85..2d2c3ef5a35d60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.1.0 +pyatmo==3.2.0 # homeassistant.components.atome pyatome==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75fc04fd736244..7431e6f85f4330 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -399,7 +399,7 @@ pyalmond==0.0.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==3.1.0 +pyatmo==3.2.0 # homeassistant.components.blackbird pyblackbird==0.5 From 7acc0356224cd14f2c8b809e62c239058a882126 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Jan 2020 09:41:05 +0100 Subject: [PATCH 45/58] Remove list comprehension --- homeassistant/components/netatmo/camera.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 752acfa87cbd6c..ccb9f6ebf41280 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -343,10 +343,7 @@ def get_all_cameras(self): def get_modules(self, camera_id): """Return all modules for a given camera.""" - return [ - module - for module in self.camera_data.get_camera(camera_id).get("modules", []) - ] + return self.camera_data.get_camera(camera_id).get("modules", []) def get_camera_type(self, camera_id): """Return camera type for a camera, cid has preference over camera.""" From 1fbcafd99ff5bb81e207fcb0a89e0751bb0982a5 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Jan 2020 13:13:16 +0100 Subject: [PATCH 46/58] Remove guideline violating code --- homeassistant/components/netatmo/camera.py | 80 ------------------- homeassistant/components/netatmo/const.py | 3 - homeassistant/components/netatmo/light.py | 41 +--------- .../components/netatmo/services.yaml | 9 +-- 4 files changed, 2 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ccb9f6ebf41280..dee0c74fbf9061 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -12,17 +12,11 @@ ) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.util import Throttle from .const import ( ATTR_PSEUDO, AUTH, - CMD_CAMERA_LIGHT_URL, - CMD_CAMERA_STATUS_URL, DATA_PERSONS, DOMAIN, MANUFACTURER, @@ -69,21 +63,6 @@ def get_entities(): async_add_entities(await hass.async_add_executor_job(get_entities), True) - 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, SCHEMA_SERVICE_SETLIGHTAUTO - ) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up access to Netatmo cameras.""" @@ -264,65 +243,6 @@ def update(self): self.is_streaming = self._alim_status == "on" - def turn_on(self): - """Enable motion detection in the camera.""" - _LOGGER.debug("Turn the camera '%s' on", self._name) - self._toggle_camera_operation(True) - - def turn_off(self): - """Disable motion detection in camera.""" - _LOGGER.debug("Turn the camera '%s' off", self._name) - self._toggle_camera_operation(False) - - def _toggle_camera_operation(self, enable): - """Enable or disable the camera.""" - try: - camera_url = self._localurl if self._localurl else self._vpnurl - params = {"status": _BOOL_TO_STATE.get(enable)} - - requests.get( - url=f"{camera_url}{CMD_CAMERA_STATUS_URL}", - params=params, - timeout=10, - verify=self._verify_ssl, - ) - except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome/Presence URL changed: %s", error) - else: - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Subscribe to signals and add camera to list.""" - if self._camera_type == "NOC": - _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 - ) - - def _set_light_mode(self, mode: str): - """Set light mode ('auto', 'on', 'off').""" - try: - camera_url = self._localurl if self._localurl else self._vpnurl - params = {"config": f'{{"mode":"{mode}"}}'} - - requests.get( - url=f"{camera_url}{CMD_CAMERA_LIGHT_URL}", - params=params, - timeout=10, - verify=self._verify_ssl, - ) - except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome/Presence URL changed: %s", error) - else: - self.async_schedule_update_ha_state(True) - - 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") - class CameraData: """Get the latest data from Netatmo.""" diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 467004deec708f..5d981dc23b4586 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -14,9 +14,6 @@ OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" -CMD_CAMERA_LIGHT_URL = "/command/floodlight_set_config" -CMD_CAMERA_STATUS_URL = "/command/changestatus" - DATA_PERSONS = "netatmo_persons" NETATMO_WEBHOOK_URL = None diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 0f5d6f9df66858..b77468e9b9dd38 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -2,12 +2,11 @@ import logging import pyatmo -import requests from homeassistant.components.light import Light from .camera import CameraData -from .const import AUTH, CMD_CAMERA_LIGHT_URL, DOMAIN, MANUFACTURER +from .const import AUTH, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -91,24 +90,6 @@ def is_on(self): """Return true if light is on.""" return self._is_on - def turn_on(self, **kwargs): - """Instruct the light to turn on.""" - _LOGGER.debug("Set the flood light on for the camera '%s'", self._name) - if self._set_light_mode("on"): - self._data.camera_data.get_camera(self._camera_id)[ - "light_mode_status" - ] = "on" - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Instruct the light to turn off.""" - _LOGGER.debug("Set the flood light off for the camera '%s'", self._name) - if self._set_light_mode("off"): - self._data.camera_data.get_camera(self._camera_id)[ - "light_mode_status" - ] = "off" - self.schedule_update_ha_state() - def update(self): """Update the camera data.""" self._data.update() @@ -119,23 +100,3 @@ def update(self): self._is_on = True else: self._is_on = False - - def _set_light_mode(self, mode: str): - """Set light mode ('auto', 'on', 'off').""" - try: - camera_url = self._localurl if self._localurl else self._vpnurl - params = {"config": f'{{"mode":"{mode}"}}'} - - resp = requests.get( - url=f"{camera_url}{CMD_CAMERA_LIGHT_URL}", - params=params, - timeout=10, - verify=self._verify_ssl, - ) - if resp.ok: - return True - return False - except requests.exceptions.RequestException as error: - _LOGGER.error("Welcome/Presence URL changed: %s", error) - else: - self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index bc1e307d3a45df..46de69b5cb36d3 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -1,11 +1,4 @@ # Describes the format for available Netatmo services -set_light_auto: - description: Set the camera (Presence only) light in automatic mode. - fields: - entity_id: - description: Entity id. - example: "light.living_room" - set_schedule: description: Set the heating schedule. fields: @@ -14,4 +7,4 @@ set_schedule: example: Standard home_name: description: Home name. - example: MyHome \ No newline at end of file + example: MyHome From 07e52a7863d696ba39e3205686ccd4c05b889b58 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Jan 2020 13:17:14 +0100 Subject: [PATCH 47/58] Remove stale code --- homeassistant/components/netatmo/sensor.py | 24 ---------------------- 1 file changed, 24 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 3cdef8805e337a..04201bb9e3dc2a 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -155,30 +155,6 @@ def get_devices(): async_add_entities(await hass.async_add_executor_job(get_devices), True) - # def setup_public(): - # devices = [] - # _LOGGER.debug("Adding public weather sensor %s", config_public) - - # if config_public.get(CONF_AREAS) is not None: - # for area in config_public[CONF_AREAS]: - # _LOGGER.debug("Adding public weather sensor %s", area[CONF_NAME]) - # data = NetatmoPublicData( - # hass.data[DOMAIN][AUTH], - # lat_ne=area[CONF_LAT_NE], - # lon_ne=area[CONF_LON_NE], - # lat_sw=area[CONF_LAT_SW], - # lon_sw=area[CONF_LON_SW], - # ) - # for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: - # devices.append( - # NetatmoPublicSensor( - # area[CONF_NAME], data, sensor_type, area[CONF_MODE] - # ) - # ) - # return devices - - # async_add_entities(await hass.async_add_executor_job(setup_public), True) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" From a8d182ac6d5a985a3934a274e10cc3215bc44ac4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Jan 2020 13:21:38 +0100 Subject: [PATCH 48/58] Rename devices to entities --- homeassistant/components/netatmo/climate.py | 12 ++++++------ homeassistant/components/netatmo/sensor.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 7f57120bfd06de..e34706fdbc9287 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -110,9 +110,9 @@ async def async_setup_entry(hass, entry, async_add_entities): home_data = HomeData(auth) - def get_devices(): - """Retrieve Netatmo devices.""" - devices = [] + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] try: home_data.setup() except pyatmo.NoDevice: @@ -128,10 +128,10 @@ def get_devices(): for room_id in room_data.get_room_ids(): room_name = room_data.homedata.rooms[home_id][room_id]["name"] _LOGGER.debug("Setting up room %s (%s) ...", room_name, room_id) - devices.append(NetatmoThermostat(room_data, room_id)) - return devices + entities.append(NetatmoThermostat(room_data, room_id)) + return entities - async_add_entities(await hass.async_add_executor_job(get_devices), True) + async_add_entities(await hass.async_add_executor_job(get_entities), True) def _service_setschedule(service): """Service to change current home schedule.""" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 04201bb9e3dc2a..7ce039fca7509c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -122,8 +122,8 @@ async def async_setup_entry(hass, entry, async_add_entities): auth = hass.data[DOMAIN][entry.entry_id][AUTH] # config_public = hass.data[DOMAIN][CONF_PUBLIC] - def find_devices(data): - """Find all devices.""" + def find_entities(data): + """Find all entities.""" all_module_infos = data.get_module_infos() entities = [] for module in all_module_infos.values(): @@ -134,9 +134,9 @@ def find_devices(data): entities.append(NetatmoSensor(data, module, condition.lower())) return entities - def get_devices(): - """Retrieve Netatmo devices.""" - devices = [] + def get_entities(): + """Retrieve Netatmo entities.""" + entities = [] for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: @@ -145,15 +145,15 @@ def get_devices(): data = NetatmoData(auth, dc_data) except pyatmo.NoDevice: _LOGGER.debug( - "No %s devices found", NETATMO_DEVICE_TYPES[data_class.__name__] + "No %s entities found", NETATMO_DEVICE_TYPES[data_class.__name__] ) continue - devices.extend(find_devices(data)) + entities.extend(find_entities(data)) - return devices + return entities - async_add_entities(await hass.async_add_executor_job(get_devices), True) + async_add_entities(await hass.async_add_executor_job(get_entities), True) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): From ec72262f059bc7ede6a2f6ae76816f6f85e60391 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 3 Jan 2020 07:31:12 +0100 Subject: [PATCH 49/58] Remove light platform --- homeassistant/components/netatmo/__init__.py | 2 +- homeassistant/components/netatmo/light.py | 102 ------------------- 2 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 homeassistant/components/netatmo/light.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index e2d9cc8adcd0cc..ace12d3838cada 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -26,7 +26,7 @@ extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["binary_sensor", "camera", "climate", "light", "sensor"] +PLATFORMS = ["binary_sensor", "camera", "climate", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py deleted file mode 100644 index b77468e9b9dd38..00000000000000 --- a/homeassistant/components/netatmo/light.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Support for the Netatmo camera lights.""" -import logging - -import pyatmo - -from homeassistant.components.light import Light - -from .camera import CameraData -from .const import AUTH, DOMAIN, MANUFACTURER - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up the Netatmo camera light platform.""" - auth = hass.data[DOMAIN][entry.entry_id][AUTH] - - def get_entities(): - """Retrieve Netatmo entities.""" - entities = [] - - try: - camera_data = CameraData(hass, auth) - - for camera in camera_data.get_all_cameras(): - if camera["type"] == "NOC": - _LOGGER.debug("Setting up camera %s", camera["id"]) - entities.append(NetatmoLight(camera_data, camera["id"])) - except pyatmo.NoDevice: - _LOGGER.debug("No cameras found") - - return entities - - async_add_entities(await hass.async_add_executor_job(get_entities), True) - - -class NetatmoLight(Light): - """Representation of a Netatmo Presence camera light.""" - - def __init__(self, camera_data: CameraData, camera_id: str): - """Initialize a Netatmo Presence camera light.""" - self._camera_id = camera_id - self._data = camera_data - self._camera_type = self._data.camera_data.get_camera(camera_id).get("type") - self._name = ( - f"{MANUFACTURER} {self._data.camera_data.get_camera(camera_id).get('name')}" - ) - self._is_on = False - self._unique_id = f"{self._camera_id}-{self._camera_type}-light" - self._vpnurl = self._localurl = None - self._verify_ssl = True - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def device_info(self): - """Return the device info for the sensor.""" - return { - "identifiers": {(DOMAIN, self._camera_id)}, - "name": self._name, - "manufacturer": MANUFACTURER, - "model": self._camera_type, - } - - @property - def unique_id(self): - """Return the unique ID for this light.""" - return self._unique_id - - @property - def supported_features(self): - """Flag supported features.""" - return 0 - - @property - def should_poll(self): - """Return if we should poll this device.""" - return True - - @property - def assumed_state(self) -> bool: - """Return False if unable to access real state of the entity.""" - return False - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - def update(self): - """Update the camera data.""" - self._data.update() - (self._vpnurl, self._localurl) = self._data.camera_data.camera_urls( - cid=self._camera_id - ) - if self._data.camera_data.get_light_state(self._camera_id) == "on": - self._is_on = True - else: - self._is_on = False From 7c066d1b6962712be2240a5a26cbbc1ceb86a9cb Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 3 Jan 2020 13:57:15 +0100 Subject: [PATCH 50/58] Remove commented code --- homeassistant/components/netatmo/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 7ce039fca7509c..de31a2ee597823 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -120,7 +120,6 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up the Netatmo weather and homecoach platform.""" auth = hass.data[DOMAIN][entry.entry_id][AUTH] - # config_public = hass.data[DOMAIN][CONF_PUBLIC] def find_entities(data): """Find all entities.""" From 0ec05680f83752b15fee9ed1b1064ccecaa847f6 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 3 Jan 2020 13:59:31 +0100 Subject: [PATCH 51/58] Exclude files from coverage --- .coveragerc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 05682c79744083..5a6e1e0650f14c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -455,8 +455,12 @@ omit = homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* - homeassistant/components/netatmo/* - homeassistant/components/netatmo_public/sensor.py + homeassistant/components/netatmo/__init__.py + homeassistant/components/netatmo/binary_sensor.py + homeassistant/components/netatmo/api.py + homeassistant/components/netatmo/camera.py + homeassistant/components/netatmo/climate.py + homeassistant/components/netatmo/const.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear_lte/* From aee09d33753839522eeeb104e7e56b03cb654b46 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 3 Jan 2020 14:00:29 +0100 Subject: [PATCH 52/58] Remove unused code --- homeassistant/components/netatmo/sensor.py | 23 ---------------------- 1 file changed, 23 deletions(-) diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index de31a2ee597823..b6aaceed78ee3c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -5,18 +5,13 @@ from time import time import pyatmo -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MODE, - CONF_NAME, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -87,24 +82,6 @@ "health_idx": ["Health", "", "mdi:cloud", None], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_AREAS): vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_LAT_NE): cv.latitude, - vol.Required(CONF_LAT_SW): cv.latitude, - vol.Required(CONF_LON_NE): cv.longitude, - vol.Required(CONF_LON_SW): cv.longitude, - vol.Optional(CONF_MODE, default=DEFAULT_MODE): vol.In(MODE_TYPES), - vol.Optional(CONF_NAME, default=DEFAULT_NAME_PUBLIC): cv.string, - } - ], - ) - } -) - MODULE_TYPE_OUTDOOR = "NAModule1" MODULE_TYPE_WIND = "NAModule2" MODULE_TYPE_RAIN = "NAModule3" From dbfae855eb322d98591ef0eea9026e3ea1d3f78d Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 3 Jan 2020 14:07:35 +0100 Subject: [PATCH 53/58] Fix unique id --- homeassistant/components/netatmo/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index e34706fdbc9287..dea28e791dca5b 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -193,7 +193,7 @@ def device_info(self): @property def unique_id(self): """Return a unique ID.""" - return self._room_id + return self._unique_id @property def supported_features(self): From fa1078574544208305495494abe56a7fc1a7c41c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 9 Jan 2020 20:31:27 +0100 Subject: [PATCH 54/58] Address comments --- homeassistant/components/netatmo/camera.py | 4 +-- homeassistant/components/netatmo/climate.py | 4 +-- homeassistant/components/netatmo/sensor.py | 27 +++++---------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index dee0c74fbf9061..eb2c166fb6a540 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -64,8 +64,8 @@ def get_entities(): async_add_entities(await hass.async_add_executor_job(get_entities), True) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up access to Netatmo cameras.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the available Netatmo weather sensors.""" return diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index dea28e791dca5b..e2bda0f22b1a7e 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -149,8 +149,8 @@ def _service_setschedule(service): ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the NetAtmo Thermostat.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the available Netatmo weather sensors.""" return diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index b6aaceed78ee3c..f8245e736fbcd4 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,7 +1,6 @@ """Support for the Netatmo Weather Service.""" from datetime import timedelta import logging -import threading from time import time import pyatmo @@ -30,8 +29,6 @@ DEFAULT_MODE = "avg" MODE_TYPES = {"max", "avg"} -DEFAULT_NAME_PUBLIC = "Public Data" - # This is the Netatmo data upload interval in seconds NETATMO_UPDATE_INTERVAL = 600 @@ -519,7 +516,6 @@ def __init__(self, auth, station_data): self.station_data = station_data self._next_update = time() self.auth = auth - self._update_in_progress = threading.Lock() def get_module_infos(self): """Return all modules available on the API as a dict.""" @@ -527,22 +523,11 @@ def get_module_infos(self): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Call the Netatmo API to update the data. + """Call the Netatmo API to update the data.""" + self.station_data = self.station_data.__class__(self.auth) - This method is not throttled by the builtin Throttle decorator - but with a custom logic, which takes into account the time - of the last update from the cloud. - """ - if not self._update_in_progress.acquire(False): + data = self.station_data.lastData(exclude=3600, byId=True) + if not data: + _LOGGER.debug("No data received when updating station data") return - - try: - self.station_data = self.station_data.__class__(self.auth) - - data = self.station_data.lastData(exclude=3600, byId=True) - if not data: - _LOGGER.debug("No data received when updating station data") - return - self.data = data - finally: - self._update_in_progress.release() + self.data = data From 62b6bfd419402c6975713d3971b6a9b68cd8125c Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 9 Jan 2020 21:56:13 +0100 Subject: [PATCH 55/58] Fix comments --- homeassistant/components/netatmo/binary_sensor.py | 2 +- homeassistant/components/netatmo/camera.py | 2 +- homeassistant/components/netatmo/climate.py | 2 +- homeassistant/components/netatmo/sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 0ed5243e6736bc..d420fbb1783d2b 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass, entry, async_add_entities): - """Set up the Netatmo energy platform.""" + """Set up the access to Netatmo binary sensor.""" auth = hass.data[DOMAIN][entry.entry_id][AUTH] def get_entities(): diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index eb2c166fb6a540..08a3847c0b75a8 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -65,7 +65,7 @@ def get_entities(): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the available Netatmo weather sensors.""" + """Set up the Netatmo camera platform.""" return diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index e2bda0f22b1a7e..f36328a58877ca 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -150,7 +150,7 @@ def _service_setschedule(service): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the available Netatmo weather sensors.""" + """Set up the Netatmo energy sensors.""" return diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index f8245e736fbcd4..64a203c47a23ca 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -130,7 +130,7 @@ def get_entities(): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the available Netatmo weather sensors.""" + """Set up the Netatmo weather and homecoach platform.""" return From 86124f08e904e4b5c9b253657d954b7fa69d44a4 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 9 Jan 2020 22:51:27 +0100 Subject: [PATCH 56/58] Exclude sensor as well --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 5a6e1e0650f14c..be11fa5998c76f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -461,6 +461,7 @@ omit = homeassistant/components/netatmo/camera.py homeassistant/components/netatmo/climate.py homeassistant/components/netatmo/const.py + homeassistant/components/netatmo/sensor.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear_lte/* From 5c573bf893d913db049bb42f7bd3946b54090cad Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 9 Jan 2020 23:34:28 +0100 Subject: [PATCH 57/58] Add another test --- tests/components/netatmo/test_config_flow.py | 23 +++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 593948c8e6efe9..113f19472b0e03 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Netatmo config flow.""" -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( DOMAIN, OAUTH2_AUTHORIZE, @@ -7,10 +8,30 @@ ) from homeassistant.helpers import config_entry_oauth2_flow +from tests.common import MockConfigEntry + CLIENT_ID = "1234" CLIENT_SECRET = "5678" +async def test_abort_if_existing_entry(hass): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + flow = config_flow.NetatmoFlowHandler() + flow.hass = hass + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + result = await flow.async_step_homekit( + {"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_setup" + + async def test_full_flow(hass, aiohttp_client, aioclient_mock): """Check full flow.""" assert await setup.async_setup_component( From f32424e1cbf16c06082474fafdbd8ee3a347a889 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Fri, 10 Jan 2020 22:52:16 +0100 Subject: [PATCH 58/58] Use core interfaces --- tests/components/netatmo/test_config_flow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 113f19472b0e03..24aac6dc878b09 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -21,12 +21,16 @@ async def test_abort_if_existing_entry(hass): flow = config_flow.NetatmoFlowHandler() flow.hass = hass - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + "netatmo", context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" - result = await flow.async_step_homekit( - {"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}} + result = await hass.config_entries.flow.async_init( + "netatmo", + context={"source": "homekit"}, + data={"host": "0.0.0.0", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup"