From eb1ca20cfc0509d8b591bc9ed7cece0f0838cd8a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 14:14:36 -0700 Subject: [PATCH 001/924] Version bump to 0.65.4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 203a9c63d95..12d988c552e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '3' +PATCH_VERSION = '4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 101a6ab07c96e770b004059837ec39fec08431c7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 13:54:56 -0700 Subject: [PATCH 002/924] Fix unavailable property for wemo switch (#13106) * Fix unavailable property for wemo switch * Have subscriptions respect the lock * Move subscription callback to added to hass section --- homeassistant/components/switch/wemo.py | 91 +++++++++++++++++-------- 1 file changed, 62 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 4339c92bb60..4f06f941558 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -4,16 +4,19 @@ Support for WeMo switches. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.wemo/ """ +import asyncio import logging from datetime import datetime, timedelta +import async_timeout + from homeassistant.components.switch import SwitchDevice from homeassistant.util import convert from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN) -from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] +SCAN_INTERVAL = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) @@ -54,29 +57,35 @@ class WemoSwitch(SwitchDevice): self.maker_params = None self.coffeemaker_mode = None self._state = None + self._available = True + self._update_lock = None # look up model name once as it incurs network traffic self._model_name = self.wemo.model_name - wemo = get_component('wemo') - wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) - wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) - - def _update_callback(self, _device, _type, _params): + def _subscription_callback(self, _device, _type, _params): """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", _device) + _LOGGER.info("Subscription update for %s", self.name) updated = self.wemo.subscription_update(_type, _params) - self._update(force_update=(not updated)) + self.hass.add_job( + self._async_locked_subscription_callback(not updated)) - if not hasattr(self, 'hass'): + async def _async_locked_subscription_callback(self, force_update): + """Helper to handle an update from a subscription.""" + # If an update is in progress, we don't do anything + if self._update_lock.locked(): return - self.schedule_update_ha_state() + + await self._async_locked_update(force_update) + self.async_schedule_update_ha_state() @property def should_poll(self): - """No polling needed with subscriptions.""" - if self._model_name == 'Insight': - return True - return False + """Device should poll. + + Subscriptions push the state, however it won't detect if a device + is no longer available. Use polling to detect if a device is available. + """ + return True @property def unique_id(self): @@ -172,13 +181,7 @@ class WemoSwitch(SwitchDevice): @property def available(self): """Return true if switch is available.""" - if self._model_name == 'Insight' and self.insight_params is None: - return False - if self._model_name == 'Maker' and self.maker_params is None: - return False - if self._model_name == 'CoffeeMaker' and self.coffeemaker_mode is None: - return False - return True + return self._available @property def icon(self): @@ -189,21 +192,46 @@ class WemoSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self._state = WEMO_ON self.wemo.on() - self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the switch off.""" - self._state = WEMO_OFF self.wemo.off() - self.schedule_update_ha_state() - def update(self): - """Update WeMo state.""" - self._update(force_update=True) + async def async_added_to_hass(self): + """Wemo switch added to HASS.""" + # Define inside async context so we know our event loop + self._update_lock = asyncio.Lock() - def _update(self, force_update=True): + registry = self.hass.components.wemo.SUBSCRIPTION_REGISTRY + await self.hass.async_add_job(registry.register, self.wemo) + registry.on(self.wemo, None, self._subscription_callback) + + async def async_update(self): + """Update WeMo state. + + Wemo has an aggressive retry logic that sometimes can take over a + minute to return. If we don't get a state after 5 seconds, assume the + Wemo switch is unreachable. If update goes through, it will be made + available again. + """ + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + try: + with async_timeout.timeout(5): + await asyncio.shield(self._async_locked_update(True)) + except asyncio.TimeoutError: + _LOGGER.warning('Lost connection to %s', self.name) + self._available = False + + async def _async_locked_update(self, force_update): + """Try updating within an async lock.""" + async with self._update_lock: + await self.hass.async_add_job(self._update, force_update) + + def _update(self, force_update): """Update the device state.""" try: self._state = self.wemo.get_state(force_update) @@ -215,6 +243,11 @@ class WemoSwitch(SwitchDevice): self.maker_params = self.wemo.maker_params elif self._model_name == 'CoffeeMaker': self.coffeemaker_mode = self.wemo.mode + + if not self._available: + _LOGGER.info('Reconnected to %s', self.name) + self._available = True except AttributeError as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) + self._available = False From 3560fa754c8c410b7c0f0d1e0d1d1d9f00de62aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 12 Mar 2018 13:55:22 -0700 Subject: [PATCH 003/924] Catch if bridge goes unavailable (#13109) --- homeassistant/components/hue.py | 1 + homeassistant/components/light/hue.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue.py b/homeassistant/components/hue.py index d3870f0a3a1..f6e654ab44b 100644 --- a/homeassistant/components/hue.py +++ b/homeassistant/components/hue.py @@ -181,6 +181,7 @@ class HueBridge(object): self.allow_in_emulated_hue = allow_in_emulated_hue self.allow_hue_groups = allow_hue_groups + self.available = True self.bridge = None self.lights = {} self.lightgroups = {} diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 75825683928..661b7c2b3a1 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -123,15 +123,20 @@ def unthrottled_update_lights(hass, bridge, add_devices): api = bridge.get_api() except phue.PhueRequestTimeout: _LOGGER.warning("Timeout trying to reach the bridge") + bridge.available = False return except ConnectionRefusedError: _LOGGER.error("The bridge refused the connection") + bridge.available = False return except socket.error: # socket.error when we cannot reach Hue _LOGGER.exception("Cannot reach the bridge") + bridge.available = False return + bridge.available = True + new_lights = process_lights( hass, api, bridge, lambda **kw: update_lights(hass, bridge, add_devices, **kw)) @@ -266,8 +271,9 @@ class HueLight(Light): @property def available(self): """Return if light is available.""" - return (self.is_group or self.allow_unreachable or - self.info['state']['reachable']) + return self.bridge.available and (self.is_group or + self.allow_unreachable or + self.info['state']['reachable']) @property def supported_features(self): From c384fd96533955fded181c1fd8e301d072a43cbf Mon Sep 17 00:00:00 2001 From: Jeroen ter Heerdt Date: Mon, 12 Mar 2018 22:03:05 +0100 Subject: [PATCH 004/924] Adding check for empty discovery info in alarm control panel Egardia. (#13114) --- homeassistant/components/alarm_control_panel/egardia.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 64e165f6b16..845eb81bbe0 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -33,6 +33,8 @@ STATES = { def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Egardia platform.""" + if discovery_info is None: + return device = EgardiaAlarm( discovery_info['name'], hass.data[EGARDIA_DEVICE], From e54394e90698e95388ed1466ecab26de3dab8f2a Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 12 Mar 2018 16:56:33 -0400 Subject: [PATCH 005/924] Throttle Arlo api calls (#13143) --- homeassistant/components/arlo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 7e51ec8c045..77201e5ead9 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,11 +5,13 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging +from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['pyarlo==0.1.2'] @@ -45,6 +47,7 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False + arlo.update = Throttle(timedelta(seconds=10))(arlo.update) hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) From 0ef43400990b61387503e42bb608eec06411ba80 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 14 Mar 2018 04:50:08 +0100 Subject: [PATCH 006/924] Fix freegeoip (#13193) --- homeassistant/util/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index 0cd0b14d3ab..dae8ed17dc9 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -10,7 +10,7 @@ from typing import Any, Optional, Tuple, Dict import requests ELEVATION_URL = 'http://maps.googleapis.com/maps/api/elevation/json' -FREEGEO_API = 'https://freegeoip.io/json/' +FREEGEO_API = 'https://freegeoip.net/json/' IP_API = 'http://ip-api.com/json' # Constants from https://github.com/maurycyp/vincenty From cfded7eab91a82aaed199802f773a95ec5e534c2 Mon Sep 17 00:00:00 2001 From: JC Connell Date: Wed, 14 Mar 2018 03:01:10 -0400 Subject: [PATCH 007/924] Python Spotcrime sensor requires API key, fixes include/exclude (#12926) * Add spotcrime.py to dev * Modify sensor to accept user API key * Update Spotcrime to 1.0.3 in requirements_all.txt * Fix line 76 (97 > 79 characters) * Fix lint errors --- homeassistant/components/sensor/spotcrime.py | 19 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/spotcrime.py b/homeassistant/components/sensor/spotcrime.py index 169bcc5f867..08177c9a7b9 100644 --- a/homeassistant/components/sensor/spotcrime.py +++ b/homeassistant/components/sensor/spotcrime.py @@ -12,14 +12,15 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_INCLUDE, CONF_EXCLUDE, CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_RADIUS) +from homeassistant.const import (CONF_API_KEY, CONF_INCLUDE, CONF_EXCLUDE, + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + ATTR_ATTRIBUTION, ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_RADIUS) from homeassistant.helpers.entity import Entity from homeassistant.util import slugify import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['spotcrime==1.0.2'] +REQUIREMENTS = ['spotcrime==1.0.3'] _LOGGER = logging.getLogger(__name__) @@ -34,6 +35,7 @@ SCAN_INTERVAL = timedelta(minutes=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_RADIUS): vol.Coerce(float), + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive(CONF_LATITUDE, 'coordinates'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates'): cv.longitude, vol.Optional(CONF_DAYS, default=DEFAULT_DAYS): cv.positive_int, @@ -49,28 +51,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): longitude = config.get(CONF_LONGITUDE, hass.config.longitude) name = config[CONF_NAME] radius = config[CONF_RADIUS] + api_key = config[CONF_API_KEY] days = config.get(CONF_DAYS) include = config.get(CONF_INCLUDE) exclude = config.get(CONF_EXCLUDE) add_devices([SpotCrimeSensor( name, latitude, longitude, radius, include, - exclude, days)], True) + exclude, api_key, days)], True) class SpotCrimeSensor(Entity): """Representation of a Spot Crime Sensor.""" def __init__(self, name, latitude, longitude, radius, - include, exclude, days): + include, exclude, api_key, days): """Initialize the Spot Crime sensor.""" import spotcrime self._name = name self._include = include self._exclude = exclude + self.api_key = api_key self.days = days self._spotcrime = spotcrime.SpotCrime( - (latitude, longitude), radius, None, None, self.days) + (latitude, longitude), radius, self._include, + self._exclude, self.api_key, self.days) self._attributes = None self._state = None self._previous_incidents = set() diff --git a/requirements_all.txt b/requirements_all.txt index c4c640b063a..9ad1be678fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1153,7 +1153,7 @@ somecomfort==0.5.0 speedtest-cli==2.0.0 # homeassistant.components.sensor.spotcrime -spotcrime==1.0.2 +spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator From 6310deb5c20b6e4cd4996582426f486081421359 Mon Sep 17 00:00:00 2001 From: Mark Perdue Date: Wed, 14 Mar 2018 03:10:47 -0400 Subject: [PATCH 008/924] Add new platform for VeSync switches (#13000) * Added vesync platform Support for power toggling, current power, and daily energy kWh * Adds vesync to requirements file. * Reorder vesync item in requirements_all.txt from gen_requirements_all * Removes unnecessary global values that are not used in this component * Removes try/catch from setup_platform -no throws. Guard check login() * Remove unnecessary boolean convert * Fix indentation of log messages --- homeassistant/components/switch/vesync.py | 104 ++++++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 107 insertions(+) create mode 100644 homeassistant/components/switch/vesync.py diff --git a/homeassistant/components/switch/vesync.py b/homeassistant/components/switch/vesync.py new file mode 100644 index 00000000000..fbc73545e19 --- /dev/null +++ b/homeassistant/components/switch/vesync.py @@ -0,0 +1,104 @@ +""" +Support for Etekcity VeSync switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.vesync/ +""" +import logging +import voluptuous as vol +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['pyvesync==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the VeSync switch platform.""" + from pyvesync.vesync import VeSync + + switches = [] + + manager = VeSync(config.get(CONF_USERNAME), config.get(CONF_PASSWORD)) + + if not manager.login(): + _LOGGER.error("Unable to login to VeSync") + return + + manager.update() + + if manager.devices is not None and manager.devices: + if len(manager.devices) == 1: + count_string = 'switch' + else: + count_string = 'switches' + + _LOGGER.info("Discovered %d VeSync %s", + len(manager.devices), count_string) + + for switch in manager.devices: + switches.append(VeSyncSwitchHA(switch)) + _LOGGER.info("Added a VeSync switch named '%s'", + switch.device_name) + else: + _LOGGER.info("No VeSync devices found") + + add_devices(switches) + + +class VeSyncSwitchHA(SwitchDevice): + """Representation of a VeSync switch.""" + + def __init__(self, plug): + """Initialize the VeSync switch device.""" + self.smartplug = plug + + @property + def unique_id(self): + """Return the ID of this switch.""" + return self.smartplug.cid + + @property + def name(self): + """Return the name of the switch.""" + return self.smartplug.device_name + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self.smartplug.get_power() + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + return self.smartplug.get_kwh_today() + + @property + def available(self) -> bool: + """Return True if switch is available.""" + return self.smartplug.connection_status == "online" + + @property + def is_on(self): + """Return True if switch is on.""" + return self.smartplug.device_status == "on" + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self.smartplug.turn_on() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self.smartplug.turn_off() + + def update(self): + """Handle data changes for node values.""" + self.smartplug.update() diff --git a/requirements_all.txt b/requirements_all.txt index 9ad1be678fa..1733f0d9a7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1023,6 +1023,9 @@ pyunifi==2.13 # homeassistant.components.vera pyvera==0.2.42 +# homeassistant.components.switch.vesync +pyvesync==0.1.1 + # homeassistant.components.media_player.vizio pyvizio==0.0.2 From 948f29544ac5a8d5ce280aac765ef75b8b043bf7 Mon Sep 17 00:00:00 2001 From: Vincent Van Den Berghe Date: Wed, 14 Mar 2018 08:14:36 +0100 Subject: [PATCH 009/924] Fixed SI units for current consumption (#13190) --- homeassistant/components/sensor/smappee.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index 51595d19b1a..c59798d16d7 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -21,17 +21,17 @@ SENSOR_TYPES = { 'active_power': ['Active Power', 'mdi:power-plug', 'local', 'W', 'active_power'], 'current': - ['Current', 'mdi:gauge', 'local', 'Amps', 'current'], + ['Current', 'mdi:gauge', 'local', 'A', 'current'], 'voltage': ['Voltage', 'mdi:gauge', 'local', 'V', 'voltage'], 'active_cosfi': ['Power Factor', 'mdi:gauge', 'local', '%', 'active_cosfi'], 'alwayson_today': - ['Always On Today', 'mdi:gauge', 'remote', 'kW', 'alwaysOn'], + ['Always On Today', 'mdi:gauge', 'remote', 'kWh', 'alwaysOn'], 'solar_today': - ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kW', 'solar'], + ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'], 'power_today': - ['Power Today', 'mdi:power-plug', 'remote', 'kW', 'consumption'] + ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'] } SCAN_INTERVAL = timedelta(seconds=30) From b6bed1dfabdf4ab59727ef5fc6ffb8571172d379 Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Wed, 14 Mar 2018 07:47:45 +0000 Subject: [PATCH 010/924] Report swap in MiB (#13148) It makes sense to report swap and memory in the same unit and MiB is more useful considering Home Assistant may be running on lower end hardware (Raspberry Pi for example) where 100MiB resolution is not adequate. --- homeassistant/components/sensor/systemmonitor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 79d5c261b88..2f970796fe1 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -42,8 +42,8 @@ SENSOR_TYPES = { 'process': ['Process', ' ', 'mdi:memory'], 'processor_use': ['Processor use', '%', 'mdi:memory'], 'since_last_boot': ['Since last boot', '', 'mdi:clock'], - 'swap_free': ['Swap free', 'GiB', 'mdi:harddisk'], - 'swap_use': ['Swap use', 'GiB', 'mdi:harddisk'], + 'swap_free': ['Swap free', 'MiB', 'mdi:harddisk'], + 'swap_use': ['Swap use', 'MiB', 'mdi:harddisk'], 'swap_use_percent': ['Swap use (percent)', '%', 'mdi:harddisk'], } @@ -135,9 +135,9 @@ class SystemMonitorSensor(Entity): elif self.type == 'swap_use_percent': self._state = psutil.swap_memory().percent elif self.type == 'swap_use': - self._state = round(psutil.swap_memory().used / 1024**3, 1) + self._state = round(psutil.swap_memory().used / 1024**2, 1) elif self.type == 'swap_free': - self._state = round(psutil.swap_memory().free / 1024**3, 1) + self._state = round(psutil.swap_memory().free / 1024**2, 1) elif self.type == 'processor_use': self._state = round(psutil.cpu_percent(interval=None)) elif self.type == 'process': From c48c8710b7f34b18ad946aadedb59262fbfcd6ba Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 14 Mar 2018 13:22:38 +0100 Subject: [PATCH 011/924] Bugfix HomeKit: Error string values for temperature (#13162) --- homeassistant/components/homekit/thermostats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/thermostats.py index 766a7e3585d..6d342273e8d 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/thermostats.py @@ -157,12 +157,12 @@ class Thermostat(HomeAccessory): # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: + if isinstance(current_temp, (int, float)): self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) - if target_temp is not None: + if isinstance(target_temp, (int, float)): if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) From 7e2fc19f5a10cbac4260c0bd66f0814c5accf986 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 11:39:38 -0700 Subject: [PATCH 012/924] Sort coveragerc --- .coveragerc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.coveragerc b/.coveragerc index 07d84523780..4149deee609 100644 --- a/.coveragerc +++ b/.coveragerc @@ -332,6 +332,7 @@ omit = homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py homeassistant/components/camera/onvif.py + homeassistant/components/camera/proxy.py homeassistant/components/camera/ring.py homeassistant/components/camera/rpi_camera.py homeassistant/components/camera/synology.py @@ -403,20 +404,20 @@ omit = homeassistant/components/image_processing/dlib_face_detect.py homeassistant/components/image_processing/dlib_face_identify.py homeassistant/components/image_processing/seven_segments.py - homeassistant/components/keyboard.py homeassistant/components/keyboard_remote.py + homeassistant/components/keyboard.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py - homeassistant/components/light/decora.py homeassistant/components/light/decora_wifi.py + homeassistant/components/light/decora.py homeassistant/components/light/flux_led.py homeassistant/components/light/greenwave.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py homeassistant/components/light/iglo.py - homeassistant/components/light/lifx.py homeassistant/components/light/lifx_legacy.py + homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py homeassistant/components/light/osramlightify.py @@ -442,6 +443,7 @@ omit = homeassistant/components/media_player/bluesound.py homeassistant/components/media_player/braviatv.py homeassistant/components/media_player/cast.py + homeassistant/components/media_player/channels.py homeassistant/components/media_player/clementine.py homeassistant/components/media_player/cmus.py homeassistant/components/media_player/denon.py @@ -482,8 +484,8 @@ omit = homeassistant/components/media_player/vlc.py homeassistant/components/media_player/volumio.py homeassistant/components/media_player/xiaomi_tv.py - homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/yamaha_musiccast.py + homeassistant/components/media_player/yamaha.py homeassistant/components/media_player/ziggo_mediabox_xl.py homeassistant/components/mycroft.py homeassistant/components/notify/aws_lambda.py @@ -491,8 +493,8 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend.py homeassistant/components/notify/clicksend_tts.py + homeassistant/components/notify/clicksend.py homeassistant/components/notify/discord.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py @@ -588,8 +590,8 @@ omit = homeassistant/components/sensor/haveibeenpwned.py homeassistant/components/sensor/hp_ilo.py homeassistant/components/sensor/htu21d.py - homeassistant/components/sensor/imap.py homeassistant/components/sensor/imap_email_content.py + homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py @@ -632,8 +634,8 @@ omit = homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py - homeassistant/components/sensor/serial.py homeassistant/components/sensor/serial_pm.py + homeassistant/components/sensor/serial.py homeassistant/components/sensor/shodan.py homeassistant/components/sensor/simulated.py homeassistant/components/sensor/skybeacon.py From e2029e397047338e9096e4f438d0ede301373e77 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 12:05:17 -0700 Subject: [PATCH 013/924] Add vesync to coveragerc --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 4149deee609..5fd43d5aec7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -699,6 +699,7 @@ omit = homeassistant/components/switch/telnet.py homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py + homeassistant/components/switch/vesync.py homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py From 7fc9ac09314a8e0a1cce326d55ab1ed577e22ce7 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:07:50 +0100 Subject: [PATCH 014/924] Avoid Sonos error when joining with self (#13196) --- homeassistant/components/media_player/sonos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9ea33b4c396..edd7d17c67d 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -886,7 +886,8 @@ class SonosDevice(MediaPlayerDevice): self.soco.unjoin() for slave in slaves: - slave.soco.join(self.soco) + if slave.unique_id != self.unique_id: + slave.soco.join(self.soco) @soco_error() def unjoin(self): From ef7ce5eb1bbd62b31dd22a34e25be1dc50781ac2 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:08:41 +0100 Subject: [PATCH 015/924] Ignore unsupported Sonos favorites (#13195) --- homeassistant/components/media_player/sonos.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index edd7d17c67d..2a12b59e7c7 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -426,7 +426,17 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = self.soco.play_mode self._night_sound = self.soco.night_mode self._speech_enhance = self.soco.dialog_mode - self._favorites = self.soco.music_library.get_sonos_favorites() + + self._favorites = [] + for fav in self.soco.music_library.get_sonos_favorites(): + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + # pylint: disable=broad-except + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) def _subscribe_to_player_events(self): """Add event subscriptions.""" From a9917e7a56e4056d3bef59d810e637637a1317d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 14:29:51 -0700 Subject: [PATCH 016/924] Fix history API (#13214) --- homeassistant/components/history.py | 10 +++++----- tests/components/test_history.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index dd14bbf6811..8ab91b08a3d 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -239,15 +239,16 @@ def get_state(hass, utc_point_in_time, entity_id, run=None): def async_setup(hass, config): """Set up the history hooks.""" filters = Filters() - exclude = config[DOMAIN].get(CONF_EXCLUDE) + conf = config.get(DOMAIN, {}) + exclude = conf.get(CONF_EXCLUDE) if exclude: filters.excluded_entities = exclude.get(CONF_ENTITIES, []) filters.excluded_domains = exclude.get(CONF_DOMAINS, []) - include = config[DOMAIN].get(CONF_INCLUDE) + include = conf.get(CONF_INCLUDE) if include: filters.included_entities = include.get(CONF_ENTITIES, []) filters.included_domains = include.get(CONF_DOMAINS, []) - use_include_order = config[DOMAIN].get(CONF_ORDER) + use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) yield from hass.components.frontend.async_register_built_in_panel( @@ -308,7 +309,7 @@ class HistoryPeriodView(HomeAssistantView): result = yield from hass.async_add_job( get_significant_states, hass, start_time, end_time, entity_ids, self.filters, include_start_time_state) - result = result.values() + result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start _LOGGER.debug( @@ -318,7 +319,6 @@ class HistoryPeriodView(HomeAssistantView): # by any entities explicitly included in the configuration. if self.use_include_order: - result = list(result) sorted_result = [] for order_entity in self.filters.included_entities: for state_list in result: diff --git a/tests/components/test_history.py b/tests/components/test_history.py index 4a759e7e0ac..be768f5ec69 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -4,7 +4,7 @@ from datetime import timedelta import unittest from unittest.mock import patch, sentinel -from homeassistant.setup import setup_component +from homeassistant.setup import setup_component, async_setup_component import homeassistant.core as ha import homeassistant.util.dt as dt_util from homeassistant.components import history, recorder @@ -481,3 +481,13 @@ class TestComponentHistory(unittest.TestCase): set_state(therm, 22, attributes={'current_temperature': 21, 'hidden': True}) return zero, four, states + + +async def test_fetch_period_api(hass, test_client): + """Test the fetch period view for history.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'history', {}) + client = await test_client(hass.http.app) + response = await client.get( + '/api/history/period/{}'.format(dt_util.utcnow().isoformat())) + assert response.status == 200 From e1a5e5a8ba5f922d5d223b0b2f97d5d9e60aba6f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 15:07:37 -0700 Subject: [PATCH 017/924] Fix input_boolean Google Assistant serialize error (#13220) --- .../components/google_assistant/smart_home.py | 22 ++++++++++++++----- .../google_assistant/test_smart_home.py | 15 +++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 48d24c00b97..834d40c367c 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -17,7 +17,16 @@ from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script, climate, + climate, + cover, + fan, + group, + input_boolean, + light, + media_player, + scene, + script, + switch, ) from . import trait @@ -33,15 +42,16 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) DOMAIN_TO_GOOGLE_TYPES = { + climate.DOMAIN: TYPE_THERMOSTAT, + cover.DOMAIN: TYPE_SWITCH, + fan.DOMAIN: TYPE_SWITCH, group.DOMAIN: TYPE_SWITCH, + input_boolean.DOMAIN: TYPE_SWITCH, + light.DOMAIN: TYPE_LIGHT, + media_player.DOMAIN: TYPE_SWITCH, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, - fan.DOMAIN: TYPE_SWITCH, - light.DOMAIN: TYPE_LIGHT, - cover.DOMAIN: TYPE_SWITCH, - media_player.DOMAIN: TYPE_SWITCH, - climate.DOMAIN: TYPE_THERMOSTAT, } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8d139fa8211..24d74afa6da 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,5 @@ """Test Google Smart Home.""" +from homeassistant.core import State from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from homeassistant.setup import async_setup_component @@ -244,3 +245,17 @@ async def test_raising_error_trait(hass): }] } } + + +def test_serialize_input_boolean(): + """Test serializing an input boolean entity.""" + state = State('input_boolean.bla', 'on') + entity = sh._GoogleEntity(None, BASIC_CONFIG, state) + assert entity.sync_serialize() == { + 'id': 'input_boolean.bla', + 'attributes': {}, + 'name': {'name': 'bla'}, + 'traits': ['action.devices.traits.OnOff'], + 'type': 'action.devices.types.SWITCH', + 'willReportState': False, + } From 874cccd5302477b072fd428230e8693dfe6a45d8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 14 Mar 2018 13:22:38 +0100 Subject: [PATCH 018/924] Bugfix HomeKit: Error string values for temperature (#13162) --- homeassistant/components/homekit/thermostats.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/thermostats.py index 766a7e3585d..6d342273e8d 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/thermostats.py @@ -157,12 +157,12 @@ class Thermostat(HomeAccessory): # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if current_temp is not None: + if isinstance(current_temp, (int, float)): self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) - if target_temp is not None: + if isinstance(target_temp, (int, float)): if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) From 8a6370f7c91fec6c220bc2e438a236816c636341 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 13 Mar 2018 17:12:28 -0400 Subject: [PATCH 019/924] Revert throttle Arlo api calls (#13174) --- homeassistant/components/arlo.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/arlo.py b/homeassistant/components/arlo.py index 77201e5ead9..7e51ec8c045 100644 --- a/homeassistant/components/arlo.py +++ b/homeassistant/components/arlo.py @@ -5,13 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/arlo/ """ import logging -from datetime import timedelta import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.helpers import config_validation as cv -from homeassistant.util import Throttle from homeassistant.const import CONF_USERNAME, CONF_PASSWORD REQUIREMENTS = ['pyarlo==0.1.2'] @@ -47,7 +45,6 @@ def setup(hass, config): arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: return False - arlo.update = Throttle(timedelta(seconds=10))(arlo.update) hass.data[DATA_ARLO] = arlo except (ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Netgear Arlo: %s", str(ex)) From 4e569ac0c3f1a24c3cad2a66e6f57581abc587f3 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:08:41 +0100 Subject: [PATCH 020/924] Ignore unsupported Sonos favorites (#13195) --- homeassistant/components/media_player/sonos.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 9ea33b4c396..ec9bdf34d56 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -426,7 +426,17 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = self.soco.play_mode self._night_sound = self.soco.night_mode self._speech_enhance = self.soco.dialog_mode - self._favorites = self.soco.music_library.get_sonos_favorites() + + self._favorites = [] + for fav in self.soco.music_library.get_sonos_favorites(): + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + # pylint: disable=broad-except + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) def _subscribe_to_player_events(self): """Add event subscriptions.""" From 30a1fedce8e8a736bae88b2186ef531fc9a5d99b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 14 Mar 2018 20:07:50 +0100 Subject: [PATCH 021/924] Avoid Sonos error when joining with self (#13196) --- homeassistant/components/media_player/sonos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index ec9bdf34d56..2a12b59e7c7 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -896,7 +896,8 @@ class SonosDevice(MediaPlayerDevice): self.soco.unjoin() for slave in slaves: - slave.soco.join(self.soco) + if slave.unique_id != self.unique_id: + slave.soco.join(self.soco) @soco_error() def unjoin(self): From 25fe6ec53672d493c4ace565fa60220ecb6c51a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 15:07:37 -0700 Subject: [PATCH 022/924] Fix input_boolean Google Assistant serialize error (#13220) --- .../components/google_assistant/smart_home.py | 22 ++++++++++++++----- .../google_assistant/test_smart_home.py | 15 +++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 48d24c00b97..834d40c367c 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -17,7 +17,16 @@ from homeassistant.core import callback from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES) from homeassistant.components import ( - switch, light, cover, media_player, group, fan, scene, script, climate, + climate, + cover, + fan, + group, + input_boolean, + light, + media_player, + scene, + script, + switch, ) from . import trait @@ -33,15 +42,16 @@ HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) DOMAIN_TO_GOOGLE_TYPES = { + climate.DOMAIN: TYPE_THERMOSTAT, + cover.DOMAIN: TYPE_SWITCH, + fan.DOMAIN: TYPE_SWITCH, group.DOMAIN: TYPE_SWITCH, + input_boolean.DOMAIN: TYPE_SWITCH, + light.DOMAIN: TYPE_LIGHT, + media_player.DOMAIN: TYPE_SWITCH, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, - fan.DOMAIN: TYPE_SWITCH, - light.DOMAIN: TYPE_LIGHT, - cover.DOMAIN: TYPE_SWITCH, - media_player.DOMAIN: TYPE_SWITCH, - climate.DOMAIN: TYPE_THERMOSTAT, } diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8d139fa8211..24d74afa6da 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,5 @@ """Test Google Smart Home.""" +from homeassistant.core import State from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from homeassistant.setup import async_setup_component @@ -244,3 +245,17 @@ async def test_raising_error_trait(hass): }] } } + + +def test_serialize_input_boolean(): + """Test serializing an input boolean entity.""" + state = State('input_boolean.bla', 'on') + entity = sh._GoogleEntity(None, BASIC_CONFIG, state) + assert entity.sync_serialize() == { + 'id': 'input_boolean.bla', + 'attributes': {}, + 'name': {'name': 'bla'}, + 'traits': ['action.devices.traits.OnOff'], + 'type': 'action.devices.types.SWITCH', + 'willReportState': False, + } From 8e05a5c12bd99484d7d8100263113e58ea30d766 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 15:08:34 -0700 Subject: [PATCH 023/924] Version bump to 0.65.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 12d988c552e..7e2b9f3061a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '4' +PATCH_VERSION = '5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 07f20676cbcd54771b39f62bb0e4aaa1fe8f4360 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Thu, 15 Mar 2018 01:03:40 +0200 Subject: [PATCH 024/924] Add notifications to downloader.py (#12961) * Update downloader.py Add persistent notification to alert when download is finished or in case of download failure. * Update downloader.py * Update downloader.py * Update downloader.py * Fire and event when download is requested Added 2 events to represent download completed and download failed. This will allow the user to trigger an automation based on the status of the download. * Update downloader.py * Update downloader.py replaced . with _ * Update downloader.py fixed linting errors --- homeassistant/components/downloader.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/downloader.py b/homeassistant/components/downloader.py index b7354b4f0a7..0d57740a83d 100644 --- a/homeassistant/components/downloader.py +++ b/homeassistant/components/downloader.py @@ -25,6 +25,8 @@ ATTR_OVERWRITE = 'overwrite' CONF_DOWNLOAD_DIR = 'download_dir' DOMAIN = 'downloader' +DOWNLOAD_FAILED_EVENT = 'download_failed' +DOWNLOAD_COMPLETED_EVENT = 'download_completed' SERVICE_DOWNLOAD_FILE = 'download_file' @@ -133,9 +135,19 @@ def setup(hass, config): fil.write(chunk) _LOGGER.debug("Downloading of %s done", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), { + 'url': url, + 'filename': filename + }) except requests.exceptions.ConnectionError: _LOGGER.exception("ConnectionError occurred for %s", url) + hass.bus.fire( + "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), { + 'url': url, + 'filename': filename + }) # Remove file if we started downloading but failed if final_path and os.path.isfile(final_path): From be2e202618879f141be020a7d11685ed6624fb34 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 16:13:43 -0700 Subject: [PATCH 025/924] Bump frontend to 20180315.0 --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1f5a7576302..153d1f6564e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180310.0'] +REQUIREMENTS = ['home-assistant-frontend==20180315.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] From 64f18c62f4af12cc98e7754a43edbec711d46758 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 16:39:15 -0700 Subject: [PATCH 026/924] Update frontend --- 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 1733f0d9a7d..b35b3d0991a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -353,7 +353,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180310.0 +home-assistant-frontend==20180315.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c785bee3af..9def3a7b301 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180310.0 +home-assistant-frontend==20180315.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d348f09d3d42c6bd65727a471cf363cbca19105f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 15 Mar 2018 02:48:21 +0100 Subject: [PATCH 027/924] HomeKit Restructure (new config options) (#12997) * Restructure * Pincode will now be autogenerated and display using a persistence notification * Added 'homekit.start' service * Added config options * Renamed files for types * Improved tests * Changes (based on feedback) * Removed CONF_PIN_CODE * Added services.yaml * Service will only be registered if auto_start=False * Bugfix names, changed default port * Generate aids with zlib.adler32 * Added entity filter, minor changes * Small changes --- homeassistant/components/homekit/__init__.py | 211 ++++++++------ .../components/homekit/accessories.py | 89 +++--- homeassistant/components/homekit/const.py | 30 +- .../components/homekit/services.yaml | 4 + .../homekit/{covers.py => type_covers.py} | 27 +- ...ty_systems.py => type_security_systems.py} | 30 +- .../homekit/{sensors.py => type_sensors.py} | 11 +- .../homekit/{switches.py => type_switches.py} | 23 +- .../{thermostats.py => type_thermostats.py} | 113 ++++---- homeassistant/components/homekit/util.py | 46 ++++ tests/components/homekit/__init__.py | 1 - tests/components/homekit/test_accessories.py | 260 +++++++++--------- .../homekit/test_get_accessories.py | 142 +++++++--- tests/components/homekit/test_homekit.py | 214 +++++++++----- tests/components/homekit/test_switches.py | 64 ----- .../{test_covers.py => test_type_covers.py} | 15 +- ...stems.py => test_type_security_systems.py} | 36 ++- .../{test_sensors.py => test_type_sensors.py} | 30 +- .../components/homekit/test_type_switches.py | 104 +++++++ ...hermostats.py => test_type_thermostats.py} | 87 +++++- tests/components/homekit/test_util.py | 83 ++++++ tests/mock/homekit.py | 133 --------- 22 files changed, 1038 insertions(+), 715 deletions(-) create mode 100644 homeassistant/components/homekit/services.yaml rename homeassistant/components/homekit/{covers.py => type_covers.py} (77%) rename homeassistant/components/homekit/{security_systems.py => type_security_systems.py} (80%) rename homeassistant/components/homekit/{sensors.py => type_sensors.py} (84%) rename homeassistant/components/homekit/{switches.py => type_switches.py} (72%) rename homeassistant/components/homekit/{thermostats.py => type_thermostats.py} (71%) create mode 100644 homeassistant/components/homekit/util.py delete mode 100644 tests/components/homekit/__init__.py delete mode 100644 tests/components/homekit/test_switches.py rename tests/components/homekit/{test_covers.py => test_type_covers.py} (87%) rename tests/components/homekit/{test_security_systems.py => test_type_security_systems.py} (72%) rename tests/components/homekit/{test_sensors.py => test_type_sensors.py} (64%) create mode 100644 tests/components/homekit/test_type_switches.py rename tests/components/homekit/{test_thermostats.py => test_type_thermostats.py} (65%) create mode 100644 tests/components/homekit/test_util.py delete mode 100644 tests/mock/homekit.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ad70740536e..63013bd8fc9 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,154 +3,199 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ -import asyncio import logging -import re +from zlib import adler32 import voluptuous as vol -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, - TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.cover import SUPPORT_SET_POSITION +from homeassistant.const import ( + ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry +from .const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, + DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START) +from .util import ( + validate_entity_config, show_setup_message) TYPES = Registry() _LOGGER = logging.getLogger(__name__) -_RE_VALID_PINCODE = r"^(\d{3}-\d{2}-\d{3})$" - -DOMAIN = 'homekit' REQUIREMENTS = ['HAP-python==1.1.7'] -BRIDGE_NAME = 'Home Assistant' -CONF_PIN_CODE = 'pincode' - -HOMEKIT_FILE = '.homekit.state' - - -def valid_pin(value): - """Validate pin code value.""" - match = re.match(_RE_VALID_PINCODE, str(value).strip()) - if not match: - raise vol.Invalid("Pin must be in the format: '123-45-678'") - return match.group(0) - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ - vol.Optional(CONF_PORT, default=51826): vol.Coerce(int), - vol.Optional(CONF_PIN_CODE, default='123-45-678'): valid_pin, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, + vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, + vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, }) }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Setup the HomeKit component.""" - _LOGGER.debug("Begin setup HomeKit") + _LOGGER.debug('Begin setup HomeKit') conf = config[DOMAIN] - port = conf.get(CONF_PORT) - pin = str.encode(conf.get(CONF_PIN_CODE)) + port = conf[CONF_PORT] + auto_start = conf[CONF_AUTO_START] + entity_filter = conf[CONF_FILTER] + entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port) - homekit.setup_bridge(pin) + homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit.setup() + + if auto_start: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) + return True + + def handle_homekit_service_start(service): + """Handle start HomeKit service call.""" + if homekit.started: + _LOGGER.warning('HomeKit is already running') + return + homekit.start() + + hass.services.async_register(DOMAIN, SERVICE_HOMEKIT_START, + handle_homekit_service_start) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, homekit.start_driver) return True -def import_types(): - """Import all types from files in the HomeKit directory.""" - _LOGGER.debug("Import type files.") - # pylint: disable=unused-variable - from . import ( # noqa F401 - covers, security_systems, sensors, switches, thermostats) - - -def get_accessory(hass, state): +def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" + _LOGGER.debug('%s: ') + if not aid: + _LOGGER.warning('The entitiy "%s" is not supported, since it ' + 'generates an invalid aid, please change it.', + state.entity_id) + return None + if state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: - _LOGGER.debug("Add \"%s\" as \"%s\"", + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, - state.name) + state.name, aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position - if state.attributes.get(ATTR_SUPPORTED_FEATURES) & 4: - _LOGGER.debug("Add \"%s\" as \"%s\"", - state.entity_id, 'Window') - return TYPES['Window'](hass, state.entity_id, state.name) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & SUPPORT_SET_POSITION: + _LOGGER.debug('Add "%s" as "%s"', + state.entity_id, 'WindowCovering') + return TYPES['WindowCovering'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'alarm_control_panel': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') - return TYPES['SecuritySystem'](hass, state.entity_id, state.name) + return TYPES['SecuritySystem'](hass, state.entity_id, state.name, + alarm_code=config[ATTR_CODE], aid=aid) elif state.domain == 'climate': - support_auto = False - features = state.attributes.get(ATTR_SUPPORTED_FEATURES) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE_HIGH # Check if climate device supports auto mode - if (features & SUPPORT_TARGET_TEMPERATURE_HIGH) \ - and (features & SUPPORT_TARGET_TEMPERATURE_LOW): - support_auto = True - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Thermostat') + support_auto = bool(features & support_temp_range) + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat') return TYPES['Thermostat'](hass, state.entity_id, - state.name, support_auto) + state.name, support_auto, aid=aid) elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean': - _LOGGER.debug("Add \"%s\" as \"%s\"", state.entity_id, 'Switch') - return TYPES['Switch'](hass, state.entity_id, state.name) + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') + return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) + _LOGGER.warning('The entity "%s" is not supported yet', + state.entity_id) return None +def generate_aid(entity_id): + """Generate accessory aid with zlib adler32.""" + aid = adler32(entity_id.encode('utf-8')) + if aid == 0 or aid == 1: + return None + return aid + + class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port): + def __init__(self, hass, port, entity_filter, entity_config): """Initialize a HomeKit object.""" self._hass = hass self._port = port + self._filter = entity_filter + self._config = entity_config + self.started = False + self.bridge = None self.driver = None - def setup_bridge(self, pin): - """Setup the bridge component to track all accessories.""" - from .accessories import HomeBridge - self.bridge = HomeBridge(BRIDGE_NAME, 'homekit.bridge', pin) + def setup(self): + """Setup bridge and accessory driver.""" + from .accessories import HomeBridge, HomeDriver - def start_driver(self, event): - """Start the accessory driver.""" - from pyhap.accessory_driver import AccessoryDriver - self._hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, self.stop_driver) + self._hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self.stop) - import_types() - _LOGGER.debug("Start adding accessories.") - for state in self._hass.states.all(): - acc = get_accessory(self._hass, state) - if acc is not None: - self.bridge.add_accessory(acc) - - ip_address = get_local_ip() path = self._hass.config.path(HOMEKIT_FILE) - self.driver = AccessoryDriver(self.bridge, self._port, - ip_address, path) - _LOGGER.debug("Driver started") + self.bridge = HomeBridge(self._hass) + self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + + def add_bridge_accessory(self, state): + """Try adding accessory to bridge if configured beforehand.""" + if not state or not self._filter(state.entity_id): + return + aid = generate_aid(state.entity_id) + conf = self._config.pop(state.entity_id, {}) + acc = get_accessory(self._hass, state, aid, conf) + if acc is not None: + self.bridge.add_accessory(acc) + + def start(self, *args): + """Start the accessory driver.""" + if self.started: + return + self.started = True + + # pylint: disable=unused-variable + from . import ( # noqa F401 + type_covers, type_security_systems, type_sensors, + type_switches, type_thermostats) + + for state in self._hass.states.all(): + self.add_bridge_accessory(state) + for entity_id in self._config: + _LOGGER.warning('The entity "%s" was not setup when HomeKit ' + 'was started', entity_id) + self.bridge.set_broker(self.driver) + + if not self.bridge.paired: + show_setup_message(self.bridge, self._hass) + + _LOGGER.debug('Driver start') self.driver.start() - def stop_driver(self, event): + def stop(self, *args): """Stop the accessory driver.""" - _LOGGER.debug("Driver stop") - if self.driver is not None: + if not self.started: + return + + _LOGGER.debug('Driver stop') + if self.driver and self.driver.run_sentinel: self.driver.stop() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 1cd94070289..0af25bc4453 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -2,15 +2,31 @@ import logging from pyhap.accessory import Accessory, Bridge, Category +from pyhap.accessory_driver import AccessoryDriver from .const import ( - SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, MANUFACTURER, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) - + ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, + MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) +from .util import ( + show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) +def add_preload_service(acc, service, chars=None): + """Define and return a service to be available for the accessory.""" + from pyhap.loader import get_serv_loader, get_char_loader + service = get_serv_loader().get(service) + if chars: + chars = chars if isinstance(chars, list) else [chars] + for char_name in chars: + char = get_char_loader().get(char_name) + service.add_characteristic(char) + acc.add_service(service) + return service + + def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, serial_number='0000'): """Set the default accessory information.""" @@ -21,36 +37,23 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) -def add_preload_service(acc, service, chars=None, opt_chars=None): - """Define and return a service to be available for the accessory.""" - from pyhap.loader import get_serv_loader, get_char_loader - service = get_serv_loader().get(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - char = get_char_loader().get(char_name) - service.add_characteristic(char) - if opt_chars: - opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] - for opt_char_name in opt_chars: - opt_char = get_char_loader().get(opt_char_name) - service.add_opt_characteristic(opt_char) - acc.add_service(service) - return service +def override_properties(char, properties=None, valid_values=None): + """Override characteristic property values and valid values.""" + if properties: + char.properties.update(properties) - -def override_properties(char, new_properties): - """Override characteristic property values.""" - char.properties.update(new_properties) + if valid_values: + char.properties['ValidValues'].update(valid_values) class HomeAccessory(Accessory): - """Class to extend the Accessory class.""" + """Adapter class for Accessory.""" - def __init__(self, display_name, model, category='OTHER', **kwargs): + def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, + category='OTHER', **kwargs): """Initialize a Accessory object.""" - super().__init__(display_name, **kwargs) - set_accessory_info(self, display_name, model) + super().__init__(name, **kwargs) + set_accessory_info(self, name, model) self.category = getattr(Category, category, Category.OTHER) def _set_services(self): @@ -58,13 +61,37 @@ class HomeAccessory(Accessory): class HomeBridge(Bridge): - """Class to extend the Bridge class.""" + """Adapter class for Bridge.""" - def __init__(self, display_name, model, pincode, **kwargs): + def __init__(self, hass, name=BRIDGE_NAME, + model=BRIDGE_MODEL, **kwargs): """Initialize a Bridge object.""" - super().__init__(display_name, pincode=pincode, **kwargs) - set_accessory_info(self, display_name, model) + super().__init__(name, **kwargs) + set_accessory_info(self, name, model) + self._hass = hass def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) add_preload_service(self, SERV_BRIDGING_STATE) + + def setup_message(self): + """Prevent print of pyhap setup message to terminal.""" + pass + + def add_paired_client(self, client_uuid, client_public): + """Override super function to dismiss setup message if paired.""" + super().add_paired_client(client_uuid, client_public) + dismiss_setup_message(self._hass) + + def remove_paired_client(self, client_uuid): + """Override super function to show setup message if unpaired.""" + super().remove_paired_client(client_uuid) + show_setup_message(self, self._hass) + + +class HomeDriver(AccessoryDriver): + """Adapter class for AccessoryDriver.""" + + def __init__(self, *args, **kwargs): + """Initialize a AccessoryDriver object.""" + super().__init__(*args, **kwargs) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 73dfbf69049..d2b1caffe53 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,7 +1,30 @@ """Constants used be the HomeKit component.""" +# #### MISC #### +DOMAIN = 'homekit' +HOMEKIT_FILE = '.homekit.state' +HOMEKIT_NOTIFY_ID = 4663548 + +# #### CONFIG #### +CONF_AUTO_START = 'auto_start' +CONF_ENTITY_CONFIG = 'entity_config' +CONF_FILTER = 'filter' + +# #### CONFIG DEFAULTS #### +DEFAULT_AUTO_START = True +DEFAULT_PORT = 51827 + +# #### HOMEKIT COMPONENT SERVICES #### +SERVICE_HOMEKIT_START = 'start' + +# #### STRING CONSTANTS #### +ACCESSORY_MODEL = 'homekit.accessory' +ACCESSORY_NAME = 'Home Accessory' +BRIDGE_MODEL = 'homekit.bridge' +BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' -# Services + +# #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_BRIDGING_STATE = 'BridgingState' SERV_SECURITY_SYSTEM = 'SecuritySystem' @@ -10,7 +33,8 @@ SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# Characteristics + +# #### Characteristics #### CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' @@ -33,5 +57,5 @@ CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' -# Properties +# #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} diff --git a/homeassistant/components/homekit/services.yaml b/homeassistant/components/homekit/services.yaml new file mode 100644 index 00000000000..e30e71301b3 --- /dev/null +++ b/homeassistant/components/homekit/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available HomeKit services + +start: + description: Starts the HomeKit component driver. diff --git a/homeassistant/components/homekit/covers.py b/homeassistant/components/homekit/type_covers.py similarity index 77% rename from homeassistant/components/homekit/covers.py rename to homeassistant/components/homekit/type_covers.py index 47713f6c630..0110bff3185 100644 --- a/homeassistant/components/homekit/covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -14,16 +14,17 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -@TYPES.register('Window') -class Window(HomeAccessory): +@TYPES.register('WindowCovering') +class WindowCovering(HomeAccessory): """Generate a Window accessory for a cover entity. The cover entity must support: set_cover_position. """ - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a Window accessory object.""" - super().__init__(display_name, entity_id, 'WINDOW') + super().__init__(display_name, entity_id, 'WINDOW_COVERING', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -31,12 +32,12 @@ class Window(HomeAccessory): self.current_position = None self.homekit_target = None - self.serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = self.serv_cover. \ + serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) + self.char_current_position = serv_cover. \ get_characteristic(CHAR_CURRENT_POSITION) - self.char_target_position = self.serv_cover. \ + self.char_target_position = serv_cover. \ get_characteristic(CHAR_TARGET_POSITION) - self.char_position_state = self.serv_cover. \ + self.char_position_state = serv_cover. \ get_characteristic(CHAR_POSITION_STATE) self.char_current_position.value = 0 self.char_target_position.value = 0 @@ -55,15 +56,14 @@ class Window(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" if value != self.current_position: - _LOGGER.debug("%s: Set position to %d", self._entity_id, value) + _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value if value > self.current_position: self.char_position_state.set_value(1) elif value < self.current_position: self.char_position_state.set_value(0) - self._hass.services.call( - 'cover', 'set_cover_position', - {'entity_id': self._entity_id, 'position': value}) + self._hass.components.cover.set_cover_position( + value, self._entity_id) def update_cover_position(self, entity_id=None, old_state=None, new_state=None): @@ -71,9 +71,10 @@ class Window(HomeAccessory): if new_state is None: return - current_position = new_state.attributes[ATTR_CURRENT_POSITION] + current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if current_position is None: return + self.current_position = int(current_position) self.char_current_position.set_value(self.current_position) diff --git a/homeassistant/components/homekit/security_systems.py b/homeassistant/components/homekit/type_security_systems.py similarity index 80% rename from homeassistant/components/homekit/security_systems.py rename to homeassistant/components/homekit/type_security_systems.py index 1b8f0a6820b..02742acb75d 100644 --- a/homeassistant/components/homekit/security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -28,9 +28,11 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, hass, entity_id, display_name, alarm_code=None): + def __init__(self, hass, entity_id, display_name, + alarm_code, *args, **kwargs): """Initialize a SecuritySystem accessory object.""" - super().__init__(display_name, entity_id, 'ALARM_SYSTEM') + super().__init__(display_name, entity_id, 'ALARM_SYSTEM', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -38,11 +40,11 @@ class SecuritySystem(HomeAccessory): self.flag_target_state = False - self.service_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = self.service_alarm. \ + serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) + self.char_current_state = serv_alarm. \ get_characteristic(CHAR_CURRENT_SECURITY_STATE) self.char_current_state.value = 3 - self.char_target_state = self.service_alarm. \ + self.char_target_state = serv_alarm. \ get_characteristic(CHAR_TARGET_SECURITY_STATE) self.char_target_state.value = 3 @@ -58,15 +60,13 @@ class SecuritySystem(HomeAccessory): def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set security state to %d", + _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id} - if self._alarm_code is not None: - params[ATTR_CODE] = self._alarm_code + params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} self._hass.services.call('alarm_control_panel', service, params) def update_security_state(self, entity_id=None, @@ -78,15 +78,15 @@ class SecuritySystem(HomeAccessory): hass_state = new_state.state if hass_state not in HASS_TO_HOMEKIT: return + current_security_state = HASS_TO_HOMEKIT[hass_state] - self.char_current_state.set_value(current_security_state) - _LOGGER.debug("%s: Updated current state to %s (%d)", - self._entity_id, hass_state, - current_security_state) + self.char_current_state.set_value(current_security_state, + should_callback=False) + _LOGGER.debug('%s: Updated current state to %s (%d)', + self._entity_id, hass_state, current_security_state) if not self.flag_target_state: self.char_target_state.set_value(current_security_state, should_callback=False) - elif self.char_target_state.get_value() \ - == self.char_current_state.get_value(): + if self.char_target_state.value == self.char_current_state.value: self.flag_target_state = False diff --git a/homeassistant/components/homekit/sensors.py b/homeassistant/components/homekit/type_sensors.py similarity index 84% rename from homeassistant/components/homekit/sensors.py rename to homeassistant/components/homekit/type_sensors.py index 40f97ae3ef7..286862343f4 100644 --- a/homeassistant/components/homekit/sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -36,16 +36,15 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(display_name, entity_id, 'SENSOR') + super().__init__(display_name, entity_id, 'SENSOR', *args, **kwargs) self._hass = hass self._entity_id = entity_id - self.serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = self.serv_temp. \ - get_characteristic(CHAR_CURRENT_TEMPERATURE) + serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) + self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) override_properties(self.char_temp, PROP_CELSIUS) self.char_temp.value = 0 self.unit = None @@ -68,5 +67,5 @@ class TemperatureSensor(HomeAccessory): temperature = calc_temperature(new_state.state, unit) if temperature is not None: self.char_temp.set_value(temperature) - _LOGGER.debug("%s: Current temperature set to %d°C", + _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) diff --git a/homeassistant/components/homekit/switches.py b/homeassistant/components/homekit/type_switches.py similarity index 72% rename from homeassistant/components/homekit/switches.py rename to homeassistant/components/homekit/type_switches.py index 876b3406d28..989bf4e19f5 100644 --- a/homeassistant/components/homekit/switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,7 +1,8 @@ """Class to hold all switch accessories.""" import logging -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from homeassistant.helpers.event import async_track_state_change @@ -16,9 +17,9 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, hass, entity_id, display_name): + def __init__(self, hass, entity_id, display_name, *args, **kwargs): """Initialize a Switch accessory object to represent a remote.""" - super().__init__(display_name, entity_id, 'SWITCH') + super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -26,8 +27,8 @@ class Switch(HomeAccessory): self.flag_target_state = False - self.service_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = self.service_switch.get_characteristic(CHAR_ON) + serv_switch = add_preload_service(self, SERV_SWITCH) + self.char_on = serv_switch.get_characteristic(CHAR_ON) self.char_on.value = False self.char_on.setter_callback = self.set_state @@ -41,10 +42,10 @@ class Switch(HomeAccessory): def set_state(self, value): """Move switch state to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set switch state to %s", + _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True - service = 'turn_on' if value else 'turn_off' + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) @@ -53,10 +54,10 @@ class Switch(HomeAccessory): if new_state is None: return - current_state = (new_state.state == 'on') + current_state = (new_state.state == STATE_ON) if not self.flag_target_state: - _LOGGER.debug("%s: Set current state to %s", + _LOGGER.debug('%s: Set current state to %s', self._entity_id, current_state) self.char_on.set_value(current_state, should_callback=False) - else: - self.flag_target_state = False + + self.flag_target_state = False diff --git a/homeassistant/components/homekit/thermostats.py b/homeassistant/components/homekit/type_thermostats.py similarity index 71% rename from homeassistant/components/homekit/thermostats.py rename to homeassistant/components/homekit/type_thermostats.py index 6d342273e8d..6e720c2214e 100644 --- a/homeassistant/components/homekit/thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,8 +7,7 @@ from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from homeassistant.helpers.event import async_track_state_change from . import TYPES @@ -33,9 +32,11 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, hass, entity_id, display_name, support_auto=False): + def __init__(self, hass, entity_id, display_name, + support_auto, *args, **kwargs): """Initialize a Thermostat accessory object.""" - super().__init__(display_name, entity_id, 'THERMOSTAT') + super().__init__(display_name, entity_id, 'THERMOSTAT', + *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -46,48 +47,47 @@ class Thermostat(HomeAccessory): self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False - extra_chars = None # Add additional characteristics if auto mode is supported - if support_auto: - extra_chars = [CHAR_COOLING_THRESHOLD_TEMPERATURE, - CHAR_HEATING_THRESHOLD_TEMPERATURE] + extra_chars = [ + CHAR_COOLING_THRESHOLD_TEMPERATURE, + CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None # Preload the thermostat service - self.service_thermostat = add_preload_service(self, SERV_THERMOSTAT, - extra_chars) + serv_thermostat = add_preload_service(self, SERV_THERMOSTAT, + extra_chars) # Current and target mode characteristics - self.char_current_heat_cool = self.service_thermostat. \ + self.char_current_heat_cool = serv_thermostat. \ get_characteristic(CHAR_CURRENT_HEATING_COOLING) self.char_current_heat_cool.value = 0 - self.char_target_heat_cool = self.service_thermostat. \ + self.char_target_heat_cool = serv_thermostat. \ get_characteristic(CHAR_TARGET_HEATING_COOLING) self.char_target_heat_cool.value = 0 self.char_target_heat_cool.setter_callback = self.set_heat_cool # Current and target temperature characteristics - self.char_current_temp = self.service_thermostat. \ + self.char_current_temp = serv_thermostat. \ get_characteristic(CHAR_CURRENT_TEMPERATURE) self.char_current_temp.value = 21.0 - self.char_target_temp = self.service_thermostat. \ + self.char_target_temp = serv_thermostat. \ get_characteristic(CHAR_TARGET_TEMPERATURE) self.char_target_temp.value = 21.0 self.char_target_temp.setter_callback = self.set_target_temperature # Display units characteristic - self.char_display_units = self.service_thermostat. \ + self.char_display_units = serv_thermostat. \ get_characteristic(CHAR_TEMP_DISPLAY_UNITS) self.char_display_units.value = 0 # If the device supports it: high and low temperature characteristics if support_auto: - self.char_cooling_thresh_temp = self.service_thermostat. \ + self.char_cooling_thresh_temp = serv_thermostat. \ get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE) self.char_cooling_thresh_temp.value = 23.0 self.char_cooling_thresh_temp.setter_callback = \ self.set_cooling_threshold - self.char_heating_thresh_temp = self.service_thermostat. \ + self.char_heating_thresh_temp = serv_thermostat. \ get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE) self.char_heating_thresh_temp.value = 19.0 self.char_heating_thresh_temp.setter_callback = \ @@ -107,47 +107,40 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: - _LOGGER.debug("%s: Set heat-cool to %d", self._entity_id, value) + _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] - self._hass.services.call('climate', 'set_operation_mode', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_OPERATION_MODE: hass_value}) + self._hass.components.climate.set_operation_mode( + operation_mode=hass_value, entity_id=self._entity_id) def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set cooling threshold temperature to %.2f", + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', self._entity_id, value) self.coolingthresh_flag_target_state = True - low = self.char_heating_thresh_temp.get_value() - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TARGET_TEMP_HIGH: value, - ATTR_TARGET_TEMP_LOW: low}) + low = self.char_heating_thresh_temp.value + self._hass.components.climate.set_temperature( + entity_id=self._entity_id, target_temp_high=value, + target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set heating threshold temperature to %.2f", + _LOGGER.debug('%s: Set heating threshold temperature to %.2f', self._entity_id, value) self.heatingthresh_flag_target_state = True # Home assistant always wants to set low and high at the same time - high = self.char_cooling_thresh_temp.get_value() - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TARGET_TEMP_LOW: value, - ATTR_TARGET_TEMP_HIGH: high}) + high = self.char_cooling_thresh_temp.value + self._hass.components.climate.set_temperature( + entity_id=self._entity_id, target_temp_high=high, + target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug("%s: Set target temperature to %.2f", + _LOGGER.debug('%s: Set target temperature to %.2f', self._entity_id, value) self.temperature_flag_target_state = True - self._hass.services.call( - 'climate', 'set_temperature', - {ATTR_ENTITY_ID: self._entity_id, - ATTR_TEMPERATURE: value}) + self._hass.components.climate.set_temperature( + temperature=value, entity_id=self._entity_id) def update_thermostat(self, entity_id=None, old_state=None, new_state=None): @@ -166,62 +159,58 @@ class Thermostat(HomeAccessory): if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) - else: - self.temperature_flag_target_state = False + self.temperature_flag_target_state = False # Update cooling threshold temperature if characteristic exists - if self.char_cooling_thresh_temp is not None: + if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh is not None: + if cooling_thresh: if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) - else: - self.coolingthresh_flag_target_state = False + self.coolingthresh_flag_target_state = False # Update heating threshold temperature if characteristic exists - if self.char_heating_thresh_temp is not None: + if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh is not None: + if heating_thresh: if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) - else: - self.heatingthresh_flag_target_state = False + self.heatingthresh_flag_target_state = False # Update display units display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units is not None \ + if display_units \ and display_units in UNIT_HASS_TO_HOMEKIT: self.char_display_units.set_value( UNIT_HASS_TO_HOMEKIT[display_units]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode is not None \ + if operation_mode \ and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False) - else: - self.heat_cool_flag_target_state = False + self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode if operation_mode == STATE_HEAT: - if current_temp < target_temp: + if isinstance(target_temp, float) and current_temp < target_temp: current_operation_mode = STATE_HEAT else: current_operation_mode = STATE_OFF elif operation_mode == STATE_COOL: - if current_temp > target_temp: + if isinstance(target_temp, float) and current_temp > target_temp: current_operation_mode = STATE_COOL else: current_operation_mode = STATE_OFF elif operation_mode == STATE_AUTO: # Check if auto is supported - if self.char_cooling_thresh_temp is not None: - lower_temp = self.char_heating_thresh_temp.get_value() - upper_temp = self.char_cooling_thresh_temp.get_value() + if self.char_cooling_thresh_temp: + lower_temp = self.char_heating_thresh_temp.value + upper_temp = self.char_cooling_thresh_temp.value if current_temp < lower_temp: current_operation_mode = STATE_HEAT elif current_temp > upper_temp: @@ -232,9 +221,11 @@ class Thermostat(HomeAccessory): # Check if heating or cooling are supported heat = STATE_HEAT in new_state.attributes[ATTR_OPERATION_LIST] cool = STATE_COOL in new_state.attributes[ATTR_OPERATION_LIST] - if current_temp < target_temp and heat: + if isinstance(target_temp, float) and \ + current_temp < target_temp and heat: current_operation_mode = STATE_HEAT - elif current_temp > target_temp and cool: + elif isinstance(target_temp, float) and \ + current_temp > target_temp and cool: current_operation_mode = STATE_COOL else: current_operation_mode = STATE_OFF diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py new file mode 100644 index 00000000000..f18eb2273db --- /dev/null +++ b/homeassistant/components/homekit/util.py @@ -0,0 +1,46 @@ +"""Collection of useful functions for the HomeKit component.""" +import logging + +import voluptuous as vol + +from homeassistant.core import split_entity_id +from homeassistant.const import ( + ATTR_CODE) +import homeassistant.helpers.config_validation as cv +from .const import HOMEKIT_NOTIFY_ID + +_LOGGER = logging.getLogger(__name__) + + +def validate_entity_config(values): + """Validate config entry for CONF_ENTITY.""" + entities = {} + for key, config in values.items(): + entity = cv.entity_id(key) + params = {} + if not isinstance(config, dict): + raise vol.Invalid('The configuration for "{}" must be ' + ' an dictionary.'.format(entity)) + + domain, _ = split_entity_id(entity) + + if domain == 'alarm_control_panel': + code = config.get(ATTR_CODE) + params[ATTR_CODE] = cv.string(code) if code else None + + entities[entity] = params + return entities + + +def show_setup_message(bridge, hass): + """Display persistent notification with setup information.""" + pin = bridge.pincode.decode() + message = 'To setup Home Assistant in the Home App, enter the ' \ + 'following code:\n### {}'.format(pin) + hass.components.persistent_notification.create( + message, 'HomeKit Setup', HOMEKIT_NOTIFY_ID) + + +def dismiss_setup_message(hass): + """Dismiss persistent notification and remove QR code.""" + hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) diff --git a/tests/components/homekit/__init__.py b/tests/components/homekit/__init__.py deleted file mode 100644 index 61a60cee2ac..00000000000 --- a/tests/components/homekit/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The tests for the homekit component.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6f39a8c792b..4d230b81686 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,166 +2,164 @@ This includes tests for all mock object types. """ - -from unittest.mock import patch - -# pylint: disable=unused-import -from pyhap.loader import get_serv_loader, get_char_loader # noqa F401 +import unittest +from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - set_accessory_info, add_preload_service, override_properties, - HomeAccessory, HomeBridge) + add_preload_service, set_accessory_info, override_properties, + HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( + ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MODEL, CHAR_MANUFACTURER, CHAR_NAME, CHAR_SERIAL_NUMBER) - -from tests.mock.homekit import ( - get_patch_paths, mock_preload_service, - MockTypeLoader, MockAccessory, MockService, MockChar) - -PATH_SERV = 'pyhap.loader.get_serv_loader' -PATH_CHAR = 'pyhap.loader.get_char_loader' -PATH_ACC, _ = get_patch_paths() + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) -@patch(PATH_CHAR, return_value=MockTypeLoader('char')) -@patch(PATH_SERV, return_value=MockTypeLoader('service')) -def test_add_preload_service(mock_serv, mock_char): - """Test method add_preload_service. +class TestAccessories(unittest.TestCase): + """Test pyhap adapter methods.""" - The methods 'get_serv_loader' and 'get_char_loader' are mocked. - """ - acc = MockAccessory('Accessory') - serv = add_preload_service(acc, 'TestService', - ['TestChar', 'TestChar2'], - ['TestOptChar', 'TestOptChar2']) + def test_add_preload_service(self): + """Test add_preload_service without additional characteristics.""" + acc = Mock() + serv = add_preload_service(acc, 'AirPurifier') + self.assertEqual(acc.mock_calls, [call.add_service(serv)]) + with self.assertRaises(AssertionError): + serv.get_characteristic('Name') - assert serv.display_name == 'TestService' - assert len(serv.characteristics) == 2 - assert len(serv.opt_characteristics) == 2 + # Test with typo in service name + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifierTypo') - acc.services = [] - serv = add_preload_service(acc, 'TestService') + # Test adding additional characteristic as string + serv = add_preload_service(Mock(), 'AirPurifier', 'Name') + serv.get_characteristic('Name') - assert not serv.characteristics - assert not serv.opt_characteristics + # Test adding additional characteristics as list + serv = add_preload_service(Mock(), 'AirPurifier', + ['Name', 'RotationSpeed']) + serv.get_characteristic('Name') + serv.get_characteristic('RotationSpeed') - acc.services = [] - serv = add_preload_service(acc, 'TestService', - 'TestChar', 'TestOptChar') + # Test adding additional characteristic with typo + with self.assertRaises(KeyError): + add_preload_service(Mock(), 'AirPurifier', 'NameTypo') - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 + def test_set_accessory_info(self): + """Test setting the basic accessory information.""" + # Test HomeAccessory + acc = HomeAccessory() + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') - assert serv.characteristics[0].display_name == 'TestChar' - assert serv.opt_characteristics[0].display_name == 'TestOptChar' + serv = acc.get_service(SERV_ACCESSORY_INFO) + self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') + self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + # Test HomeBridge + acc = HomeBridge(None) + set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') -def test_override_properties(): - """Test override of characteristic properties with MockChar.""" - char = MockChar('TestChar') - new_prop = {1: 'Test', 2: 'Demo'} - override_properties(char, new_prop) + serv = acc.get_service(SERV_ACCESSORY_INFO) + self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - assert char.properties == new_prop + def test_override_properties(self): + """Test overriding property values.""" + serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed') + char_active = serv.get_characteristic('Active') + char_rotation_speed = serv.get_characteristic('RotationSpeed') -def test_set_accessory_info(): - """Test setting of basic accessory information with MockAccessory.""" - acc = MockAccessory('Accessory') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + self.assertTrue( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 100) - assert len(acc.services) == 1 - serv = acc.services[0] + override_properties(char_active, valid_values={'State': 'On'}) + override_properties(char_rotation_speed, properties={'maxValue': 200}) - assert serv.display_name == SERV_ACCESSORY_INFO - assert len(serv.characteristics) == 4 - chars = serv.characteristics + self.assertFalse( + char_active.properties['ValidValues'].get('State') is None) + self.assertEqual(char_rotation_speed.properties['maxValue'], 200) - assert chars[0].display_name == CHAR_NAME - assert chars[0].value == 'name' - assert chars[1].display_name == CHAR_MODEL - assert chars[1].value == 'model' - assert chars[2].display_name == CHAR_MANUFACTURER - assert chars[2].value == 'manufacturer' - assert chars[3].display_name == CHAR_SERIAL_NUMBER - assert chars[3].value == '0000' + def test_home_accessory(self): + """Test HomeAccessory class.""" + acc = HomeAccessory() + self.assertEqual(acc.display_name, ACCESSORY_NAME) + self.assertEqual(acc.category, 1) # Category.OTHER + self.assertEqual(len(acc.services), 1) + serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, ACCESSORY_MODEL) + acc = HomeAccessory('test_name', 'test_model', 'FAN', aid=2) + self.assertEqual(acc.display_name, 'test_name') + self.assertEqual(acc.category, 3) # Category.FAN + self.assertEqual(acc.aid, 2) + self.assertEqual(len(acc.services), 1) + serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'test_model') -@patch(PATH_ACC, side_effect=mock_preload_service) -def test_home_accessory(mock_pre_serv): - """Test initializing a HomeAccessory object.""" - acc = HomeAccessory('TestAccessory', 'test.accessory', 'WINDOW') + def test_home_bridge(self): + """Test HomeBridge class.""" + bridge = HomeBridge(None) + self.assertEqual(bridge.display_name, BRIDGE_NAME) + self.assertEqual(bridge.category, 2) # Category.BRIDGE + self.assertEqual(len(bridge.services), 2) + serv = bridge.services[0] # SERV_ACCESSORY_INFO + self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) + serv = bridge.services[1] # SERV_BRIDGING_STATE + self.assertEqual(serv.display_name, SERV_BRIDGING_STATE) - assert acc.display_name == 'TestAccessory' - assert acc.category == 13 # Category.WINDOW - assert len(acc.services) == 1 + bridge = HomeBridge('hass', 'test_name', 'test_model') + self.assertEqual(bridge.display_name, 'test_name') + self.assertEqual(len(bridge.services), 2) + serv = bridge.services[0] # SERV_ACCESSORY_INFO + self.assertEqual( + serv.get_characteristic(CHAR_MODEL).value, 'test_model') - serv = acc.services[0] - assert serv.display_name == SERV_ACCESSORY_INFO - char_model = serv.get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.accessory' + # setup_message + bridge.setup_message() + # add_paired_client + with patch('pyhap.accessory.Accessory.add_paired_client') \ + as mock_add_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + bridge.add_paired_client('client_uuid', 'client_public') -@patch(PATH_ACC, side_effect=mock_preload_service) -def test_home_bridge(mock_pre_serv): - """Test initializing a HomeBridge object.""" - bridge = HomeBridge('TestBridge', 'test.bridge', b'123-45-678') + self.assertEqual(mock_add_paired_client.call_args, + call('client_uuid', 'client_public')) + self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) - assert bridge.display_name == 'TestBridge' - assert bridge.pincode == b'123-45-678' - assert len(bridge.services) == 2 + # remove_paired_client + with patch('pyhap.accessory.Accessory.remove_paired_client') \ + as mock_remove_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + bridge.remove_paired_client('client_uuid') - assert bridge.services[0].display_name == SERV_ACCESSORY_INFO - assert bridge.services[1].display_name == SERV_BRIDGING_STATE + self.assertEqual( + mock_remove_paired_client.call_args, call('client_uuid')) + self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass')) - char_model = bridge.services[0].get_characteristic(CHAR_MODEL) - assert char_model.get_value() == 'test.bridge' + def test_home_driver(self): + """Test HomeDriver class.""" + bridge = HomeBridge(None) + ip_adress = '127.0.0.1' + port = 51826 + path = '.homekit.state' + with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ + as mock_driver: + HomeDriver(bridge, ip_adress, port, path) -def test_mock_accessory(): - """Test attributes and functions of a MockAccessory.""" - acc = MockAccessory('TestAcc') - serv = MockService('TestServ') - acc.add_service(serv) - - assert acc.display_name == 'TestAcc' - assert len(acc.services) == 1 - - assert acc.get_service('TestServ') == serv - assert acc.get_service('NewServ').display_name == 'NewServ' - assert len(acc.services) == 2 - - -def test_mock_service(): - """Test attributes and functions of a MockService.""" - serv = MockService('TestServ') - char = MockChar('TestChar') - opt_char = MockChar('TestOptChar') - serv.add_characteristic(char) - serv.add_opt_characteristic(opt_char) - - assert serv.display_name == 'TestServ' - assert len(serv.characteristics) == 1 - assert len(serv.opt_characteristics) == 1 - - assert serv.get_characteristic('TestChar') == char - assert serv.get_characteristic('TestOptChar') == opt_char - assert serv.get_characteristic('NewChar').display_name == 'NewChar' - assert len(serv.characteristics) == 2 - - -def test_mock_char(): - """Test attributes and functions of a MockChar.""" - def callback_method(value): - """Provide a callback options for 'set_value' method.""" - assert value == 'With callback' - - char = MockChar('TestChar') - char.set_value('Value') - - assert char.display_name == 'TestChar' - assert char.get_value() == 'Value' - - char.setter_callback = callback_method - char.set_value('With callback') + self.assertEqual( + mock_driver.call_args, call(bridge, ip_adress, port, path)) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e49674a7b9..6e1c67cf282 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,57 +1,113 @@ """Package to test the get_accessory method.""" -from unittest.mock import patch, MagicMock +import logging +import unittest +from unittest.mock import patch, Mock from homeassistant.core import State -from homeassistant.components.homekit import ( - TYPES, get_accessory, import_types) +from homeassistant.components.climate import ( + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, + TEMP_CELSIUS, TEMP_FAHRENHEIT) + +_LOGGER = logging.getLogger(__name__) + +CONFIG = {} -def test_import_types(): - """Test if all type files are imported correctly.""" - try: - import_types() - assert True - # pylint: disable=broad-except - except Exception: - assert False - - -def test_component_not_supported(): +def test_get_accessory_invalid(caplog): """Test with unsupported component.""" - state = State('demo.unsupported', STATE_UNKNOWN) + assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ + is None + assert caplog.records[1].levelname == 'WARNING' - assert True if get_accessory(None, state) is None else False + assert get_accessory(None, State('test.test', 'on'), None, None) \ + is None + assert caplog.records[3].levelname == 'WARNING' -def test_sensor_temperature_celsius(): - """Test temperature sensor with Celsius as unit.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'TemperatureSensor': mock_type}): - state = State('sensor.temperature', '23', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 +class TestGetAccessories(unittest.TestCase): + """Methods to test the get_accessory method.""" + def setUp(self): + """Setup Mock type.""" + self.mock_type = Mock() -# pylint: disable=invalid-name -def test_sensor_temperature_fahrenheit(): - """Test temperature sensor with Fahrenheit as unit.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'TemperatureSensor': mock_type}): - state = State('sensor.temperature', '74', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 + def tearDown(self): + """Test if mock type was called.""" + self.assertTrue(self.mock_type.called) + def test_sensor_temperature_celsius(self): + """Test temperature sensor with Celsius as unit.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + get_accessory(None, state, 2, {}) -def test_cover_set_position(): - """Test cover with support for set_cover_position.""" - mock_type = MagicMock() - with patch.dict(TYPES, {'Window': mock_type}): - state = State('cover.set_position', 'open', - {ATTR_SUPPORTED_FEATURES: 4}) - get_accessory(None, state) - assert len(mock_type.mock_calls) == 1 + # pylint: disable=invalid-name + def test_sensor_temperature_fahrenheit(self): + """Test temperature sensor with Fahrenheit as unit.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '74', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + get_accessory(None, state, 2, {}) + + def test_cover_set_position(self): + """Test cover with support for set_cover_position.""" + with patch.dict(TYPES, {'WindowCovering': self.mock_type}): + state = State('cover.set_position', 'open', + {ATTR_SUPPORTED_FEATURES: 4}) + get_accessory(None, state, 2, {}) + + def test_alarm_control_panel(self): + """Test alarm control panel.""" + config = {ATTR_CODE: '1234'} + with patch.dict(TYPES, {'SecuritySystem': self.mock_type}): + state = State('alarm_control_panel.test', 'armed') + get_accessory(None, state, 2, config) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[1].get('alarm_code'), '1234') + + def test_climate(self): + """Test climate devices.""" + with patch.dict(TYPES, {'Thermostat': self.mock_type}): + state = State('climate.test', 'auto') + get_accessory(None, state, 2, {}) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[0][-1], False) # support_auto + + def test_climate_support_auto(self): + """Test climate devices with support for auto mode.""" + with patch.dict(TYPES, {'Thermostat': self.mock_type}): + state = State('climate.test', 'auto', { + ATTR_SUPPORTED_FEATURES: + SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH}) + get_accessory(None, state, 2, {}) + + # pylint: disable=unsubscriptable-object + self.assertEqual( + self.mock_type.call_args[0][-1], True) # support_auto + + def test_switch(self): + """Test switch.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('switch.test', 'on') + get_accessory(None, state, 2, {}) + + def test_remote(self): + """Test remote.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('remote.test', 'on') + get_accessory(None, state, 2, {}) + + def test_input_boolean(self): + """Test input_boolean.""" + with patch.dict(TYPES, {'Switch': self.mock_type}): + state = State('input_boolean.test', 'on') + get_accessory(None, state, 2, {}) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 58c197e69ec..c6d79545487 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,33 +1,22 @@ """Tests for the HomeKit component.""" - import unittest -from unittest.mock import call, patch, ANY - -import voluptuous as vol - -# pylint: disable=unused-import -from pyhap.accessory_driver import AccessoryDriver # noqa F401 +from unittest.mock import call, patch, ANY, Mock from homeassistant import setup -from homeassistant.core import Event -from homeassistant.components.homekit import ( - CONF_PIN_CODE, HOMEKIT_FILE, HomeKit, valid_pin) +from homeassistant.core import State +from homeassistant.components.homekit import HomeKit, generate_aid +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import ( + DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, + DEFAULT_PORT, SERVICE_HOMEKIT_START) +from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, PATH_HOMEKIT -PATH_ACC, _ = get_patch_paths() IP_ADDRESS = '127.0.0.1' - -CONFIG_MIN = {'homekit': {}} -CONFIG = { - 'homekit': { - CONF_PORT: 11111, - CONF_PIN_CODE: '987-65-432', - } -} +PATH_HOMEKIT = 'homeassistant.components.homekit' class TestHomeKit(unittest.TestCase): @@ -41,75 +30,162 @@ class TestHomeKit(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() - def test_validate_pincode(self): - """Test async_setup with invalid config option.""" - schema = vol.Schema(valid_pin) + def test_generate_aid(self): + """Test generate aid method.""" + aid = generate_aid('demo.entity') + self.assertIsInstance(aid, int) + self.assertTrue(aid >= 2 and aid <= 18446744073709551615) - for value in ('', '123-456-78', 'a23-45-678', '12345678', 1234): - with self.assertRaises(vol.MultipleInvalid): - schema(value) - - for value in ('123-45-678', '234-56-789'): - self.assertTrue(schema(value)) + with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: + mock_adler32.side_effect = [0, 1] + self.assertIsNone(generate_aid('demo.entity')) @patch(PATH_HOMEKIT + '.HomeKit') def test_setup_min(self, mock_homekit): - """Test async_setup with minimal config option.""" + """Test async_setup with min config options.""" self.assertTrue(setup.setup_component( - self.hass, 'homekit', CONFIG_MIN)) + self.hass, DOMAIN, {DOMAIN: {}})) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 51826), - call().setup_bridge(b'123-45-678')]) + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, DEFAULT_PORT, ANY, {}), + call().setup()]) + + # Test auto start enabled mock_homekit.reset_mock() + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + + self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) + + @patch(PATH_HOMEKIT + '.HomeKit') + def test_setup_auto_start_disabled(self, mock_homekit): + """Test async_setup with auto start disabled and test service calls.""" + mock_homekit.return_value = homekit = Mock() + + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + self.assertTrue(setup.setup_component( + self.hass, DOMAIN, config)) self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.block_till_done() - self.assertEqual(mock_homekit.mock_calls, - [call().start_driver(ANY)]) + self.assertEqual(mock_homekit.mock_calls, [ + call(self.hass, 11111, ANY, {}), + call().setup()]) - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_parameters(self, mock_homekit): - """Test async_setup with full config option.""" - self.assertTrue(setup.setup_component( - self.hass, 'homekit', CONFIG)) + # Test start call with driver stopped. + homekit.reset_mock() + homekit.configure_mock(**{'started': False}) - self.assertEqual(mock_homekit.mock_calls, - [call(self.hass, 11111), - call().setup_bridge(b'987-65-432')]) + self.hass.services.call('homekit', 'start') + self.assertEqual(homekit.mock_calls, [call.start()]) - @patch('pyhap.accessory_driver.AccessoryDriver') - def test_homekit_class(self, mock_acc_driver): - """Test interaction between the HomeKit class and pyhap.""" - with patch(PATH_HOMEKIT + '.accessories.HomeBridge') as mock_bridge: - homekit = HomeKit(self.hass, 51826) - homekit.setup_bridge(b'123-45-678') + # Test start call with driver started. + homekit.reset_mock() + homekit.configure_mock(**{'started': True}) - mock_bridge.reset_mock() - self.hass.states.set('demo.demo1', 'on') - self.hass.states.set('demo.demo2', 'off') + self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) + self.assertEqual(homekit.mock_calls, []) - with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc, \ - patch(PATH_HOMEKIT + '.import_types') as mock_import_types, \ + def test_homekit_setup(self): + """Test setup of bridge and driver.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: - mock_get_acc.side_effect = ['TempSensor', 'Window'] mock_ip.return_value = IP_ADDRESS - homekit.start_driver(Event(EVENT_HOMEASSISTANT_START)) + homekit.setup() path = self.hass.config.path(HOMEKIT_FILE) + self.assertTrue(isinstance(homekit.bridge, HomeBridge)) + self.assertEqual(mock_driver.mock_calls, [ + call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) - self.assertEqual(mock_import_types.call_count, 1) - self.assertEqual(mock_get_acc.call_count, 2) - self.assertEqual(mock_bridge.mock_calls, - [call().add_accessory('TempSensor'), - call().add_accessory('Window')]) - self.assertEqual(mock_acc_driver.mock_calls, - [call(homekit.bridge, 51826, IP_ADDRESS, path), - call().start()]) - mock_acc_driver.reset_mock() + # Test if stop listener is setup + self.assertEqual( + self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) - self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) - self.hass.block_till_done() + def test_homekit_add_accessory(self): + """Add accessory if config exists and get_acc returns an accessory.""" + homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit.bridge = HomeBridge(self.hass) - self.assertEqual(mock_acc_driver.mock_calls, [call().stop()]) + with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ + as mock_add_acc, \ + patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.side_effect = [None, 'acc', None] + homekit.add_bridge_accessory(State('light.demo', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 363398124, {})) + self.assertFalse(mock_add_acc.called) + homekit.add_bridge_accessory(State('demo.test', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 294192020, {})) + self.assertTrue(mock_add_acc.called) + homekit.add_bridge_accessory(State('demo.test_2', 'on')) + self.assertEqual(mock_get_acc.call_args, + call(self.hass, ANY, 429982757, {})) + self.assertEqual(mock_add_acc.mock_calls, [call('acc')]) + + def test_homekit_entity_filter(self): + """Test the entity filter.""" + entity_filter = generate_filter(['cover'], ['demo.test'], [], []) + homekit = HomeKit(self.hass, None, entity_filter, {}) + + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.return_value = None + + homekit.add_bridge_accessory(State('cover.test', 'open')) + self.assertTrue(mock_get_acc.called) + mock_get_acc.reset_mock() + + homekit.add_bridge_accessory(State('demo.test', 'on')) + self.assertTrue(mock_get_acc.called) + mock_get_acc.reset_mock() + + homekit.add_bridge_accessory(State('light.demo', 'light')) + self.assertFalse(mock_get_acc.called) + + @patch(PATH_HOMEKIT + '.show_setup_message') + @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') + def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): + """Test HomeKit start method.""" + homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit.bridge = HomeBridge(self.hass) + homekit.driver = Mock() + + self.hass.states.set('light.demo', 'on') + state = self.hass.states.all()[0] + + homekit.start() + + self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) + self.assertEqual(mock_show_setup_msg.mock_calls, [ + call(homekit.bridge, self.hass)]) + self.assertEqual(homekit.driver.mock_calls, [call.start()]) + self.assertTrue(homekit.started) + + # Test start() if already started + homekit.driver.reset_mock() + homekit.start() + self.assertEqual(homekit.driver.mock_calls, []) + + def test_homekit_stop(self): + """Test HomeKit stop method.""" + homekit = HomeKit(None, None, None, None) + homekit.driver = Mock() + + # Test if started = False + homekit.stop() + self.assertFalse(homekit.driver.stop.called) + + # Test if driver not started + homekit.started = True + homekit.driver.configure_mock(**{'run_sentinel': None}) + homekit.stop() + self.assertFalse(homekit.driver.stop.called) + + # Test if driver is started + homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'}) + homekit.stop() + self.assertTrue(homekit.driver.stop.called) diff --git a/tests/components/homekit/test_switches.py b/tests/components/homekit/test_switches.py deleted file mode 100644 index d9f2d6c1d1a..00000000000 --- a/tests/components/homekit/test_switches.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Test different accessory types: Switches.""" -import unittest -from unittest.mock import patch - -from homeassistant.core import callback -from homeassistant.components.homekit.switches import Switch -from homeassistant.const import ATTR_SERVICE, EVENT_CALL_SERVICE - -from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('switches') - - -class TestHomekitSwitches(unittest.TestCase): - """Test class for all accessory types regarding switches.""" - - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] - - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) - - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) - - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - switch = 'switch.testswitch' - - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Switch(self.hass, switch, 'Switch') - acc.run() - - self.assertEqual(acc.char_on.value, False) - - self.hass.states.set(switch, 'on') - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, True) - - self.hass.states.set(switch, 'off') - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.set_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'turn_on') - self.assertEqual(acc.char_on.value, True) - - acc.char_on.set_value(False) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'turn_off') - self.assertEqual(acc.char_on.value, False) diff --git a/tests/components/homekit/test_covers.py b/tests/components/homekit/test_type_covers.py similarity index 87% rename from tests/components/homekit/test_covers.py rename to tests/components/homekit/test_type_covers.py index fe0ede5d8fb..45631a76c98 100644 --- a/tests/components/homekit/test_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,19 +1,15 @@ """Test different accessory types: Covers.""" import unittest -from unittest.mock import patch from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION) -from homeassistant.components.homekit.covers import Window +from homeassistant.components.homekit.type_covers import WindowCovering from homeassistant.const import ( STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('covers') class TestHomekitSensors(unittest.TestCase): @@ -39,10 +35,11 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Window(self.hass, window_cover, 'Cover') - acc.run() + acc = WindowCovering(self.hass, window_cover, 'Cover', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 14) # WindowCovering self.assertEqual(acc.char_current_position.value, 0) self.assertEqual(acc.char_target_position.value, 0) diff --git a/tests/components/homekit/test_security_systems.py b/tests/components/homekit/test_type_security_systems.py similarity index 72% rename from tests/components/homekit/test_security_systems.py rename to tests/components/homekit/test_type_security_systems.py index 4753e86c084..4d61fc4a44c 100644 --- a/tests/components/homekit/test_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,18 +1,15 @@ """Test different accessory types: Security Systems.""" import unittest -from unittest.mock import patch from homeassistant.core import callback -from homeassistant.components.homekit.security_systems import SecuritySystem +from homeassistant.components.homekit.type_security_systems import ( + SecuritySystem) from homeassistant.const import ( - ATTR_SERVICE, EVENT_CALL_SERVICE, + ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('security_systems') class TestHomekitSecuritySystems(unittest.TestCase): @@ -36,12 +33,14 @@ class TestHomekitSecuritySystems(unittest.TestCase): def test_switch_set_state(self): """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.testsecurity' + acp = 'alarm_control_panel.test' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = SecuritySystem(self.hass, acp, 'SecuritySystem') - acc.run() + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code='1234', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 11) # AlarmSystem self.assertEqual(acc.char_current_state.value, 3) self.assertEqual(acc.char_target_state.value, 3) @@ -66,27 +65,40 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_target_state.value, 3) self.assertEqual(acc.char_current_state.value, 3) + self.hass.states.set(acp, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_target_state.value, 3) + self.assertEqual(acc.char_current_state.value, 3) + # Set from HomeKit acc.char_target_state.set_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 0) acc.char_target_state.set_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 1) acc.char_target_state.set_value(2) self.hass.block_till_done() self.assertEqual( self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 2) acc.char_target_state.set_value(3) self.hass.block_till_done() self.assertEqual( self.events[3].data[ATTR_SERVICE], 'alarm_disarm') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) diff --git a/tests/components/homekit/test_sensors.py b/tests/components/homekit/test_type_sensors.py similarity index 64% rename from tests/components/homekit/test_sensors.py rename to tests/components/homekit/test_type_sensors.py index 4698c363503..f9a14f6b8cf 100644 --- a/tests/components/homekit/test_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,17 +1,13 @@ """Test different accessory types: Sensors.""" import unittest -from unittest.mock import patch from homeassistant.components.homekit.const import PROP_CELSIUS -from homeassistant.components.homekit.sensors import ( +from homeassistant.components.homekit.type_sensors import ( TemperatureSensor, calc_temperature) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('sensors') def test_calc_temperature(): @@ -32,7 +28,6 @@ class TestHomekitSensors(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - get_patch_paths('sensors') def tearDown(self): """Stop down everything that was started.""" @@ -40,27 +35,28 @@ class TestHomekitSensors(unittest.TestCase): def test_temperature(self): """Test if accessory is updated after state change.""" - temperature_sensor = 'sensor.temperature' + entity_id = 'sensor.temperature' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = TemperatureSensor(self.hass, temperature_sensor, - 'Temperature') - acc.run() + acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor self.assertEqual(acc.char_temp.value, 0.0) - self.assertEqual(acc.char_temp.properties, PROP_CELSIUS) + for key, value in PROP_CELSIUS.items(): + self.assertEqual(acc.char_temp.properties[key], value) - self.hass.states.set(temperature_sensor, STATE_UNKNOWN, + self.hass.states.set(entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() - self.hass.states.set(temperature_sensor, '20', + self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 20) - self.hass.states.set(temperature_sensor, '75.2', + self.hass.states.set(entity_id, '75.2', {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 24) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py new file mode 100644 index 00000000000..21d7583152e --- /dev/null +++ b/tests/components/homekit/test_type_switches.py @@ -0,0 +1,104 @@ +"""Test different accessory types: Switches.""" +import unittest + +from homeassistant.core import callback, split_entity_id +from homeassistant.components.homekit.type_switches import Switch +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, + SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF) + +from tests.common import get_test_home_assistant + + +class TestHomekitSwitches(unittest.TestCase): + """Test class for all accessory types regarding switches.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_switch_set_state(self): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'switch.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 8) # Switch + + self.assertEqual(acc.char_on.value, False) + + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, True) + + self.hass.states.set(entity_id, STATE_OFF) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + acc.char_on.set_value(False) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + def test_remote_set_state(self): + """Test service call for remote as domain.""" + entity_id = 'remote.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(acc.char_on.value, True) + + def test_input_boolean_set_state(self): + """Test service call for remote as domain.""" + entity_id = 'input_boolean.test' + domain = split_entity_id(entity_id)[0] + + acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc.run() + + self.assertEqual(acc.char_on.value, False) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], domain) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(acc.char_on.value, True) diff --git a/tests/components/homekit/test_thermostats.py b/tests/components/homekit/test_type_thermostats.py similarity index 65% rename from tests/components/homekit/test_thermostats.py rename to tests/components/homekit/test_type_thermostats.py index fabffe881bb..6505bf72efb 100644 --- a/tests/components/homekit/test_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,21 +1,18 @@ """Test different accessory types: Thermostats.""" import unittest -from unittest.mock import patch from homeassistant.core import callback from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - ATTR_OPERATION_MODE, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.thermostats import Thermostat, STATE_OFF + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) +from homeassistant.components.homekit.type_thermostats import ( + Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from tests.common import get_test_home_assistant -from tests.mock.homekit import get_patch_paths, mock_preload_service - -PATH_ACC, PATH_FILE = get_patch_paths('thermostats') class TestHomekitThermostats(unittest.TestCase): @@ -39,12 +36,13 @@ class TestHomekitThermostats(unittest.TestCase): def test_default_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' - with patch(PATH_ACC, side_effect=mock_preload_service): - with patch(PATH_FILE, side_effect=mock_preload_service): - acc = Thermostat(self.hass, climate, 'Climate', False) - acc.run() + acc = Thermostat(self.hass, climate, 'Climate', False, aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 9) # Thermostat self.assertEqual(acc.char_current_heat_cool.value, 0) self.assertEqual(acc.char_target_heat_cool.value, 0) @@ -78,6 +76,30 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_current_temp.value, 23.0) self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 20.0) + self.assertEqual(acc.char_current_heat_cool.value, 2) + self.assertEqual(acc.char_target_heat_cool.value, 2) + self.assertEqual(acc.char_current_temp.value, 25.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 19.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 20.0) + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 2) + self.assertEqual(acc.char_current_temp.value, 19.0) + self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_OFF, {ATTR_OPERATION_MODE: STATE_OFF, ATTR_TEMPERATURE: 22.0, @@ -90,6 +112,45 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_current_temp.value, 18.0) self.assertEqual(acc.char_display_units.value, 0) + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 1) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 18.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 2) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 25.0) + self.assertEqual(acc.char_display_units.value, 0) + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 22.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + self.hass.block_till_done() + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 3) + self.assertEqual(acc.char_current_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit acc.char_target_temp.set_value(19.0) self.hass.block_till_done() @@ -110,7 +171,7 @@ class TestHomekitThermostats(unittest.TestCase): def test_auto_thermostat(self): """Test if accessory and HA are updated accordingly.""" - climate = 'climate.testclimate' + climate = 'climate.test' acc = Thermostat(self.hass, climate, 'Climate', True) acc.run() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py new file mode 100644 index 00000000000..f95db9a4a13 --- /dev/null +++ b/tests/components/homekit/test_util.py @@ -0,0 +1,83 @@ +"""Test HomeKit util module.""" +import unittest + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID +from homeassistant.components.homekit.util import ( + show_setup_message, dismiss_setup_message, ATTR_CODE) +from homeassistant.components.homekit.util import validate_entity_config \ + as vec +from homeassistant.components.persistent_notification import ( + SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) +from homeassistant.const import ( + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) + +from tests.common import get_test_home_assistant + + +class TestUtil(unittest.TestCase): + """Test all HomeKit util methods.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_validate_entity_config(self): + """Test validate entities.""" + configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, + {'demo.test': 'test'}, {'demo.test': [1, 2]}, + {'demo.test': None}] + + for conf in configs: + with self.assertRaises(vol.Invalid): + vec(conf) + + self.assertEqual(vec({}), {}) + self.assertEqual( + vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}), + {'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) + + def test_show_setup_msg(self): + """Test show setup message as persistence notification.""" + bridge = HomeBridge(self.hass) + + show_setup_message(bridge, self.hass) + self.hass.block_till_done() + + data = self.events[0].data + self.assertEqual( + data.get(ATTR_DOMAIN, None), 'persistent_notification') + self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE) + self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) + self.assertEqual( + data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), + HOMEKIT_NOTIFY_ID) + + def test_dismiss_setup_msg(self): + """Test dismiss setup message.""" + dismiss_setup_message(self.hass) + self.hass.block_till_done() + + data = self.events[0].data + self.assertEqual( + data.get(ATTR_DOMAIN, None), 'persistent_notification') + self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS) + self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) + self.assertEqual( + data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), + HOMEKIT_NOTIFY_ID) diff --git a/tests/mock/homekit.py b/tests/mock/homekit.py deleted file mode 100644 index 2872fa59f19..00000000000 --- a/tests/mock/homekit.py +++ /dev/null @@ -1,133 +0,0 @@ -"""Basic mock functions and objects related to the HomeKit component.""" -PATH_HOMEKIT = 'homeassistant.components.homekit' - - -def get_patch_paths(name=None): - """Return paths to mock 'add_preload_service'.""" - path_acc = PATH_HOMEKIT + '.accessories.add_preload_service' - path_file = PATH_HOMEKIT + '.' + str(name) + '.add_preload_service' - return (path_acc, path_file) - - -def mock_preload_service(acc, service, chars=None, opt_chars=None): - """Mock alternative for function 'add_preload_service'.""" - service = MockService(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - service.add_characteristic(char_name) - if opt_chars: - opt_chars = opt_chars if isinstance(opt_chars, list) else [opt_chars] - for opt_char_name in opt_chars: - service.add_characteristic(opt_char_name) - acc.add_service(service) - return service - - -class MockAccessory(): - """Define all attributes and methods for a MockAccessory.""" - - def __init__(self, name): - """Initialize a MockAccessory object.""" - self.display_name = name - self.services = [] - - def __repr__(self): - """Return a representation of a MockAccessory. Use for debugging.""" - serv_list = [serv.display_name for serv in self.services] - return "".format( - self.display_name, serv_list) - - def add_service(self, service): - """Add service to list of services.""" - self.services.append(service) - - def get_service(self, name): - """Retrieve service from service list or return new MockService.""" - for serv in self.services: - if serv.display_name == name: - return serv - serv = MockService(name) - self.add_service(serv) - return serv - - -class MockService(): - """Define all attributes and methods for a MockService.""" - - def __init__(self, name): - """Initialize a MockService object.""" - self.characteristics = [] - self.opt_characteristics = [] - self.display_name = name - - def __repr__(self): - """Return a representation of a MockService. Use for debugging.""" - char_list = [char.display_name for char in self.characteristics] - opt_char_list = [ - char.display_name for char in self.opt_characteristics] - return "".format( - self.display_name, char_list, opt_char_list) - - def add_characteristic(self, char): - """Add characteristic to char list.""" - self.characteristics.append(char) - - def add_opt_characteristic(self, char): - """Add characteristic to opt_char list.""" - self.opt_characteristics.append(char) - - def get_characteristic(self, name): - """Get char for char lists or return new MockChar.""" - for char in self.characteristics: - if char.display_name == name: - return char - for char in self.opt_characteristics: - if char.display_name == name: - return char - char = MockChar(name) - self.add_characteristic(char) - return char - - -class MockChar(): - """Define all attributes and methods for a MockChar.""" - - def __init__(self, name): - """Initialize a MockChar object.""" - self.display_name = name - self.properties = {} - self.value = None - self.type_id = None - self.setter_callback = None - - def __repr__(self): - """Return a representation of a MockChar. Use for debugging.""" - return "".format( - self.display_name, self.value) - - def set_value(self, value, should_notify=True, should_callback=True): - """Set value of char.""" - self.value = value - if self.setter_callback is not None and should_callback: - # pylint: disable=not-callable - self.setter_callback(value) - - def get_value(self): - """Get char value.""" - return self.value - - -class MockTypeLoader(): - """Define all attributes and methods for a MockTypeLoader.""" - - def __init__(self, class_type): - """Initialize a MockTypeLoader object.""" - self.class_type = class_type - - def get(self, name): - """Return a MockService or MockChar object.""" - if self.class_type == 'service': - return MockService(name) - elif self.class_type == 'char': - return MockChar(name) From 76874e1cbc17bd0f13f76ce524f5a2aacf832786 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 14 Mar 2018 19:47:31 -0700 Subject: [PATCH 028/924] Update translations --- .../.translations/de.json | 24 +++++++++++++++++ .../.translations/fi.json | 11 ++++++++ .../.translations/ko.json | 24 +++++++++++++++++ .../.translations/nl.json | 23 ++++++++++++++++ .../.translations/no.json | 24 +++++++++++++++++ .../.translations/pl.json | 24 +++++++++++++++++ .../.translations/ro.json | 15 +++++++++++ .../.translations/sl.json | 24 +++++++++++++++++ .../.translations/zh-Hans.json | 24 +++++++++++++++++ .../components/hue/.translations/de.json | 26 +++++++++++++++++++ .../components/hue/.translations/en.json | 2 +- .../components/hue/.translations/ko.json | 26 +++++++++++++++++++ .../components/hue/.translations/nl.json | 26 +++++++++++++++++++ .../components/hue/.translations/no.json | 26 +++++++++++++++++++ .../components/hue/.translations/pl.json | 26 +++++++++++++++++++ .../components/hue/.translations/ro.json | 18 +++++++++++++ .../components/hue/.translations/sl.json | 26 +++++++++++++++++++ .../components/hue/.translations/zh-Hans.json | 26 +++++++++++++++++++ .../sensor/.translations/season.cs.json | 8 ++++++ .../sensor/.translations/season.cy.json | 8 ++++++ .../sensor/.translations/season.de.json | 8 ++++++ .../sensor/.translations/season.es.json | 8 ++++++ .../sensor/.translations/season.fi.json | 8 ++++++ .../sensor/.translations/season.ja.json | 8 ++++++ .../sensor/.translations/season.ko.json | 8 ++++++ .../sensor/.translations/season.nl.json | 8 ++++++ .../sensor/.translations/season.no.json | 8 ++++++ .../sensor/.translations/season.pl.json | 8 ++++++ .../sensor/.translations/season.pt.json | 8 ++++++ .../sensor/.translations/season.ro.json | 8 ++++++ .../sensor/.translations/season.sl.json | 8 ++++++ .../sensor/.translations/season.sv.json | 8 ++++++ .../sensor/.translations/season.th.json | 8 ++++++ .../sensor/.translations/season.zh-Hans.json | 8 ++++++ .../sensor/.translations/season.zh-Hant.json | 8 ++++++ 35 files changed, 530 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/config_entry_example/.translations/de.json create mode 100644 homeassistant/components/config_entry_example/.translations/fi.json create mode 100644 homeassistant/components/config_entry_example/.translations/ko.json create mode 100644 homeassistant/components/config_entry_example/.translations/nl.json create mode 100644 homeassistant/components/config_entry_example/.translations/no.json create mode 100644 homeassistant/components/config_entry_example/.translations/pl.json create mode 100644 homeassistant/components/config_entry_example/.translations/ro.json create mode 100644 homeassistant/components/config_entry_example/.translations/sl.json create mode 100644 homeassistant/components/config_entry_example/.translations/zh-Hans.json create mode 100644 homeassistant/components/hue/.translations/de.json create mode 100644 homeassistant/components/hue/.translations/ko.json create mode 100644 homeassistant/components/hue/.translations/nl.json create mode 100644 homeassistant/components/hue/.translations/no.json create mode 100644 homeassistant/components/hue/.translations/pl.json create mode 100644 homeassistant/components/hue/.translations/ro.json create mode 100644 homeassistant/components/hue/.translations/sl.json create mode 100644 homeassistant/components/hue/.translations/zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.cs.json create mode 100644 homeassistant/components/sensor/.translations/season.cy.json create mode 100644 homeassistant/components/sensor/.translations/season.de.json create mode 100644 homeassistant/components/sensor/.translations/season.es.json create mode 100644 homeassistant/components/sensor/.translations/season.fi.json create mode 100644 homeassistant/components/sensor/.translations/season.ja.json create mode 100644 homeassistant/components/sensor/.translations/season.ko.json create mode 100644 homeassistant/components/sensor/.translations/season.nl.json create mode 100644 homeassistant/components/sensor/.translations/season.no.json create mode 100644 homeassistant/components/sensor/.translations/season.pl.json create mode 100644 homeassistant/components/sensor/.translations/season.pt.json create mode 100644 homeassistant/components/sensor/.translations/season.ro.json create mode 100644 homeassistant/components/sensor/.translations/season.sl.json create mode 100644 homeassistant/components/sensor/.translations/season.sv.json create mode 100644 homeassistant/components/sensor/.translations/season.th.json create mode 100644 homeassistant/components/sensor/.translations/season.zh-Hans.json create mode 100644 homeassistant/components/sensor/.translations/season.zh-Hant.json diff --git a/homeassistant/components/config_entry_example/.translations/de.json b/homeassistant/components/config_entry_example/.translations/de.json new file mode 100644 index 00000000000..75b88f2f822 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ung\u00fcltige Objekt-ID" + }, + "step": { + "init": { + "data": { + "object_id": "Objekt-ID" + }, + "description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.", + "title": "W\u00e4hle eine Objekt-ID" + }, + "name": { + "data": { + "name": "Name" + }, + "description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein", + "title": "Name des Test-Entity" + } + }, + "title": "Beispiel Konfig-Eintrag" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/fi.json b/homeassistant/components/config_entry_example/.translations/fi.json new file mode 100644 index 00000000000..054a6f372bc --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "name": { + "data": { + "name": "Nimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ko.json b/homeassistant/components/config_entry_example/.translations/ko.json new file mode 100644 index 00000000000..f12e3fc52f1 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "object_id": "\uc624\ube0c\uc81d\ud2b8 ID" + }, + "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694", + "title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd" + }, + "name": { + "data": { + "name": "\uc774\ub984" + }, + "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.", + "title": "\uad6c\uc131\uc694\uc18c \uc774\ub984" + } + }, + "title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json new file mode 100644 index 00000000000..10469dd0804 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ongeldig object ID" + }, + "step": { + "init": { + "data": { + "object_id": "Object ID" + }, + "description": "Voer een object_id in voor het testen van de entiteit.", + "title": "Kies object id" + }, + "name": { + "data": { + "name": "Naam" + }, + "description": "Voer een naam in voor het testen van de entiteit.", + "title": "Naam van de entiteit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/no.json b/homeassistant/components/config_entry_example/.translations/no.json new file mode 100644 index 00000000000..380c539f8af --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Ugyldig objekt ID" + }, + "step": { + "init": { + "data": { + "object_id": "Objekt ID" + }, + "description": "Vennligst skriv inn en object_id for testenheten.", + "title": "Velg objekt ID" + }, + "name": { + "data": { + "name": "Navn" + }, + "description": "Vennligst skriv inn et navn for testenheten.", + "title": "Navn p\u00e5 enheten" + } + }, + "title": "Konfigureringseksempel" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/pl.json b/homeassistant/components/config_entry_example/.translations/pl.json new file mode 100644 index 00000000000..35cca168249 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu" + }, + "step": { + "init": { + "data": { + "object_id": "Identyfikator obiektu" + }, + "description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.", + "title": "Wybierz identyfikator obiektu" + }, + "name": { + "data": { + "name": "Nazwa" + }, + "description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.", + "title": "Nazwa jednostki" + } + }, + "title": "Przyk\u0142ad wpisu do konfiguracji" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ro.json b/homeassistant/components/config_entry_example/.translations/ro.json new file mode 100644 index 00000000000..1a4cdd6bbb7 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/ro.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "init": { + "description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.", + "title": "Alege\u021bi id-ul obiectului" + }, + "name": { + "data": { + "name": "Nume" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/sl.json b/homeassistant/components/config_entry_example/.translations/sl.json new file mode 100644 index 00000000000..11d2d3f5e80 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "Neveljaven ID objekta" + }, + "step": { + "init": { + "data": { + "object_id": "ID objekta" + }, + "description": "Prosimo, vnesite Id_objekta za testni subjekt.", + "title": "Izberite ID objekta" + }, + "name": { + "data": { + "name": "Ime" + }, + "description": "Vnesite ime za testni subjekt.", + "title": "Ime subjekta" + } + }, + "title": "Primer nastavitve" + } +} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/zh-Hans.json b/homeassistant/components/config_entry_example/.translations/zh-Hans.json new file mode 100644 index 00000000000..ee10e6d7b48 --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/zh-Hans.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID" + }, + "step": { + "init": { + "data": { + "object_id": "\u5bf9\u8c61 ID" + }, + "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID", + "title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID" + }, + "name": { + "data": { + "name": "\u540d\u79f0" + }, + "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0", + "title": "\u8bbe\u5907\u540d\u79f0" + } + }, + "title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json new file mode 100644 index 00000000000..b7094d91528 --- /dev/null +++ b/homeassistant/components/hue/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", + "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", + "no_bridges": "Philips Hue Bridges entdeckt" + }, + "error": { + "linking": "Unbekannte Link-Fehler aufgetreten.", + "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "W\u00e4hle eine Hue Bridge" + }, + "link": { + "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", + "title": "Hub verbinden" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index ee2e01fdb17..cbf63301da2 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -23,4 +23,4 @@ }, "title": "Philips Hue Bridge" } -} +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json new file mode 100644 index 00000000000..226ae8ba1f6 --- /dev/null +++ b/homeassistant/components/hue/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "error": { + "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" + }, + "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "title": "Hue \ube0c\ub9bf\uc9c0 \uc120\ud0dd" + }, + "link": { + "description": "\ube0c\ub9bf\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9bf\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)", + "title": "\ud5c8\ube0c \uc5f0\uacb0" + } + }, + "title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json new file mode 100644 index 00000000000..750ae39db12 --- /dev/null +++ b/homeassistant/components/hue/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", + "discover_timeout": "Hue bridges kunnen niet worden gevonden", + "no_bridges": "Geen Philips Hue bridges ontdekt" + }, + "error": { + "linking": "Er is een onbekende verbindingsfout opgetreden.", + "register_failed": "Registratie is mislukt, probeer het opnieuw" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Kies Hue bridge" + }, + "link": { + "description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json new file mode 100644 index 00000000000..604475d2ff2 --- /dev/null +++ b/homeassistant/components/hue/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Alle Philips Hue Bridger er allerede konfigurert", + "discover_timeout": "Kunne ikke oppdage Hue Bridger", + "no_bridges": "Ingen Philips Hue Bridger oppdaget" + }, + "error": { + "linking": "Ukjent koblingsfeil oppstod.", + "register_failed": "Registrering feilet, vennligst pr\u00f8v igjen" + }, + "step": { + "init": { + "data": { + "host": "Vert" + }, + "title": "Velg Hue Bridge" + }, + "link": { + "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json new file mode 100644 index 00000000000..cdd26a5b4b2 --- /dev/null +++ b/homeassistant/components/hue/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", + "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue" + }, + "error": { + "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie." + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Wybierz mostek Hue" + }, + "link": { + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant. ", + "title": "Hub Link" + } + }, + "title": "Mostek Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ro.json b/homeassistant/components/hue/.translations/ro.json new file mode 100644 index 00000000000..91541edcc7d --- /dev/null +++ b/homeassistant/components/hue/.translations/ro.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "linking": "A ap\u0103rut o eroare de leg\u0103tur\u0103 necunoscut\u0103.", + "register_failed": "Nu a reu\u0219it \u00eenregistrarea, \u00eencerca\u021bi din nou" + }, + "step": { + "init": { + "data": { + "host": "Gazd\u0103" + } + }, + "link": { + "description": "Ap\u0103sa\u021bi butonul de pe pod pentru a \u00eenregistra Philips Hue cu Home Assistant. \n\n ! [Loca\u021bia butonului pe pod] (/ static / images / config_philips_hue.jpg)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json new file mode 100644 index 00000000000..a6c858e0e40 --- /dev/null +++ b/homeassistant/components/hue/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", + "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", + "no_bridges": "Ni odkritih mostov Philips Hue" + }, + "error": { + "linking": "Pri\u0161lo je do neznane napake pri povezavi.", + "register_failed": "Registracija ni uspela, poskusite znova" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Izberite Hue most" + }, + "link": { + "description": "Pritisnite gumb na mostu, da registrirate Philips Hue s Home Assistentom. \n\n ! [Polo\u017eaj gumba na mostu] (/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json new file mode 100644 index 00000000000..5a94e084dd2 --- /dev/null +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e", + "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", + "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge" + }, + "error": { + "linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002", + "register_failed": "\u6ce8\u518c\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u673a" + }, + "title": "\u9009\u62e9 Hue Bridge" + }, + "link": { + "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", + "title": "\u8fde\u63a5\u4e2d\u67a2" + } + }, + "title": "\u98de\u5229\u6d66 Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.cs.json b/homeassistant/components/sensor/.translations/season.cs.json new file mode 100644 index 00000000000..e2d7e7919be --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.cs.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Podzim", + "spring": "Jaro", + "summer": "L\u00e9to", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.cy.json b/homeassistant/components/sensor/.translations/season.cy.json new file mode 100644 index 00000000000..0d1553ac3ea --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.cy.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Hydref", + "spring": "Gwanwyn", + "summer": "Haf", + "winter": "Gaeaf" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.de.json b/homeassistant/components/sensor/.translations/season.de.json new file mode 100644 index 00000000000..50d702340b9 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Herbst", + "spring": "Fr\u00fchling", + "summer": "Sommer", + "winter": "Winter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.es.json b/homeassistant/components/sensor/.translations/season.es.json new file mode 100644 index 00000000000..65df6a58b10 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.es.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Oto\u00f1o", + "spring": "Primavera", + "summer": "Verano", + "winter": "Invierno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.fi.json b/homeassistant/components/sensor/.translations/season.fi.json new file mode 100644 index 00000000000..f01f6451549 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.fi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Syksy", + "spring": "Kev\u00e4t", + "summer": "Kes\u00e4", + "winter": "Talvi" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ja.json b/homeassistant/components/sensor/.translations/season.ja.json new file mode 100644 index 00000000000..e441b1aa8ac --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ja.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb", + "spring": "\u6625", + "summer": "\u590f", + "winter": "\u51ac" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ko.json b/homeassistant/components/sensor/.translations/season.ko.json new file mode 100644 index 00000000000..f2bf0a7bae5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ko.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\uac00\uc744", + "spring": "\ubd04", + "summer": "\uc5ec\ub984", + "winter": "\uaca8\uc6b8" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.nl.json b/homeassistant/components/sensor/.translations/season.nl.json new file mode 100644 index 00000000000..6054a8e2be5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Herfst", + "spring": "Lente", + "summer": "Zomer", + "winter": "Winter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.no.json b/homeassistant/components/sensor/.translations/season.no.json new file mode 100644 index 00000000000..9d520dae6a5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "H\u00f8st", + "spring": "V\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pl.json b/homeassistant/components/sensor/.translations/season.pl.json new file mode 100644 index 00000000000..f5a7da57e7f --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesie\u0144", + "spring": "Wiosna", + "summer": "Lato", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.pt.json b/homeassistant/components/sensor/.translations/season.pt.json new file mode 100644 index 00000000000..fde45ad6c8e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.pt.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Outono", + "spring": "Primavera", + "summer": "Ver\u00e3o", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ro.json b/homeassistant/components/sensor/.translations/season.ro.json new file mode 100644 index 00000000000..04f90318290 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ro.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Toamn\u0103", + "spring": "Prim\u0103var\u0103", + "summer": "Var\u0103", + "winter": "Iarn\u0103" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.sl.json b/homeassistant/components/sensor/.translations/season.sl.json new file mode 100644 index 00000000000..f715a3ec13a --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.sl.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesen", + "spring": "Pomlad", + "summer": "Poletje", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.sv.json b/homeassistant/components/sensor/.translations/season.sv.json new file mode 100644 index 00000000000..02332d76906 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "H\u00f6st", + "spring": "V\u00e5r", + "summer": "Sommar", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.th.json b/homeassistant/components/sensor/.translations/season.th.json new file mode 100644 index 00000000000..09799730389 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.th.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0e24\u0e14\u0e39\u0e43\u0e1a\u0e44\u0e21\u0e49\u0e23\u0e48\u0e27\u0e07", + "spring": "\u0e24\u0e14\u0e39\u0e43\u0e1a\u0e44\u0e21\u0e49\u0e1c\u0e25\u0e34", + "summer": "\u0e24\u0e14\u0e39\u0e23\u0e49\u0e2d\u0e19", + "winter": "\u0e24\u0e14\u0e39\u0e2b\u0e19\u0e32\u0e27" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.zh-Hans.json b/homeassistant/components/sensor/.translations/season.zh-Hans.json new file mode 100644 index 00000000000..78801f4b1df --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.zh-Hans.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb\u5b63", + "spring": "\u6625\u5b63", + "summer": "\u590f\u5b63", + "winter": "\u51ac\u5b63" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.zh-Hant.json b/homeassistant/components/sensor/.translations/season.zh-Hant.json new file mode 100644 index 00000000000..78801f4b1df --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.zh-Hant.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u79cb\u5b63", + "spring": "\u6625\u5b63", + "summer": "\u590f\u5b63", + "winter": "\u51ac\u5b63" + } +} \ No newline at end of file From e122692b46715820dc4e56460d367a9fbc99b777 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Thu, 15 Mar 2018 04:07:37 +0100 Subject: [PATCH 029/924] deCONZ - Add support for consumption and power sensors (#13218) * Add support for consumption and power sensors * Keep attr_current inside component --- homeassistant/components/binary_sensor/deconz.py | 11 +++++------ homeassistant/components/deconz/__init__.py | 2 +- homeassistant/components/sensor/deconz.py | 16 ++++++++++------ requirements_all.txt | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 1effcf1800a..ef3ec506e3a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -23,8 +23,7 @@ async def async_setup_platform(hass, config, async_add_devices, sensors = hass.data[DATA_DECONZ].sensors entities = [] - for key in sorted(sensors.keys(), key=int): - sensor = sensors[key] + for sensor in sensors.values(): if sensor and sensor.type in DECONZ_BINARY_SENSOR: entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) @@ -93,9 +92,9 @@ class DeconzBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" from pydeconz.sensor import PRESENCE - attr = { - ATTR_BATTERY_LEVEL: self._sensor.battery, - } - if self._sensor.type in PRESENCE: + attr = {} + if self._sensor.battery: + attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in PRESENCE and self._sensor.dark: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index de6d3e89859..26d9fb401e4 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==31'] +REQUIREMENTS = ['pydeconz==32'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index a3c2aa683dc..081b304dc55 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ATTR_BATTERY_LEVEL, CONF_EVENT, CONF_ID +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) from homeassistant.core import EventOrigin, callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level @@ -14,6 +15,7 @@ from homeassistant.util import slugify DEPENDENCIES = ['deconz'] +ATTR_CURRENT = 'current' ATTR_EVENT_ID = 'event_id' @@ -27,8 +29,7 @@ async def async_setup_platform(hass, config, async_add_devices, sensors = hass.data[DATA_DECONZ].sensors entities = [] - for key in sorted(sensors.keys(), key=int): - sensor = sensors[key] + for sensor in sensors.values(): if sensor and sensor.type in DECONZ_SENSOR: if sensor.type in DECONZ_REMOTE: DeconzEvent(hass, sensor) @@ -106,9 +107,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" - attr = { - ATTR_BATTERY_LEVEL: self._sensor.battery, - } + attr = {} + if self._sensor.battery: + attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self.unit_of_measurement == 'Watts': + attr[ATTR_CURRENT] = self._sensor.current + attr[ATTR_VOLTAGE] = self._sensor.voltage return attr diff --git a/requirements_all.txt b/requirements_all.txt index b35b3d0991a..608618eb166 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -701,7 +701,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==31 +pydeconz==32 # homeassistant.components.zwave pydispatcher==2.0.5 From c971d61422d2dffb0102c63b3e82870b5eac4cdd Mon Sep 17 00:00:00 2001 From: c727 Date: Thu, 15 Mar 2018 04:56:56 +0100 Subject: [PATCH 030/924] Change Hass.io icon to home-assistant (#13230) --- homeassistant/components/hassio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 540659273b3..87251a2745c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -156,7 +156,7 @@ def async_setup(hass, config): if 'frontend' in hass.config.components: yield from hass.components.frontend.async_register_built_in_panel( - 'hassio', 'Hass.io', 'mdi:access-point-network') + 'hassio', 'Hass.io', 'mdi:home-assistant') if 'http' in config: yield from hassio.update_hass_api(config['http']) From 223bc187dcf047445b44154ab215e1edd5576e02 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Wed, 14 Mar 2018 21:44:13 -0700 Subject: [PATCH 031/924] More robust MJPEG parser. Fixes #13138. (#13226) * More robust MJPEG parser. Fixes ##13138. * Reimplement image extraction from mjpeg without ascy generator to support python 3.5 --- homeassistant/components/camera/proxy.py | 52 ++++++++---------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index d045235c3ad..1984c21fadb 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([ProxyCamera(hass, config)]) -async def _read_frame(req): - """Read a single frame from an MJPEG stream.""" - # based on https://gist.github.com/russss/1143799 - import cgi - # Read in HTTP headers: - stream = req.content - # multipart/x-mixed-replace; boundary=--frameboundary - _mimetype, options = cgi.parse_header(req.headers['content-type']) - boundary = options.get('boundary').encode('utf-8') - if not boundary: - _LOGGER.error("Malformed MJPEG missing boundary") - raise Exception("Can't find content-type") - - line = await stream.readline() - # Seek ahead to the first chunk - while line.strip() != boundary: - line = await stream.readline() - # Read in chunk headers - while line.strip() != b'': - parts = line.split(b':') - if len(parts) > 1 and parts[0].lower() == b'content-length': - # Grab chunk length - length = int(parts[1].strip()) - line = await stream.readline() - image = await stream.read(length) - return image - - def _resize_image(image, opts): """Resize image.""" from PIL import Image @@ -227,9 +199,9 @@ class ProxyCamera(Camera): 'boundary=--frameboundary') await response.prepare(request) - def write(img_bytes): + async def write(img_bytes): """Write image to stream.""" - response.write(bytes( + await response.write(bytes( '--frameboundary\r\n' 'Content-Type: {}\r\n' 'Content-Length: {}\r\n\r\n'.format( @@ -240,13 +212,23 @@ class ProxyCamera(Camera): req = await stream_coro try: + # This would be nicer as an async generator + # But that would only be supported for python >=3.6 + data = b'' + stream = req.content while True: - image = await _read_frame(req) - if not image: + chunk = await stream.read(102400) + if not chunk: break - image = await self.hass.async_add_job( - _resize_image, image, self._stream_opts) - write(image) + data += chunk + jpg_start = data.find(b'\xff\xd8') + jpg_end = data.find(b'\xff\xd9') + if jpg_start != -1 and jpg_end != -1: + image = data[jpg_start:jpg_end + 2] + image = await self.hass.async_add_job( + _resize_image, image, self._stream_opts) + await write(image) + data = data[jpg_end + 2:] except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") req.close() From 6909be1cc74a44f8c6deaa79ab491ee2ceb7d030 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 15 Mar 2018 11:45:54 +0100 Subject: [PATCH 032/924] Add docstring (#13232) --- homeassistant/components/sensor/crimereports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/crimereports.py b/homeassistant/components/sensor/crimereports.py index aecfca60bf1..a2d7315a314 100644 --- a/homeassistant/components/sensor/crimereports.py +++ b/homeassistant/components/sensor/crimereports.py @@ -89,6 +89,7 @@ class CrimeReportsSensor(Entity): return self._attributes def _incident_event(self, incident): + """Fire if an event occurs.""" data = { 'type': incident.get('type'), 'description': incident.get('friendly_description'), From 27c18068971096219b720e0f18ebb68cde34739e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 15 Mar 2018 12:10:54 +0100 Subject: [PATCH 033/924] Python 3.5 adjustments (#13173) --- homeassistant/bootstrap.py | 8 -------- homeassistant/components/mysensors.py | 5 ++--- homeassistant/core.py | 6 +----- homeassistant/monkey_patch.py | 2 +- homeassistant/scripts/check_config.py | 9 ++++++--- tests/scripts/test_check_config.py | 4 +--- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 34eab679581..00822d93299 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -86,14 +86,6 @@ def async_from_config_dict(config: Dict[str, Any], if enable_log: async_enable_logging(hass, verbose, log_rotate_days, log_file) - if sys.version_info[:2] < (3, 5): - _LOGGER.warning( - 'Python 3.4 support has been deprecated and will be removed in ' - 'the beginning of 2018. Please upgrade Python or your operating ' - 'system. More info: https://home-assistant.io/blog/2017/10/06/' - 'deprecating-python-3.4-support/' - ) - core_config = config.get(core.DOMAIN, {}) try: diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 37e257e5eb9..a560b49648f 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -545,9 +545,8 @@ def setup_mysensors_platform( device_class_copy = device_class[s_type] name = get_mysensors_name(gateway, node_id, child_id) - # python 3.4 cannot unpack inside tuple, but combining tuples works - args_copy = device_args + ( - gateway, node_id, child_id, name, value_type) + args_copy = (*device_args, gateway, node_id, child_id, name, + value_type) devices[dev_id] = device_class_copy(*args_copy) new_devices.append(devices[dev_id]) if new_devices: diff --git a/homeassistant/core.py b/homeassistant/core.py index a486ee1adbf..b49b94f853d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -117,11 +117,7 @@ class HomeAssistant(object): else: self.loop = loop or asyncio.get_event_loop() - executor_opts = {'max_workers': 10} - if sys.version_info[:2] >= (3, 5): - # It will default set to the number of processors on the machine, - # multiplied by 5. That is better for overlap I/O workers. - executor_opts['max_workers'] = None + executor_opts = {'max_workers': None} if sys.version_info[:2] >= (3, 6): executor_opts['thread_name_prefix'] = 'SyncWorker' diff --git a/homeassistant/monkey_patch.py b/homeassistant/monkey_patch.py index 5aa051f2bb5..d5c629c9d34 100644 --- a/homeassistant/monkey_patch.py +++ b/homeassistant/monkey_patch.py @@ -61,7 +61,7 @@ def disable_c_asyncio(): def find_module(self, fullname, path=None): """Find a module.""" if fullname == self.PATH_TRIGGER: - # We lint in Py34, exception is introduced in Py36 + # We lint in Py35, exception is introduced in Py36 # pylint: disable=undefined-variable raise ModuleNotFoundError() # noqa return None diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 1a58757d17f..ac3ac62e82d 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -95,9 +95,12 @@ def run(script_args: List) -> int: if args.files: print(color(C_HEAD, 'yaml files'), '(used /', color('red', 'not used') + ')') - # Python 3.5 gets a recursive, but not in 3.4 - for yfn in sorted(glob(os.path.join(config_dir, '*.yaml')) + - glob(os.path.join(config_dir, '*/*.yaml'))): + deps = os.path.join(config_dir, 'deps') + yaml_files = [f for f in glob(os.path.join(config_dir, '**/*.yaml'), + recursive=True) + if not f.startswith(deps)] + + for yfn in sorted(yaml_files): the_color = '' if yfn in res['yaml_files'] else 'red' print(color(the_color, '-', yfn)) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 677ed8de110..28a3f2ebdc8 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -41,9 +41,7 @@ class TestCheckConfig(unittest.TestCase): # this ensures we have one. try: asyncio.get_event_loop() - except (RuntimeError, AssertionError): - # Py35: RuntimeError - # Py34: AssertionError + except RuntimeError: asyncio.set_event_loop(asyncio.new_event_loop()) # Will allow seeing full diff From 646ed5de528a2227ac1239426305f1a3abb0d3b9 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 15 Mar 2018 12:31:31 +0100 Subject: [PATCH 034/924] Added cover.group platform (replaces #12303) (#12692) * Added cover.group platform * Added async/await, smaller changes * Made (async_update) methods regular methods * Small improvements * Changed classname * Changes based on feedback * Service calls * update_supported_features is now a callback method * combined all 'update_attr_*' methods in 'async_update' * Small changes * Fixes * is_closed * current_position * current_tilt_position * Updated tests * Small changes 2 --- CODEOWNERS | 1 + homeassistant/components/cover/group.py | 271 ++++++++++++++++++ tests/components/cover/test_group.py | 350 ++++++++++++++++++++++++ 3 files changed, 622 insertions(+) create mode 100755 homeassistant/components/cover/group.py create mode 100644 tests/components/cover/test_group.py diff --git a/CODEOWNERS b/CODEOWNERS index fedab8f6ae4..d8ebc3cff56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,6 +49,7 @@ homeassistant/components/camera/yi.py @bachya homeassistant/components/climate/ephember.py @ttroy50 homeassistant/components/climate/eq3btsmart.py @rytilahti homeassistant/components/climate/sensibo.py @andrey-git +homeassistant/components/cover/group.py @cdce8p homeassistant/components/cover/template.py @PhracturedBlue homeassistant/components/device_tracker/automatic.py @armills homeassistant/components/device_tracker/tile.py @bachya diff --git a/homeassistant/components/cover/group.py b/homeassistant/components/cover/group.py new file mode 100755 index 00000000000..c1ea33a9cc7 --- /dev/null +++ b/homeassistant/components/cover/group.py @@ -0,0 +1,271 @@ +""" +This platform allows several cover to be grouped into one cover. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/cover.group/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.components.cover import ( + DOMAIN, PLATFORM_SCHEMA, CoverDevice, ATTR_POSITION, + ATTR_CURRENT_POSITION, ATTR_TILT_POSITION, ATTR_CURRENT_TILT_POSITION, + SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION, + SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, + SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, + SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, + SERVICE_STOP_COVER_TILT, SERVICE_SET_COVER_TILT_POSITION) +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, CONF_NAME, STATE_CLOSED) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change + +_LOGGER = logging.getLogger(__name__) + +KEY_OPEN_CLOSE = 'open_close' +KEY_STOP = 'stop' +KEY_POSITION = 'position' + +DEFAULT_NAME = 'Cover Group' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN), +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the Group Cover platform.""" + async_add_devices( + [CoverGroup(config[CONF_NAME], config[CONF_ENTITIES])]) + + +class CoverGroup(CoverDevice): + """Representation of a CoverGroup.""" + + def __init__(self, name, entities): + """Initialize a CoverGroup entity.""" + self._name = name + self._is_closed = False + self._cover_position = 100 + self._tilt_position = None + self._supported_features = 0 + self._assumed_state = True + + self._entities = entities + self._covers = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), + KEY_POSITION: set()} + self._tilts = {KEY_OPEN_CLOSE: set(), KEY_STOP: set(), + KEY_POSITION: set()} + + @callback + def update_supported_features(self, entity_id, old_state, new_state, + update_state=True): + """Update dictionaries with supported features.""" + if not new_state: + for values in self._covers.values(): + values.discard(entity_id) + for values in self._tilts.values(): + values.discard(entity_id) + if update_state: + self.async_schedule_update_ha_state(True) + return + + features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if features & (SUPPORT_OPEN | SUPPORT_CLOSE): + self._covers[KEY_OPEN_CLOSE].add(entity_id) + else: + self._covers[KEY_OPEN_CLOSE].discard(entity_id) + if features & (SUPPORT_STOP): + self._covers[KEY_STOP].add(entity_id) + else: + self._covers[KEY_STOP].discard(entity_id) + if features & (SUPPORT_SET_POSITION): + self._covers[KEY_POSITION].add(entity_id) + else: + self._covers[KEY_POSITION].discard(entity_id) + + if features & (SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT): + self._tilts[KEY_OPEN_CLOSE].add(entity_id) + else: + self._tilts[KEY_OPEN_CLOSE].discard(entity_id) + if features & (SUPPORT_STOP_TILT): + self._tilts[KEY_STOP].add(entity_id) + else: + self._tilts[KEY_STOP].discard(entity_id) + if features & (SUPPORT_SET_TILT_POSITION): + self._tilts[KEY_POSITION].add(entity_id) + else: + self._tilts[KEY_POSITION].discard(entity_id) + + if update_state: + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register listeners.""" + for entity_id in self._entities: + new_state = self.hass.states.get(entity_id) + self.update_supported_features(entity_id, None, new_state, + update_state=False) + async_track_state_change(self.hass, self._entities, + self.update_supported_features) + await self.async_update() + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def assumed_state(self): + """Enable buttons even if at end position.""" + return self._assumed_state + + @property + def should_poll(self): + """Disable polling for cover group.""" + return False + + @property + def supported_features(self): + """Flag supported features for the cover.""" + return self._supported_features + + @property + def is_closed(self): + """Return if all covers in group are closed.""" + return self._is_closed + + @property + def current_cover_position(self): + """Return current position for all covers.""" + return self._cover_position + + @property + def current_cover_tilt_position(self): + """Return current tilt position for all covers.""" + return self._tilt_position + + async def async_open_cover(self, **kwargs): + """Move the covers up.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, data, blocking=True) + + async def async_close_cover(self, **kwargs): + """Move the covers down.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, data, blocking=True) + + async def async_stop_cover(self, **kwargs): + """Fire the stop action.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_STOP]} + await self.hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, data, blocking=True) + + async def async_set_cover_position(self, **kwargs): + """Set covers position.""" + data = {ATTR_ENTITY_ID: self._covers[KEY_POSITION], + ATTR_POSITION: kwargs[ATTR_POSITION]} + await self.hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_POSITION, data, blocking=True) + + async def async_open_cover_tilt(self, **kwargs): + """Tilt covers open.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER_TILT, data, blocking=True) + + async def async_close_cover_tilt(self, **kwargs): + """Tilt covers closed.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_OPEN_CLOSE]} + await self.hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER_TILT, data, blocking=True) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_STOP]} + await self.hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER_TILT, data, blocking=True) + + async def async_set_cover_tilt_position(self, **kwargs): + """Set tilt position.""" + data = {ATTR_ENTITY_ID: self._tilts[KEY_POSITION], + ATTR_TILT_POSITION: kwargs[ATTR_TILT_POSITION]} + await self.hass.services.async_call( + DOMAIN, SERVICE_SET_COVER_TILT_POSITION, data, blocking=True) + + async def async_update(self): + """Update state and attributes.""" + self._assumed_state = False + + self._is_closed = True + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if not state: + continue + if state.state != STATE_CLOSED: + self._is_closed = False + break + + self._cover_position = None + if self._covers[KEY_POSITION]: + position = -1 + self._cover_position = 0 if self.is_closed else 100 + for entity_id in self._covers[KEY_POSITION]: + state = self.hass.states.get(entity_id) + pos = state.attributes.get(ATTR_CURRENT_POSITION) + if position == -1: + position = pos + elif position != pos: + self._assumed_state = True + break + else: + if position != -1: + self._cover_position = position + + self._tilt_position = None + if self._tilts[KEY_POSITION]: + position = -1 + self._tilt_position = 100 + for entity_id in self._tilts[KEY_POSITION]: + state = self.hass.states.get(entity_id) + pos = state.attributes.get(ATTR_CURRENT_TILT_POSITION) + if position == -1: + position = pos + elif position != pos: + self._assumed_state = True + break + else: + if position != -1: + self._tilt_position = position + + supported_features = 0 + supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE \ + if self._covers[KEY_OPEN_CLOSE] else 0 + supported_features |= SUPPORT_STOP \ + if self._covers[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_POSITION \ + if self._covers[KEY_POSITION] else 0 + supported_features |= SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT \ + if self._tilts[KEY_OPEN_CLOSE] else 0 + supported_features |= SUPPORT_STOP_TILT \ + if self._tilts[KEY_STOP] else 0 + supported_features |= SUPPORT_SET_TILT_POSITION \ + if self._tilts[KEY_POSITION] else 0 + self._supported_features = supported_features + + if not self._assumed_state: + for entity_id in self._entities: + state = self.hass.states.get(entity_id) + if state and state.attributes.get(ATTR_ASSUMED_STATE): + self._assumed_state = True + break diff --git a/tests/components/cover/test_group.py b/tests/components/cover/test_group.py new file mode 100644 index 00000000000..288e1c5e047 --- /dev/null +++ b/tests/components/cover/test_group.py @@ -0,0 +1,350 @@ +"""The tests for the group cover platform.""" + +import unittest +from datetime import timedelta +import homeassistant.util.dt as dt_util + +from homeassistant import setup +from homeassistant.components import cover +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, DOMAIN) +from homeassistant.components.cover.group import DEFAULT_NAME +from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, STATE_OPEN, STATE_CLOSED) +from tests.common import ( + assert_setup_component, get_test_home_assistant, fire_time_changed) + +COVER_GROUP = 'cover.cover_group' +DEMO_COVER = 'cover.kitchen_window' +DEMO_COVER_POS = 'cover.hall_window' +DEMO_COVER_TILT = 'cover.living_room_window' +DEMO_TILT = 'cover.tilt_demo' + +CONFIG = { + DOMAIN: [ + {'platform': 'demo'}, + {'platform': 'group', + CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]} + ] +} + + +class TestMultiCover(unittest.TestCase): + """Test the group cover platform.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_attributes(self): + """Test handling of state attributes.""" + config = {DOMAIN: {'platform': 'group', CONF_ENTITIES: [ + DEMO_COVER, DEMO_COVER_POS, DEMO_COVER_TILT, DEMO_TILT]}} + + with assert_setup_component(1, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, config) + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(attr.get(ATTR_FRIENDLY_NAME), DEFAULT_NAME) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports open / close / stop + self.hass.states.set( + DEMO_COVER, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 11}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 11) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports set_cover_position + self.hass.states.set( + DEMO_COVER_POS, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 70}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 15) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports open tilt / close tilt / stop tilt + self.hass.states.set( + DEMO_TILT, STATE_OPEN, {ATTR_SUPPORTED_FEATURES: 112}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 127) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + # Add Entity that supports set_tilt_position + self.hass.states.set( + DEMO_COVER_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 60}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 255) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 70) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + # ### Test assumed state ### + # ########################## + + # For covers + self.hass.states.set( + DEMO_COVER, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 4, ATTR_CURRENT_POSITION: 100}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 244) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), 100) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + self.hass.states.remove(DEMO_COVER) + self.hass.block_till_done() + self.hass.states.remove(DEMO_COVER_POS) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 240) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 60) + + # For tilts + self.hass.states.set( + DEMO_TILT, STATE_OPEN, + {ATTR_SUPPORTED_FEATURES: 128, ATTR_CURRENT_TILT_POSITION: 100}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 128) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), 100) + + self.hass.states.remove(DEMO_COVER_TILT) + self.hass.states.set(DEMO_TILT, STATE_CLOSED) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), None) + self.assertEqual(attr.get(ATTR_SUPPORTED_FEATURES), 0) + self.assertEqual(attr.get(ATTR_CURRENT_POSITION), None) + self.assertEqual(attr.get(ATTR_CURRENT_TILT_POSITION), None) + + self.hass.states.set( + DEMO_TILT, STATE_CLOSED, {ATTR_ASSUMED_STATE: True}) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + attr = state.attributes + self.assertEqual(attr.get(ATTR_ASSUMED_STATE), True) + + def test_open_covers(self): + """Test open cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 100) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 100) + + def test_close_covers(self): + """Test close cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.close_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(10): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_CLOSED) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 0) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 0) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 0) + + def test_stop_covers(self): + """Test stop cover function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + cover.stop_cover(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_OPEN) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 20) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 80) + + def test_set_cover_position(self): + """Test set cover position function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.set_cover_position(self.hass, 50, COVER_GROUP) + self.hass.block_till_done() + for _ in range(4): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_POSITION), 50) + + self.assertEqual(self.hass.states.get(DEMO_COVER).state, STATE_CLOSED) + self.assertEqual(self.hass.states.get(DEMO_COVER_POS) + .attributes.get(ATTR_CURRENT_POSITION), 50) + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_POSITION), 50) + + def test_open_tilts(self): + """Test open tilt function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 100) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 100) + + def test_close_tilts(self): + """Test close tilt function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.close_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + for _ in range(5): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 0) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 0) + + def test_stop_tilts(self): + """Test stop tilts function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.open_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + cover.stop_cover_tilt(self.hass, COVER_GROUP) + self.hass.block_till_done() + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 60) + + def test_set_tilt_positions(self): + """Test set tilt position function.""" + with assert_setup_component(2, DOMAIN): + assert setup.setup_component(self.hass, DOMAIN, CONFIG) + + cover.set_cover_tilt_position(self.hass, 80, COVER_GROUP) + self.hass.block_till_done() + for _ in range(3): + future = dt_util.utcnow() + timedelta(seconds=1) + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + state = self.hass.states.get(COVER_GROUP) + self.assertEqual(state.state, STATE_OPEN) + self.assertEqual(state.attributes.get(ATTR_CURRENT_TILT_POSITION), 80) + + self.assertEqual(self.hass.states.get(DEMO_COVER_TILT) + .attributes.get(ATTR_CURRENT_TILT_POSITION), 80) From 5c434f143e875e4d9e9a0aaf416f73cf84d330c6 Mon Sep 17 00:00:00 2001 From: Clement Wong Date: Thu, 15 Mar 2018 13:16:52 +0100 Subject: [PATCH 035/924] Tibber use appNickname as name (#13231) --- homeassistant/components/sensor/tibber.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 8c8ffdfd954..435003f76d0 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -61,7 +61,8 @@ class TibberSensor(Entity): self._state = None self._device_state_attributes = {} self._unit_of_measurement = self._tibber_home.price_unit - self._name = 'Electricity price {}'.format(tibber_home.address1) + self._name = 'Electricity price {}'.format(tibber_home.info['viewer'] + ['home']['appNickname']) async def async_update(self): """Get the latest data and updates the states.""" From 92f13ff60d5cb1bd78c5faba4c8f0d8f659a713b Mon Sep 17 00:00:00 2001 From: Eugene Kuzin Date: Thu, 15 Mar 2018 14:43:29 +0200 Subject: [PATCH 036/924] media_content_type attribute display fix (#13204) * media_content_type fix Kodi media_content_type attribute display fix * media_content_type fix (#6989) fixes attribute display for unknown media * code cleanup * trailing whitespaces * comments correction * redundant "else:" removed --- homeassistant/components/media_player/kodi.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 6450b2f5b35..33116258978 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -73,6 +73,8 @@ MEDIA_TYPES = { 'episode': MEDIA_TYPE_TVSHOW, # Type 'channel' is used for radio or tv streams from pvr 'channel': MEDIA_TYPE_CHANNEL, + # Type 'audio' is used for audio media, that Kodi couldn't scroblle + 'audio': MEDIA_TYPE_MUSIC, } SUPPORT_KODI = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -480,7 +482,12 @@ class KodiDevice(MediaPlayerDevice): @property def media_content_type(self): - """Content type of current playing media.""" + """Content type of current playing media. + + If the media type cannot be detected, the player type is used. + """ + if MEDIA_TYPES.get(self._item.get('type')) is None and self._players: + return MEDIA_TYPES.get(self._players[0]['type']) return MEDIA_TYPES.get(self._item.get('type')) @property From 1d2fd8a2e9c4c89ff3d6155d2447eccdfa71a619 Mon Sep 17 00:00:00 2001 From: Andrei Pop Date: Thu, 15 Mar 2018 17:27:42 +0200 Subject: [PATCH 037/924] Edimax component reports wrong power values (#13011) * Fixed Edimax switch authentication error for newer firmware. * pyedimax moved to pypi * Added pyedimax to gen_requirements_all.py * Cleanup * Fixed https://github.com/home-assistant/home-assistant/issues/13008 * Only ValueError now * Trivial error. --- homeassistant/components/switch/edimax.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 50b5ba93b85..49eb5d32110 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -83,14 +83,13 @@ class SmartPlugSwitch(SwitchDevice): def update(self): """Update edimax switch.""" try: - self._now_power = float(self.smartplug.now_power) / 1000000.0 - except (TypeError, ValueError): + self._now_power = float(self.smartplug.now_power) + except ValueError: self._now_power = None try: - self._now_energy_day = (float(self.smartplug.now_energy_day) / - 1000.0) - except (TypeError, ValueError): + self._now_energy_day = float(self.smartplug.now_energy_day) + except ValueError: self._now_energy_day = None self._state = self.smartplug.state == 'ON' From ee6d6a8859434cf7dc9f53570b2412dfc269525c Mon Sep 17 00:00:00 2001 From: Maximilien Cuony Date: Thu, 15 Mar 2018 16:45:27 +0100 Subject: [PATCH 038/924] myStrom: Add RGB support to Wifi bulbs (#13194) --- homeassistant/components/light/mystrom.py | 41 ++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index ecb120e3079..9f049dd2e8a 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -11,8 +11,10 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH) + SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, + ATTR_RGB_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN +from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB REQUIREMENTS = ['python-mystrom==0.3.8'] @@ -20,7 +22,10 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'myStrom bulb' -SUPPORT_MYSTROM = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH) +SUPPORT_MYSTROM = ( + SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | + SUPPORT_RGB_COLOR +) EFFECT_RAINBOW = 'rainbow' EFFECT_SUNRISE = 'sunrise' @@ -67,6 +72,8 @@ class MyStromLight(Light): self._state = None self._available = False self._brightness = 0 + self._color_h = 0 + self._color_s = 0 @property def name(self): @@ -83,6 +90,11 @@ class MyStromLight(Light): """Return the brightness of the light.""" return self._brightness + @property + def rgb_color(self): + """Return the color of the light.""" + return color_hsv_to_RGB(self._color_h, self._color_s, self._brightness) + @property def available(self) -> bool: """Return True if entity is available.""" @@ -105,11 +117,25 @@ class MyStromLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) + if ATTR_RGB_COLOR in kwargs: + # New color, compute from RGB + color_h, color_s, brightness = color_RGB_to_hsv( + *kwargs[ATTR_RGB_COLOR] + ) + brightness = brightness / 100 * 255 + elif ATTR_BRIGHTNESS in kwargs: + # Brightness update, keep color + color_h, color_s = self._color_h, self._color_s + else: + color_h, color_s = 0, 0 # Back to white + try: if not self.is_on: self._bulb.set_on() if brightness is not None: - self._bulb.set_color_hsv(0, 0, round(brightness * 100 / 255)) + self._bulb.set_color_hsv( + int(color_h), int(color_s), round(brightness * 100 / 255) + ) if effect == EFFECT_SUNRISE: self._bulb.set_sunrise(30) if effect == EFFECT_RAINBOW: @@ -132,7 +158,14 @@ class MyStromLight(Light): try: self._state = self._bulb.get_status() - self._brightness = int(self._bulb.get_brightness()) * 255 / 100 + + colors = self._bulb.get_color()['color'] + color_h, color_s, color_v = colors.split(';') + + self._color_h = int(color_h) + self._color_s = int(color_s) + self._brightness = int(color_v) * 255 / 100 + self._available = True except MyStromConnectionError: _LOGGER.warning("myStrom bulb not online") From 170b8671b9eb0594ca1f628fc7fc0865da010088 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 10:54:22 -0700 Subject: [PATCH 039/924] Fix logbook JSON serialize issue (#13229) * Fix logbook JSON serialize issue * Address flakiness * Lint * deflake ? * Deflake 2 --- homeassistant/components/logbook.py | 9 ++++++--- homeassistant/components/recorder/__init__.py | 13 +++++-------- tests/components/test_history.py | 6 ++++-- tests/components/test_logbook.py | 16 ++++++++++++++-- 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index d0b944793c4..1c3e8ed1f19 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -139,9 +139,12 @@ class LogbookView(HomeAssistantView): end_day = start_day + timedelta(days=1) hass = request.app['hass'] - events = yield from hass.async_add_job( - _get_events, hass, self.config, start_day, end_day) - response = yield from hass.async_add_job(self.json, events) + def json_events(): + """Fetch events and generate JSON.""" + return self.json(list( + _get_events(hass, self.config, start_day, end_day))) + + response = yield from hass.async_add_job(json_events) return response diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 392bccb56d4..23c073ff80a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -29,6 +29,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util +from homeassistant.loader import bind_hass from . import migration, purge from .const import DATA_INSTANCE @@ -87,14 +88,10 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def wait_connection_ready(hass): - """ - Wait till the connection is ready. - - Returns a coroutine object. - """ - return (yield from hass.data[DATA_INSTANCE].async_db_ready) +@bind_hass +async def wait_connection_ready(hass): + """Wait till the connection is ready.""" + return await hass.data[DATA_INSTANCE].async_db_ready def run_information(hass, point_in_time: Optional[datetime] = None): diff --git a/tests/components/test_history.py b/tests/components/test_history.py index be768f5ec69..bea2af396cb 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -483,11 +483,13 @@ class TestComponentHistory(unittest.TestCase): return zero, four, states -async def test_fetch_period_api(hass, test_client): +async def test_fetch_period_api(hass, aiohttp_client): """Test the fetch period view for history.""" await hass.async_add_job(init_recorder_component, hass) await async_setup_component(hass, 'history', {}) - client = await test_client(hass.http.app) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await aiohttp_client(hass.http.app) response = await client.get( '/api/history/period/{}'.format(dt_util.utcnow().isoformat())) assert response.status == 200 diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index bd10416c7a2..6c71a263afa 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -10,8 +10,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF) import homeassistant.util.dt as dt_util -from homeassistant.components import logbook -from homeassistant.setup import setup_component +from homeassistant.components import logbook, recorder +from homeassistant.setup import setup_component, async_setup_component from tests.common import ( init_recorder_component, get_test_home_assistant) @@ -555,3 +555,15 @@ class TestComponentLogbook(unittest.TestCase): 'old_state': state, 'new_state': state, }, time_fired=event_time_fired) + + +async def test_logbook_view(hass, aiohttp_client): + """Test the logbook view.""" + await hass.async_add_job(init_recorder_component, hass) + await async_setup_component(hass, 'logbook', {}) + await hass.components.recorder.wait_connection_ready() + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + client = await aiohttp_client(hass.http.app) + response = await client.get( + '/api/logbook/{}'.format(dt_util.utcnow().isoformat())) + assert response.status == 200 From ff416c0e7ab8861dcb1356d1730ccd46e29391b4 Mon Sep 17 00:00:00 2001 From: maxlaverse Date: Thu, 15 Mar 2018 18:58:11 +0100 Subject: [PATCH 040/924] Try to fix caldav (#13236) * Fix device attribute type for event end * Fix is_over and add tests --- homeassistant/components/calendar/caldav.py | 6 ++- tests/components/calendar/test_caldav.py | 54 ++++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/caldav.py b/homeassistant/components/calendar/caldav.py index d70e7ff8946..6f92891c551 100644 --- a/homeassistant/components/calendar/caldav.py +++ b/homeassistant/components/calendar/caldav.py @@ -194,7 +194,9 @@ class WebDavCalendarData(object): @staticmethod def is_over(vevent): """Return if the event is over.""" - return dt.now() > WebDavCalendarData.get_end_date(vevent) + return dt.now() >= WebDavCalendarData.to_datetime( + WebDavCalendarData.get_end_date(vevent) + ) @staticmethod def get_hass_date(obj): @@ -230,4 +232,4 @@ class WebDavCalendarData(object): else: enddate = obj.dtstart.value + timedelta(days=1) - return WebDavCalendarData.to_datetime(enddate) + return enddate diff --git a/tests/components/calendar/test_caldav.py b/tests/components/calendar/test_caldav.py index e44e5cfc1f0..11dd0cb9635 100644 --- a/tests/components/calendar/test_caldav.py +++ b/tests/components/calendar/test_caldav.py @@ -105,6 +105,20 @@ LOCATION:Hamburg DESCRIPTION:What a day END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:7 +DTSTART;TZID=America/Los_Angeles:20171127T083000 +DTSTAMP:20180301T020053Z +DTEND;TZID=America/Los_Angeles:20171127T093000 +SUMMARY:Enjoy the sun +LOCATION:San Francisco +DESCRIPTION:Sunny day +END:VEVENT +END:VCALENDAR """ ] @@ -225,7 +239,7 @@ class TestComponentsWebDavCalendar(unittest.TestCase): }, _add_device) - @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 45)) def test_ongoing_event(self, mock_now): """Test that the ongoing event is returned.""" cal = caldav.WebDavCalendarEventDevice(self.hass, @@ -244,6 +258,44 @@ class TestComponentsWebDavCalendar(unittest.TestCase): "description": "Surprisingly rainy" }) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 30)) + def test_just_ended_event(self, mock_now): + """Test that the next ongoing event is returned.""" + cal = caldav.WebDavCalendarEventDevice(self.hass, + DEVICE_DATA, + self.calendar) + + self.assertEqual(cal.name, DEVICE_DATA["name"]) + self.assertEqual(cal.state, STATE_ON) + self.assertEqual(cal.device_state_attributes, { + "message": "This is a normal event", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 17:00:00", + "end_time": "2017-11-27 18:00:00", + "location": "Hamburg", + "description": "Surprisingly rainy" + }) + + @patch('homeassistant.util.dt.now', return_value=_local_datetime(17, 00)) + def test_ongoing_event_different_tz(self, mock_now): + """Test that the ongoing event with another timezone is returned.""" + cal = caldav.WebDavCalendarEventDevice(self.hass, + DEVICE_DATA, + self.calendar) + + self.assertEqual(cal.name, DEVICE_DATA["name"]) + self.assertEqual(cal.state, STATE_ON) + self.assertEqual(cal.device_state_attributes, { + "message": "Enjoy the sun", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 16:30:00", + "description": "Sunny day", + "end_time": "2017-11-27 17:30:00", + "location": "San Francisco" + }) + @patch('homeassistant.util.dt.now', return_value=_local_datetime(8, 30)) def test_ongoing_event_with_offset(self, mock_now): """Test that the offset is taken into account.""" From 5e675677ad26b9e8093faf6fb23156b09ae08951 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 15 Mar 2018 20:43:28 +0100 Subject: [PATCH 041/924] Cleanup Sonos platform setup (#13225) * Cleanup Sonos platform setup * Remove unneeded lists --- .../components/media_player/sonos.py | 28 ++++++-------- tests/components/media_player/test_sonos.py | 38 ++++--------------- 2 files changed, 19 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 2a12b59e7c7..e124fbd0443 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -54,7 +54,6 @@ DATA_SONOS = 'sonos' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' -CONF_ADVERTISE_ADDR = 'advertise_addr' CONF_INTERFACE_ADDR = 'interface_addr' # Service call validation schemas @@ -73,7 +72,6 @@ ATTR_IS_COORDINATOR = 'is_coordinator' UPNP_ERRORS_TO_IGNORE = ['701', '711'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ADVERTISE_ADDR): cv.string, vol.Optional(CONF_INTERFACE_ADDR): cv.string, vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [cv.string]), }) @@ -141,10 +139,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() - advertise_addr = config.get(CONF_ADVERTISE_ADDR, None) - if advertise_addr: - soco.config.EVENT_ADVERTISE_IP = advertise_addr - + players = [] if discovery_info: player = soco.SoCo(discovery_info.get('host')) @@ -152,25 +147,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if player.uid in hass.data[DATA_SONOS].uids: return - if player.is_visible: - hass.data[DATA_SONOS].uids.add(player.uid) - add_devices([SonosDevice(player)]) + # If invisible, such as a stereo slave + if not player.is_visible: + return + + players.append(player) else: - players = None - hosts = config.get(CONF_HOSTS, None) + hosts = config.get(CONF_HOSTS) if hosts: # Support retro compatibility with comma separated list of hosts # from config hosts = hosts[0] if len(hosts) == 1 else hosts hosts = hosts.split(',') if isinstance(hosts, str) else hosts - players = [] for host in hosts: try: players.append(soco.SoCo(socket.gethostbyname(host))) except OSError: _LOGGER.warning("Failed to initialize '%s'", host) - - if not players: + else: players = soco.discover( interface_addr=config.get(CONF_INTERFACE_ADDR)) @@ -178,9 +172,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.warning("No Sonos speakers found") return - hass.data[DATA_SONOS].uids.update([p.uid for p in players]) - add_devices([SonosDevice(p) for p in players]) - _LOGGER.debug("Added %s Sonos speakers", len(players)) + hass.data[DATA_SONOS].uids.update(p.uid for p in players) + add_devices(SonosDevice(p) for p in players) + _LOGGER.debug("Added %s Sonos speakers", len(players)) def service_handle(service): """Handle for services.""" diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 3470c79ad64..f741898d15e 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -9,8 +9,7 @@ from soco import alarms from homeassistant.setup import setup_component from homeassistant.components.media_player import sonos, DOMAIN -from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR, \ - CONF_ADVERTISE_ADDR +from homeassistant.components.media_player.sonos import CONF_INTERFACE_ADDR from homeassistant.const import CONF_HOSTS, CONF_PLATFORM from tests.common import get_test_home_assistant @@ -162,7 +161,7 @@ class TestSonosMediaPlayer(unittest.TestCase): 'host': '192.0.2.1' }) - devices = self.hass.data[sonos.DATA_SONOS].devices + devices = list(self.hass.data[sonos.DATA_SONOS].devices) self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') @@ -185,27 +184,6 @@ class TestSonosMediaPlayer(unittest.TestCase): self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) self.assertEqual(discover_mock.call_count, 1) - @mock.patch('soco.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch('soco.discover') - def test_ensure_setup_config_advertise_addr(self, discover_mock, - *args): - """Test an advertise address config'd by the HASS config file.""" - discover_mock.return_value = {SoCoMock('192.0.2.1')} - - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_ADVERTISE_ADDR: '192.0.1.1', - } - } - - assert setup_component(self.hass, DOMAIN, config) - - self.assertEqual(len(self.hass.data[sonos.DATA_SONOS].devices), 1) - self.assertEqual(discover_mock.call_count, 1) - self.assertEqual(soco.config.EVENT_ADVERTISE_IP, '192.0.1.1') - @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('socket.create_connection', side_effect=socket.error()) def test_ensure_setup_config_hosts_string_single(self, *args): @@ -263,7 +241,7 @@ class TestSonosMediaPlayer(unittest.TestCase): def test_ensure_setup_sonos_discovery(self, *args): """Test a single device using the autodiscovery provided by Sonos.""" sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass)) - devices = self.hass.data[sonos.DATA_SONOS].devices + devices = list(self.hass.data[sonos.DATA_SONOS].devices) self.assertEqual(len(devices), 1) self.assertEqual(devices[0].name, 'Kitchen') @@ -275,7 +253,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass device.set_sleep_timer(30) @@ -289,7 +267,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass device.set_sleep_timer(None) @@ -303,7 +281,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass alarm1 = alarms.Alarm(soco_mock) alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, @@ -333,7 +311,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass snapshotMock.return_value = True @@ -351,7 +329,7 @@ class TestSonosMediaPlayer(unittest.TestCase): sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' }) - device = self.hass.data[sonos.DATA_SONOS].devices[-1] + device = list(self.hass.data[sonos.DATA_SONOS].devices)[-1] device.hass = self.hass restoreMock.return_value = True From a86bf81768bc050f49557be4d2da8d9a01ac3f93 Mon Sep 17 00:00:00 2001 From: Juggels Date: Thu, 15 Mar 2018 21:43:20 +0100 Subject: [PATCH 042/924] Fix 'dict' object has no attribute 'strftime' (#13215) * Fix 'dict' object has no attribute 'strftime' * Clear existing list instead of new object --- homeassistant/components/calendar/todoist.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index c5ae1dd3c11..02840c7d0ee 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -496,6 +496,10 @@ class TodoistProjectData(object): # We had no valid tasks return True + # Make sure the task collection is reset to prevent an + # infinite collection repeating the same tasks + self.all_project_tasks.clear() + # Organize the best tasks (so users can see all the tasks # they have, organized) while project_tasks: From 89a19c89a768988a35effe4c0463c3a639304a60 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 13:49:49 -0700 Subject: [PATCH 043/924] Fix aiohttp deprecation warnings (#13240) * Fix aiohttp deprecation warnings * Fix Ring deprecation warning * Lint --- homeassistant/components/ring.py | 4 +- homeassistant/helpers/aiohttp_client.py | 41 ++++++--------- .../components/alexa/test_flash_briefings.py | 4 +- tests/components/alexa/test_intent.py | 4 +- tests/components/alexa/test_smart_home.py | 12 ++--- tests/components/camera/test_generic.py | 12 ++--- tests/components/camera/test_local_file.py | 8 +-- tests/components/camera/test_mqtt.py | 4 +- tests/components/cloud/test_http_api.py | 4 +- .../components/config/test_config_entries.py | 4 +- tests/components/config/test_core.py | 4 +- tests/components/config/test_customize.py | 16 +++--- .../components/config/test_entity_registry.py | 4 +- tests/components/config/test_group.py | 20 +++---- tests/components/config/test_hassbian.py | 8 +-- tests/components/config/test_init.py | 4 +- tests/components/config/test_zwave.py | 4 +- .../device_tracker/test_geofency.py | 4 +- .../device_tracker/test_locative.py | 4 +- .../components/device_tracker/test_meraki.py | 4 +- .../device_tracker/test_owntracks_http.py | 4 +- tests/components/emulated_hue/test_hue_api.py | 4 +- .../google_assistant/test_google_assistant.py | 4 +- tests/components/hassio/conftest.py | 4 +- tests/components/http/test_auth.py | 20 +++---- tests/components/http/test_ban.py | 8 +-- tests/components/http/test_cors.py | 4 +- tests/components/http/test_data_validator.py | 12 ++--- tests/components/http/test_init.py | 10 ++-- tests/components/http/test_real_ip.py | 8 +-- tests/components/mailbox/test_init.py | 4 +- tests/components/mqtt/test_init.py | 4 +- tests/components/notify/test_html5.py | 51 +++++++++--------- tests/components/sensor/test_mhz19.py | 6 +-- tests/components/test_api.py | 4 +- tests/components/test_conversation.py | 12 ++--- tests/components/test_frontend.py | 12 ++--- tests/components/test_prometheus.py | 6 +-- tests/components/test_ring.py | 5 +- tests/components/test_rss_feed_template.py | 4 +- tests/components/test_shopping_list.py | 24 ++++----- tests/components/test_system_log.py | 52 +++++++++---------- tests/components/test_websocket_api.py | 8 +-- tests/helpers/test_aiohttp_client.py | 4 +- tests/test_util/aiohttp.py | 3 +- 45 files changed, 221 insertions(+), 225 deletions(-) diff --git a/homeassistant/components/ring.py b/homeassistant/components/ring.py index 6e70ddb244d..1a15e22fca0 100644 --- a/homeassistant/components/ring.py +++ b/homeassistant/components/ring.py @@ -37,8 +37,8 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Ring component.""" conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] try: from ring_doorbell import Ring diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 72f2214b5e7..bb34942ad79 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -149,34 +149,27 @@ def _async_get_connector(hass, verify_ssl=True): This method must be run in the event loop. """ - is_new = False + key = DATA_CONNECTOR if verify_ssl else DATA_CONNECTOR_NOTVERIFY + + if key in hass.data: + return hass.data[key] if verify_ssl: - if DATA_CONNECTOR not in hass.data: - ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - ssl_context.load_verify_locations(cafile=certifi.where(), - capath=None) - connector = aiohttp.TCPConnector(loop=hass.loop, - ssl_context=ssl_context) - hass.data[DATA_CONNECTOR] = connector - is_new = True - else: - connector = hass.data[DATA_CONNECTOR] + ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ssl_context.load_verify_locations(cafile=certifi.where(), + capath=None) else: - if DATA_CONNECTOR_NOTVERIFY not in hass.data: - connector = aiohttp.TCPConnector(loop=hass.loop, verify_ssl=False) - hass.data[DATA_CONNECTOR_NOTVERIFY] = connector - is_new = True - else: - connector = hass.data[DATA_CONNECTOR_NOTVERIFY] + ssl_context = False - if is_new: - @callback - def _async_close_connector(event): - """Close connector pool.""" - connector.close() + connector = aiohttp.TCPConnector(loop=hass.loop, ssl=ssl_context) + hass.data[key] = connector - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) + @callback + def _async_close_connector(event): + """Close connector pool.""" + connector.close() + + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_CLOSE, _async_close_connector) return connector diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index d9f0c8e156d..d7871e82afc 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -21,7 +21,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, test_client): +def alexa_client(loop, hass, aiohttp_client): """Initialize a Home Assistant server for testing this module.""" @callback def mock_service(call): @@ -49,7 +49,7 @@ def alexa_client(loop, hass, test_client): }, } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) def _flash_briefing_req(client, briefing_id): diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 2c8fafde155..d15c7ccbb34 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -23,7 +23,7 @@ NPR_NEWS_MP3_URL = "https://pd.npr.org/anon.npr-mp3/npr/news/newscast.mp3" @pytest.fixture -def alexa_client(loop, hass, test_client): +def alexa_client(loop, hass, aiohttp_client): """Initialize a Home Assistant server for testing this module.""" @callback def mock_service(call): @@ -95,7 +95,7 @@ def alexa_client(loop, hass, test_client): }, } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) def _intent_req(client, data=None): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 8de4d0d9aff..a5375ba2662 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1199,10 +1199,10 @@ def test_unsupported_domain(hass): @asyncio.coroutine -def do_http_discovery(config, hass, test_client): +def do_http_discovery(config, hass, aiohttp_client): """Submit a request to the Smart Home HTTP API.""" yield from async_setup_component(hass, alexa.DOMAIN, config) - http_client = yield from test_client(hass.http.app) + http_client = yield from aiohttp_client(hass.http.app) request = get_new_request('Alexa.Discovery', 'Discover') response = yield from http_client.post( @@ -1213,7 +1213,7 @@ def do_http_discovery(config, hass, test_client): @asyncio.coroutine -def test_http_api(hass, test_client): +def test_http_api(hass, aiohttp_client): """With `smart_home:` HTTP API is exposed.""" config = { 'alexa': { @@ -1221,7 +1221,7 @@ def test_http_api(hass, test_client): } } - response = yield from do_http_discovery(config, hass, test_client) + response = yield from do_http_discovery(config, hass, aiohttp_client) response_data = yield from response.json() # Here we're testing just the HTTP view glue -- details of discovery are @@ -1230,12 +1230,12 @@ def test_http_api(hass, test_client): @asyncio.coroutine -def test_http_api_disabled(hass, test_client): +def test_http_api_disabled(hass, aiohttp_client): """Without `smart_home:`, the HTTP API is disabled.""" config = { 'alexa': {} } - response = yield from do_http_discovery(config, hass, test_client) + response = yield from do_http_discovery(config, hass, aiohttp_client) assert response.status == 404 diff --git a/tests/components/camera/test_generic.py b/tests/components/camera/test_generic.py index 84eaf107d70..01edca1e996 100644 --- a/tests/components/camera/test_generic.py +++ b/tests/components/camera/test_generic.py @@ -6,7 +6,7 @@ from homeassistant.setup import async_setup_component @asyncio.coroutine -def test_fetching_url(aioclient_mock, hass, test_client): +def test_fetching_url(aioclient_mock, hass, aiohttp_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com', text='hello world') @@ -19,7 +19,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): 'password': 'pass' }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/camera_proxy/camera.config_test') @@ -33,7 +33,7 @@ def test_fetching_url(aioclient_mock, hass, test_client): @asyncio.coroutine -def test_limit_refetch(aioclient_mock, hass, test_client): +def test_limit_refetch(aioclient_mock, hass, aiohttp_client): """Test that it fetches the given url.""" aioclient_mock.get('http://example.com/5a', text='hello world') aioclient_mock.get('http://example.com/10a', text='hello world') @@ -49,7 +49,7 @@ def test_limit_refetch(aioclient_mock, hass, test_client): 'limit_refetch_to_url_change': True, }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/camera_proxy/camera.config_test') @@ -94,7 +94,7 @@ def test_limit_refetch(aioclient_mock, hass, test_client): @asyncio.coroutine -def test_camera_content_type(aioclient_mock, hass, test_client): +def test_camera_content_type(aioclient_mock, hass, aiohttp_client): """Test generic camera with custom content_type.""" svg_image = '' urlsvg = 'https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg' @@ -113,7 +113,7 @@ def test_camera_content_type(aioclient_mock, hass, test_client): yield from async_setup_component(hass, 'camera', { 'camera': [cam_config_svg, cam_config_normal]}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp_1 = yield from client.get('/api/camera_proxy/camera.config_test_svg') assert aioclient_mock.call_count == 1 diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 42ce7bd7add..1098c8c9233 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -12,7 +12,7 @@ from tests.common import mock_registry @asyncio.coroutine -def test_loading_file(hass, test_client): +def test_loading_file(hass, aiohttp_client): """Test that it loads image from disk.""" mock_registry(hass) @@ -25,7 +25,7 @@ def test_loading_file(hass, test_client): 'file_path': 'mock.file', }}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) m_open = MockOpen(read_data=b'hello') with mock.patch( @@ -57,7 +57,7 @@ def test_file_not_readable(hass, caplog): @asyncio.coroutine -def test_camera_content_type(hass, test_client): +def test_camera_content_type(hass, aiohttp_client): """Test local_file camera content_type.""" cam_config_jpg = { 'name': 'test_jpg', @@ -84,7 +84,7 @@ def test_camera_content_type(hass, test_client): 'camera': [cam_config_jpg, cam_config_png, cam_config_svg, cam_config_noext]}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) image = 'hello' m_open = MockOpen(read_data=image.encode()) diff --git a/tests/components/camera/test_mqtt.py b/tests/components/camera/test_mqtt.py index 20d15efd982..d83054d7732 100644 --- a/tests/components/camera/test_mqtt.py +++ b/tests/components/camera/test_mqtt.py @@ -8,7 +8,7 @@ from tests.common import ( @asyncio.coroutine -def test_run_camera_setup(hass, test_client): +def test_run_camera_setup(hass, aiohttp_client): """Test that it fetches the given payload.""" topic = 'test/camera' yield from async_mock_mqtt_component(hass) @@ -24,7 +24,7 @@ def test_run_camera_setup(hass, test_client): async_fire_mqtt_message(hass, topic, 'beer') yield from hass.async_block_till_done() - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get(url) assert resp.status == 200 body = yield from resp.text() diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 98ddebb5db3..1ed3d1b4744 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -12,7 +12,7 @@ from tests.common import mock_coro @pytest.fixture -def cloud_client(hass, test_client): +def cloud_client(hass, aiohttp_client): """Fixture that can fetch from the cloud client.""" with patch('homeassistant.components.cloud.Cloud.async_start', return_value=mock_coro()): @@ -28,7 +28,7 @@ def cloud_client(hass, test_client): hass.data['cloud']._decode_claims = \ lambda token: jwt.get_unverified_claims(token) with patch('homeassistant.components.cloud.Cloud.write_user_info'): - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 84667b8704b..cfe6b12baac 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,11 +17,11 @@ from tests.common import MockConfigEntry, MockModule, mock_coro_func @pytest.fixture -def client(hass, test_client): +def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(config_entries.async_setup(hass)) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 4d82d695f8b..5b52b3d5711 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -8,14 +8,14 @@ from tests.common import mock_coro @asyncio.coroutine -def test_validate_config_ok(hass, test_client): +def test_validate_config_ok(hass, aiohttp_client): """Test checking config.""" with patch.object(config, 'SECTIONS', ['core']): yield from async_setup_component(hass, 'config', {}) yield from asyncio.sleep(0.1, loop=hass.loop) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) with patch( 'homeassistant.components.config.core.async_check_ha_config_file', diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index f12774c25d9..100a18618e6 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -9,12 +9,12 @@ from homeassistant.config import DATA_CUSTOMIZE @asyncio.coroutine -def test_get_entity(hass, test_client): +def test_get_entity(hass, aiohttp_client): """Test getting entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) def mock_read(path): """Mock reading data.""" @@ -38,12 +38,12 @@ def test_get_entity(hass, test_client): @asyncio.coroutine -def test_update_entity(hass, test_client): +def test_update_entity(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) orig_data = { 'hello.beer': { @@ -89,12 +89,12 @@ def test_update_entity(hass, test_client): @asyncio.coroutine -def test_update_entity_invalid_key(hass, test_client): +def test_update_entity_invalid_key(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/customize/config/not_entity', data=json.dumps({ @@ -105,12 +105,12 @@ def test_update_entity_invalid_key(hass, test_client): @asyncio.coroutine -def test_update_entity_invalid_json(hass, test_client): +def test_update_entity_invalid_json(hass, aiohttp_client): """Test updating entity.""" with patch.object(config, 'SECTIONS', ['customize']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/customize/config/hello.beer', data='not json') diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index aa7a5ce5f0e..fd7c6999477 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -8,11 +8,11 @@ from tests.common import mock_registry, MockEntity, MockEntityPlatform @pytest.fixture -def client(hass, test_client): +def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) hass.loop.run_until_complete(entity_registry.async_setup(hass)) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) async def test_get_entity(hass, client): diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index ad28b6eb9b8..06ba2ff1105 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -11,12 +11,12 @@ VIEW_NAME = 'api:config:group:config' @asyncio.coroutine -def test_get_device_config(hass, test_client): +def test_get_device_config(hass, aiohttp_client): """Test getting device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) def mock_read(path): """Mock reading data.""" @@ -40,12 +40,12 @@ def test_get_device_config(hass, test_client): @asyncio.coroutine -def test_update_device_config(hass, test_client): +def test_update_device_config(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) orig_data = { 'hello.beer': { @@ -89,12 +89,12 @@ def test_update_device_config(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_key(hass, test_client): +def test_update_device_config_invalid_key(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/not a slug', data=json.dumps({ @@ -105,12 +105,12 @@ def test_update_device_config_invalid_key(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_data(hass, test_client): +def test_update_device_config_invalid_data(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/hello_beer', data=json.dumps({ @@ -121,12 +121,12 @@ def test_update_device_config_invalid_data(hass, test_client): @asyncio.coroutine -def test_update_device_config_invalid_json(hass, test_client): +def test_update_device_config_invalid_json(hass, aiohttp_client): """Test updating device config.""" with patch.object(config, 'SECTIONS', ['group']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/group/config/hello_beer', data='not json') diff --git a/tests/components/config/test_hassbian.py b/tests/components/config/test_hassbian.py index 9038ccc6aa4..85fbf0c2e5a 100644 --- a/tests/components/config/test_hassbian.py +++ b/tests/components/config/test_hassbian.py @@ -34,13 +34,13 @@ def test_setup_check_env_works(hass, loop): @asyncio.coroutine -def test_get_suites(hass, test_client): +def test_get_suites(hass, aiohttp_client): """Test getting suites.""" with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \ patch.object(config, 'SECTIONS', ['hassbian']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/config/hassbian/suites') assert resp.status == 200 result = yield from resp.json() @@ -53,13 +53,13 @@ def test_get_suites(hass, test_client): @asyncio.coroutine -def test_install_suite(hass, test_client): +def test_install_suite(hass, aiohttp_client): """Test getting suites.""" with patch.dict(os.environ, {'FORCE_HASSBIAN': '1'}), \ patch.object(config, 'SECTIONS', ['hassbian']): yield from async_setup_component(hass, 'config', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/config/hassbian/suites/openzwave/install') assert resp.status == 200 diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 2d5d814ac8a..57ea7e7a492 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -17,7 +17,7 @@ def test_config_setup(hass, loop): @asyncio.coroutine -def test_load_on_demand_already_loaded(hass, test_client): +def test_load_on_demand_already_loaded(hass, aiohttp_client): """Test getting suites.""" mock_component(hass, 'zwave') @@ -34,7 +34,7 @@ def test_load_on_demand_already_loaded(hass, test_client): @asyncio.coroutine -def test_load_on_demand_on_load(hass, test_client): +def test_load_on_demand_on_load(hass, aiohttp_client): """Test getting suites.""" with patch.object(config, 'SECTIONS', []), \ patch.object(config, 'ON_DEMAND', ['zwave']): diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index c98385a3c32..672bafeaf28 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -16,12 +16,12 @@ VIEW_NAME = 'api:config:zwave:device_config' @pytest.fixture -def client(loop, hass, test_client): +def client(loop, hass, aiohttp_client): """Client to communicate with Z-Wave config views.""" with patch.object(config, 'SECTIONS', ['zwave']): loop.run_until_complete(async_setup_component(hass, 'config', {})) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_geofency.py b/tests/components/device_tracker/test_geofency.py index 5def6a217f4..a955dd0cc11 100644 --- a/tests/components/device_tracker/test_geofency.py +++ b/tests/components/device_tracker/test_geofency.py @@ -107,7 +107,7 @@ BEACON_EXIT_CAR = { @pytest.fixture -def geofency_client(loop, hass, test_client): +def geofency_client(loop, hass, aiohttp_client): """Geofency mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -117,7 +117,7 @@ def geofency_client(loop, hass, test_client): }})) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture(autouse=True) diff --git a/tests/components/device_tracker/test_locative.py b/tests/components/device_tracker/test_locative.py index 2476247e069..90adccf7703 100644 --- a/tests/components/device_tracker/test_locative.py +++ b/tests/components/device_tracker/test_locative.py @@ -19,7 +19,7 @@ def _url(data=None): @pytest.fixture -def locative_client(loop, hass, test_client): +def locative_client(loop, hass, aiohttp_client): """Locative mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -29,7 +29,7 @@ def locative_client(loop, hass, test_client): })) with patch('homeassistant.components.device_tracker.update_config'): - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_meraki.py b/tests/components/device_tracker/test_meraki.py index 74fc577bca8..925ba6d66db 100644 --- a/tests/components/device_tracker/test_meraki.py +++ b/tests/components/device_tracker/test_meraki.py @@ -13,7 +13,7 @@ from homeassistant.components.device_tracker.meraki import URL @pytest.fixture -def meraki_client(loop, hass, test_client): +def meraki_client(loop, hass, aiohttp_client): """Meraki mock client.""" assert loop.run_until_complete(async_setup_component( hass, device_tracker.DOMAIN, { @@ -25,7 +25,7 @@ def meraki_client(loop, hass, test_client): } })) - yield loop.run_until_complete(test_client(hass.http.app)) + yield loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/device_tracker/test_owntracks_http.py b/tests/components/device_tracker/test_owntracks_http.py index be8bdd94ecc..d7b48cafe46 100644 --- a/tests/components/device_tracker/test_owntracks_http.py +++ b/tests/components/device_tracker/test_owntracks_http.py @@ -10,7 +10,7 @@ from tests.common import mock_coro, mock_component @pytest.fixture -def mock_client(hass, test_client): +def mock_client(hass, aiohttp_client): """Start the Hass HTTP component.""" mock_component(hass, 'group') mock_component(hass, 'zone') @@ -22,7 +22,7 @@ def mock_client(hass, test_client): 'platform': 'owntracks_http' } })) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 91988a76212..1617f327d27 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -118,7 +118,7 @@ def hass_hue(loop, hass): @pytest.fixture -def hue_client(loop, hass_hue, test_client): +def hue_client(loop, hass_hue, aiohttp_client): """Create web client for emulated hue api.""" web_app = hass_hue.http.app config = Config(None, { @@ -135,7 +135,7 @@ def hue_client(loop, hass_hue, test_client): HueOneLightStateView(config).register(web_app.router) HueOneLightChangeView(config).register(web_app.router) - return loop.run_until_complete(test_client(web_app)) + return loop.run_until_complete(aiohttp_client(web_app)) @asyncio.coroutine diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index cb319b67bb2..d45680d132e 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -27,7 +27,7 @@ AUTH_HEADER = {AUTHORIZATION: 'Bearer {}'.format(ACCESS_TOKEN)} @pytest.fixture -def assistant_client(loop, hass, test_client): +def assistant_client(loop, hass, aiohttp_client): """Create web client for the Google Assistant API.""" loop.run_until_complete( setup.async_setup_component(hass, 'google_assistant', { @@ -44,7 +44,7 @@ def assistant_client(loop, hass, test_client): } })) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 56d6cbe666e..9f20efc08a5 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -26,7 +26,7 @@ def hassio_env(): @pytest.fixture -def hassio_client(hassio_env, hass, test_client): +def hassio_client(hassio_env, hass, aiohttp_client): """Create mock hassio http client.""" with patch('homeassistant.components.hassio.HassIO.update_hass_api', Mock(return_value=mock_coro({"result": "ok"}))), \ @@ -38,7 +38,7 @@ def hassio_client(hassio_env, hass, test_client): 'api_password': API_PASSWORD } })) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 604ee9c0c9b..a44d17d513d 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -55,19 +55,19 @@ async def test_auth_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_access_without_password(app, test_client): +async def test_access_without_password(app, aiohttp_client): """Test access without password.""" setup_auth(app, [], None) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/') assert resp.status == 200 -async def test_access_with_password_in_header(app, test_client): +async def test_access_with_password_in_header(app, aiohttp_client): """Test access with password in URL.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) req = await client.get( '/', headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) @@ -78,10 +78,10 @@ async def test_access_with_password_in_header(app, test_client): assert req.status == 401 -async def test_access_with_password_in_query(app, test_client): +async def test_access_with_password_in_query(app, aiohttp_client): """Test access without password.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) resp = await client.get('/', params={ 'api_password': API_PASSWORD @@ -97,10 +97,10 @@ async def test_access_with_password_in_query(app, test_client): assert resp.status == 401 -async def test_basic_auth_works(app, test_client): +async def test_basic_auth_works(app, aiohttp_client): """Test access with basic authentication.""" setup_auth(app, [], API_PASSWORD) - client = await test_client(app) + client = await aiohttp_client(app) req = await client.get( '/', @@ -125,7 +125,7 @@ async def test_basic_auth_works(app, test_client): assert req.status == 401 -async def test_access_with_trusted_ip(test_client): +async def test_access_with_trusted_ip(aiohttp_client): """Test access with an untrusted ip address.""" app = web.Application() app.router.add_get('/', mock_handler) @@ -133,7 +133,7 @@ async def test_access_with_trusted_ip(test_client): setup_auth(app, TRUSTED_NETWORKS, 'some-pass') set_mock_ip = mock_real_ip(app) - client = await test_client(app) + client = await aiohttp_client(app) for remote_addr in UNTRUSTED_ADDRESSES: set_mock_ip(remote_addr) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 2d7885d959f..c5691cf3e2a 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -15,7 +15,7 @@ from . import mock_real_ip BANNED_IPS = ['200.201.202.203', '100.64.0.2'] -async def test_access_from_banned_ip(hass, test_client): +async def test_access_from_banned_ip(hass, aiohttp_client): """Test accessing to server from banned IP. Both trusted and not.""" app = web.Application() setup_bans(hass, app, 5) @@ -24,7 +24,7 @@ async def test_access_from_banned_ip(hass, test_client): with patch('homeassistant.components.http.ban.load_ip_bans_config', return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS]): - client = await test_client(app) + client = await aiohttp_client(app) for remote_addr in BANNED_IPS: set_real_ip(remote_addr) @@ -54,7 +54,7 @@ async def test_ban_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_ip_bans_file_creation(hass, test_client): +async def test_ip_bans_file_creation(hass, aiohttp_client): """Testing if banned IP file created.""" app = web.Application() app['hass'] = hass @@ -70,7 +70,7 @@ async def test_ip_bans_file_creation(hass, test_client): with patch('homeassistant.components.http.ban.load_ip_bans_config', return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS]): - client = await test_client(app) + client = await aiohttp_client(app) m = mock_open() diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 50464b36277..27367b4173e 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -47,12 +47,12 @@ async def mock_handler(request): @pytest.fixture -def client(loop, test_client): +def client(loop, aiohttp_client): """Fixture to setup a web.Application.""" app = web.Application() app.router.add_get('/', mock_handler) setup_cors(app, [TRUSTED_ORIGIN]) - return loop.run_until_complete(test_client(app)) + return loop.run_until_complete(aiohttp_client(app)) async def test_cors_requests(client): diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index 6cca1af8ccc..2b966daff6c 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -8,7 +8,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -async def get_client(test_client, validator): +async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" app = web.Application() app['hass'] = Mock(is_running=True) @@ -24,14 +24,14 @@ async def get_client(test_client, validator): return b'' TestView().register(app.router) - client = await test_client(app) + client = await aiohttp_client(app) return client -async def test_validator(test_client): +async def test_validator(aiohttp_client): """Test the validator.""" client = await get_client( - test_client, RequestDataValidator(vol.Schema({ + aiohttp_client, RequestDataValidator(vol.Schema({ vol.Required('test'): str }))) @@ -49,10 +49,10 @@ async def test_validator(test_client): assert resp.status == 400 -async def test_validator_allow_empty(test_client): +async def test_validator_allow_empty(aiohttp_client): """Test the validator with empty data.""" client = await get_client( - test_client, RequestDataValidator(vol.Schema({ + aiohttp_client, RequestDataValidator(vol.Schema({ # Although we allow empty, our schema should still be able # to validate an empty dict. vol.Optional('test'): str diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 1dcf45f48c3..c02e203444f 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -15,12 +15,13 @@ class TestView(http.HomeAssistantView): return 'hello' -async def test_registering_view_while_running(hass, test_client, unused_port): +async def test_registering_view_while_running(hass, aiohttp_client, + aiohttp_unused_port): """Test that we can register a view while the server is running.""" await async_setup_component( hass, http.DOMAIN, { http.DOMAIN: { - http.CONF_SERVER_PORT: unused_port(), + http.CONF_SERVER_PORT: aiohttp_unused_port(), } } ) @@ -73,17 +74,16 @@ async def test_api_no_base_url(hass): assert hass.config.api.base_url == 'http://127.0.0.1:8123' -async def test_not_log_password(hass, unused_port, test_client, caplog): +async def test_not_log_password(hass, aiohttp_client, caplog): """Test access with password doesn't get logged.""" result = await async_setup_component(hass, 'api', { 'http': { - http.CONF_SERVER_PORT: unused_port(), http.CONF_API_PASSWORD: 'some-pass' } }) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.get('/api/', params={ 'api_password': 'some-pass' diff --git a/tests/components/http/test_real_ip.py b/tests/components/http/test_real_ip.py index 3e4f9023537..61846eb94c2 100644 --- a/tests/components/http/test_real_ip.py +++ b/tests/components/http/test_real_ip.py @@ -11,13 +11,13 @@ async def mock_handler(request): return web.Response(text=str(request[KEY_REAL_IP])) -async def test_ignore_x_forwarded_for(test_client): +async def test_ignore_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) setup_real_ip(app, False) - mock_api_client = await test_client(app) + mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get('/', headers={ X_FORWARDED_FOR: '255.255.255.255' @@ -27,13 +27,13 @@ async def test_ignore_x_forwarded_for(test_client): assert text != '255.255.255.255' -async def test_use_x_forwarded_for(test_client): +async def test_use_x_forwarded_for(aiohttp_client): """Test that we get the IP from the transport.""" app = web.Application() app.router.add_get('/', mock_handler) setup_real_ip(app, True) - mock_api_client = await test_client(app) + mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get('/', headers={ X_FORWARDED_FOR: '255.255.255.255' diff --git a/tests/components/mailbox/test_init.py b/tests/components/mailbox/test_init.py index c9267fa8e8e..3377fcefcf5 100644 --- a/tests/components/mailbox/test_init.py +++ b/tests/components/mailbox/test_init.py @@ -9,7 +9,7 @@ import homeassistant.components.mailbox as mailbox @pytest.fixture -def mock_http_client(hass, test_client): +def mock_http_client(hass, aiohttp_client): """Start the Hass HTTP component.""" config = { mailbox.DOMAIN: { @@ -18,7 +18,7 @@ def mock_http_client(hass, test_client): } hass.loop.run_until_complete( async_setup_component(hass, mailbox.DOMAIN, config)) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1dd89a92f04..b25479bb75a 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -59,7 +59,7 @@ class TestMQTTComponent(unittest.TestCase): """Helper for recording calls.""" self.calls.append(args) - def test_client_stops_on_home_assistant_start(self): + def aiohttp_client_stops_on_home_assistant_start(self): """Test if client stops on HA stop.""" self.hass.bus.fire(EVENT_HOMEASSISTANT_STOP) self.hass.block_till_done() @@ -156,7 +156,7 @@ class TestMQTTCallbacks(unittest.TestCase): """Helper for recording calls.""" self.calls.append(args) - def test_client_starts_on_home_assistant_mqtt_setup(self): + def aiohttp_client_starts_on_home_assistant_mqtt_setup(self): """Test if client is connected after mqtt init on bootstrap.""" self.assertEqual(self.hass.data['mqtt']._mqttc.connect.call_count, 1) diff --git a/tests/components/notify/test_html5.py b/tests/components/notify/test_html5.py index 9ec71020ef1..318f3c7512c 100644 --- a/tests/components/notify/test_html5.py +++ b/tests/components/notify/test_html5.py @@ -49,7 +49,7 @@ REGISTER_URL = '/api/notify.html5' PUBLISH_URL = '/api/notify.html5/callback' -async def mock_client(hass, test_client, registrations=None): +async def mock_client(hass, aiohttp_client, registrations=None): """Create a test client for HTML5 views.""" if registrations is None: registrations = {} @@ -62,7 +62,7 @@ async def mock_client(hass, test_client, registrations=None): } }) - return await test_client(hass.http.app) + return await aiohttp_client(hass.http.app) class TestHtml5Notify(object): @@ -151,9 +151,9 @@ class TestHtml5Notify(object): assert mock_wp.mock_calls[4][2]['gcm_key'] is None -async def test_registering_new_device_view(hass, test_client): +async def test_registering_new_device_view(hass, aiohttp_client): """Test that the HTML view works.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -165,9 +165,9 @@ async def test_registering_new_device_view(hass, test_client): } -async def test_registering_new_device_expiration_view(hass, test_client): +async def test_registering_new_device_expiration_view(hass, aiohttp_client): """Test that the HTML view works.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_4)) @@ -178,10 +178,10 @@ async def test_registering_new_device_expiration_view(hass, test_client): } -async def test_registering_new_device_fails_view(hass, test_client): +async def test_registering_new_device_fails_view(hass, aiohttp_client): """Test subs. are not altered when registering a new device fails.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json', side_effect=HomeAssistantError()): @@ -191,10 +191,10 @@ async def test_registering_new_device_fails_view(hass, test_client): assert registrations == {} -async def test_registering_existing_device_view(hass, test_client): +async def test_registering_existing_device_view(hass, aiohttp_client): """Test subscription is updated when registering existing device.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -209,10 +209,10 @@ async def test_registering_existing_device_view(hass, test_client): } -async def test_registering_existing_device_fails_view(hass, test_client): +async def test_registering_existing_device_fails_view(hass, aiohttp_client): """Test sub. is not updated when registering existing device fails.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: await client.post(REGISTER_URL, data=json.dumps(SUBSCRIPTION_1)) @@ -225,9 +225,9 @@ async def test_registering_existing_device_fails_view(hass, test_client): } -async def test_registering_new_device_validation(hass, test_client): +async def test_registering_new_device_validation(hass, aiohttp_client): """Test various errors when registering a new device.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) resp = await client.post(REGISTER_URL, data=json.dumps({ 'browser': 'invalid browser', @@ -249,13 +249,13 @@ async def test_registering_new_device_validation(hass, test_client): assert resp.status == 400 -async def test_unregistering_device_view(hass, test_client): +async def test_unregistering_device_view(hass, aiohttp_client): """Test that the HTML unregister view works.""" registrations = { 'some device': SUBSCRIPTION_1, 'other device': SUBSCRIPTION_2, } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.delete(REGISTER_URL, data=json.dumps({ @@ -269,11 +269,11 @@ async def test_unregistering_device_view(hass, test_client): } -async def test_unregister_device_view_handle_unknown_subscription(hass, - test_client): +async def test_unregister_device_view_handle_unknown_subscription( + hass, aiohttp_client): """Test that the HTML unregister view handles unknown subscriptions.""" registrations = {} - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json') as mock_save: resp = await client.delete(REGISTER_URL, data=json.dumps({ @@ -285,13 +285,14 @@ async def test_unregister_device_view_handle_unknown_subscription(hass, assert len(mock_save.mock_calls) == 0 -async def test_unregistering_device_view_handles_save_error(hass, test_client): +async def test_unregistering_device_view_handles_save_error( + hass, aiohttp_client): """Test that the HTML unregister view handles save errors.""" registrations = { 'some device': SUBSCRIPTION_1, 'other device': SUBSCRIPTION_2, } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('homeassistant.components.notify.html5.save_json', side_effect=HomeAssistantError()): @@ -306,9 +307,9 @@ async def test_unregistering_device_view_handles_save_error(hass, test_client): } -async def test_callback_view_no_jwt(hass, test_client): +async def test_callback_view_no_jwt(hass, aiohttp_client): """Test that the notification callback view works without JWT.""" - client = await mock_client(hass, test_client) + client = await mock_client(hass, aiohttp_client) resp = await client.post(PUBLISH_URL, data=json.dumps({ 'type': 'push', 'tag': '3bc28d69-0921-41f1-ac6a-7a627ba0aa72' @@ -317,12 +318,12 @@ async def test_callback_view_no_jwt(hass, test_client): assert resp.status == 401, resp.response -async def test_callback_view_with_jwt(hass, test_client): +async def test_callback_view_with_jwt(hass, aiohttp_client): """Test that the notification callback view works with JWT.""" registrations = { 'device': SUBSCRIPTION_1 } - client = await mock_client(hass, test_client, registrations) + client = await mock_client(hass, aiohttp_client, registrations) with patch('pywebpush.WebPusher') as mock_wp: await hass.services.async_call('notify', 'notify', { diff --git a/tests/components/sensor/test_mhz19.py b/tests/components/sensor/test_mhz19.py index 6948a952c31..6d071489691 100644 --- a/tests/components/sensor/test_mhz19.py +++ b/tests/components/sensor/test_mhz19.py @@ -52,7 +52,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', side_effect=OSError('test error')) - def test_client_update_oserror(self, mock_function): + def aiohttp_client_update_oserror(self, mock_function): """Test MHZClient when library throws OSError.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') @@ -61,7 +61,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', return_value=(5001, 24)) - def test_client_update_ppm_overflow(self, mock_function): + def aiohttp_client_update_ppm_overflow(self, mock_function): """Test MHZClient when ppm is too high.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') @@ -70,7 +70,7 @@ class TestMHZ19Sensor(unittest.TestCase): @patch('pmsensor.co2sensor.read_mh_z19_with_temperature', return_value=(1000, 24)) - def test_client_update_good_read(self, mock_function): + def aiohttp_client_update_good_read(self, mock_function): """Test MHZClient when ppm is too high.""" from pmsensor import co2sensor client = mhz19.MHZClient(co2sensor, 'test.serial') diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 69b9bfa69de..6d5bec046f1 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -11,10 +11,10 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_api_client(hass, test_client): +def mock_api_client(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'api', {})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index 466dc57017a..bde00e10928 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -93,7 +93,7 @@ def test_register_before_setup(hass): @asyncio.coroutine -def test_http_processing_intent(hass, test_client): +def test_http_processing_intent(hass, aiohttp_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): intent_type = 'OrderBeer' @@ -122,7 +122,7 @@ def test_http_processing_intent(hass, test_client): }) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) @@ -224,7 +224,7 @@ def test_toggle_intent(hass, sentence): @asyncio.coroutine -def test_http_api(hass, test_client): +def test_http_api(hass, aiohttp_client): """Test the HTTP conversation API.""" result = yield from component.async_setup(hass, {}) assert result @@ -232,7 +232,7 @@ def test_http_api(hass, test_client): result = yield from async_setup_component(hass, 'conversation', {}) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') @@ -249,7 +249,7 @@ def test_http_api(hass, test_client): @asyncio.coroutine -def test_http_api_wrong_data(hass, test_client): +def test_http_api_wrong_data(hass, aiohttp_client): """Test the HTTP conversation API.""" result = yield from component.async_setup(hass, {}) assert result @@ -257,7 +257,7 @@ def test_http_api_wrong_data(hass, test_client): result = yield from async_setup_component(hass, 'conversation', {}) assert result - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/conversation/process', json={ 'text': 123 diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index c4ade7f5c19..c742e215738 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -12,14 +12,14 @@ from homeassistant.components.frontend import ( @pytest.fixture -def mock_http_client(hass, test_client): +def mock_http_client(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', {})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_themes(hass, test_client): +def mock_http_client_with_themes(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { DOMAIN: { @@ -29,11 +29,11 @@ def mock_http_client_with_themes(hass, test_client): } } }})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture -def mock_http_client_with_urls(hass, test_client): +def mock_http_client_with_urls(hass, aiohttp_client): """Start the Hass HTTP component.""" hass.loop.run_until_complete(async_setup_component(hass, 'frontend', { DOMAIN: { @@ -42,7 +42,7 @@ def mock_http_client_with_urls(hass, test_client): CONF_EXTRA_HTML_URL_ES5: ["https://domain.com/my_extra_url_es5.html"] }})) - return hass.loop.run_until_complete(test_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index 052292b015d..6cc0e4fcada 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -7,14 +7,14 @@ import homeassistant.components.prometheus as prometheus @pytest.fixture -def prometheus_client(loop, hass, test_client): - """Initialize a test_client with Prometheus component.""" +def prometheus_client(loop, hass, aiohttp_client): + """Initialize a aiohttp_client with Prometheus component.""" assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, {}, )) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_ring.py b/tests/components/test_ring.py index 819f447f2f5..3837ec13061 100644 --- a/tests/components/test_ring.py +++ b/tests/components/test_ring.py @@ -1,4 +1,5 @@ """The tests for the Ring component.""" +from copy import deepcopy import os import unittest import requests_mock @@ -51,7 +52,7 @@ class TestRing(unittest.TestCase): """Test the setup when no login is configured.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) - conf = self.config.copy() + conf = deepcopy(VALID_CONFIG) del conf['ring']['username'] assert not setup.setup_component(self.hass, ring.DOMAIN, conf) @@ -60,6 +61,6 @@ class TestRing(unittest.TestCase): """Test the setup when no password is configured.""" mock.post('https://api.ring.com/clients_api/session', text=load_fixture('ring_session.json')) - conf = self.config.copy() + conf = deepcopy(VALID_CONFIG) del conf['ring']['password'] assert not setup.setup_component(self.hass, ring.DOMAIN, conf) diff --git a/tests/components/test_rss_feed_template.py b/tests/components/test_rss_feed_template.py index 8b16b5519e9..36f68e57c9f 100644 --- a/tests/components/test_rss_feed_template.py +++ b/tests/components/test_rss_feed_template.py @@ -8,7 +8,7 @@ from homeassistant.setup import async_setup_component @pytest.fixture -def mock_http_client(loop, hass, test_client): +def mock_http_client(loop, hass, aiohttp_client): """Setup test fixture.""" config = { 'rss_feed_template': { @@ -21,7 +21,7 @@ def mock_http_client(loop, hass, test_client): loop.run_until_complete(async_setup_component(hass, 'rss_feed_template', config)) - return loop.run_until_complete(test_client(hass.http.app)) + return loop.run_until_complete(aiohttp_client(hass.http.app)) @asyncio.coroutine diff --git a/tests/components/test_shopping_list.py b/tests/components/test_shopping_list.py index 4203f7587ae..3131ae092a3 100644 --- a/tests/components/test_shopping_list.py +++ b/tests/components/test_shopping_list.py @@ -54,7 +54,7 @@ def test_recent_items_intent(hass): @asyncio.coroutine -def test_api_get_all(hass, test_client): +def test_api_get_all(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -65,7 +65,7 @@ def test_api_get_all(hass, test_client): hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'wine'}} ) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/shopping_list') assert resp.status == 200 @@ -78,7 +78,7 @@ def test_api_get_all(hass, test_client): @asyncio.coroutine -def test_api_update(hass, test_client): +def test_api_update(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -92,7 +92,7 @@ def test_api_update(hass, test_client): beer_id = hass.data['shopping_list'].items[0]['id'] wine_id = hass.data['shopping_list'].items[1]['id'] - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/shopping_list/item/{}'.format(beer_id), json={ 'name': 'soda' @@ -133,7 +133,7 @@ def test_api_update(hass, test_client): @asyncio.coroutine -def test_api_update_fails(hass, test_client): +def test_api_update_fails(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -141,7 +141,7 @@ def test_api_update_fails(hass, test_client): hass, 'test', 'HassShoppingListAddItem', {'item': {'value': 'beer'}} ) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post( '/api/shopping_list/non_existing', json={ 'name': 'soda' @@ -159,7 +159,7 @@ def test_api_update_fails(hass, test_client): @asyncio.coroutine -def test_api_clear_completed(hass, test_client): +def test_api_clear_completed(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) @@ -173,7 +173,7 @@ def test_api_clear_completed(hass, test_client): beer_id = hass.data['shopping_list'].items[0]['id'] wine_id = hass.data['shopping_list'].items[1]['id'] - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) # Mark beer as completed resp = yield from client.post( @@ -196,11 +196,11 @@ def test_api_clear_completed(hass, test_client): @asyncio.coroutine -def test_api_create(hass, test_client): +def test_api_create(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/shopping_list/item', json={ 'name': 'soda' }) @@ -217,11 +217,11 @@ def test_api_create(hass, test_client): @asyncio.coroutine -def test_api_create_fail(hass, test_client): +def test_api_create_fail(hass, aiohttp_client): """Test the API.""" yield from async_setup_component(hass, 'shopping_list', {}) - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.post('/api/shopping_list/item', json={ 'name': 1234 }) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index d119c60dba2..c440ef9c30c 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -14,16 +14,16 @@ _LOGGER = logging.getLogger('test_logger') @pytest.fixture(autouse=True) @asyncio.coroutine -def setup_test_case(hass, test_client): +def setup_test_case(hass, aiohttp_client): """Setup system_log component before test case.""" config = {'system_log': {'max_entries': 2}} yield from async_setup_component(hass, system_log.DOMAIN, config) @asyncio.coroutine -def get_error_log(hass, test_client, expected_count): +def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from test_client(hass.http.app) + client = yield from aiohttp_client(hass.http.app) resp = yield from client.get('/api/error/all') assert resp.status == 200 @@ -53,41 +53,41 @@ def get_frame(name): @asyncio.coroutine -def test_normal_logs(hass, test_client): +def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, test_client, 0) + yield from get_error_log(hass, aiohttp_client, 0) @asyncio.coroutine -def test_exception(hass, test_client): +def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') @asyncio.coroutine -def test_warning(hass, test_client): +def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') @asyncio.coroutine -def test_error(hass, test_client): +def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" _LOGGER.error('error message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') @asyncio.coroutine -def test_error_posted_as_event(hass, test_client): +def test_error_posted_as_event(hass, aiohttp_client): """Test that error are posted as events.""" events = [] @@ -106,26 +106,26 @@ def test_error_posted_as_event(hass, test_client): @asyncio.coroutine -def test_critical(hass, test_client): +def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') @asyncio.coroutine -def test_remove_older_logs(hass, test_client): +def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, test_client, 2) + log = yield from get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') @asyncio.coroutine -def test_clear_logs(hass, test_client): +def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" _LOGGER.error('error message') @@ -135,7 +135,7 @@ def test_clear_logs(hass, test_client): yield from hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, test_client, 0) + yield from get_error_log(hass, aiohttp_client, 0) @asyncio.coroutine @@ -182,12 +182,12 @@ def test_write_choose_level(hass): @asyncio.coroutine -def test_unknown_path(hass, test_client): +def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -207,30 +207,30 @@ def log_error_from_test_path(path): @asyncio.coroutine -def test_homeassistant_path(hass, test_client): +def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' @asyncio.coroutine -def test_config_path(hass, test_client): +def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' @asyncio.coroutine -def test_netdisco_path(hass, test_client): +def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, test_client, 1))[0] + log = (yield from get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index d0c129e512e..4deccf65209 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -16,12 +16,12 @@ API_PASSWORD = 'test1234' @pytest.fixture -def websocket_client(loop, hass, test_client): +def websocket_client(loop, hass, aiohttp_client): """Websocket client fixture connected to websocket server.""" assert loop.run_until_complete( async_setup_component(hass, 'websocket_api')) - client = loop.run_until_complete(test_client(hass.http.app)) + client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(wapi.URL)) auth_ok = loop.run_until_complete(ws.receive_json()) assert auth_ok['type'] == wapi.TYPE_AUTH_OK @@ -33,7 +33,7 @@ def websocket_client(loop, hass, test_client): @pytest.fixture -def no_auth_websocket_client(hass, loop, test_client): +def no_auth_websocket_client(hass, loop, aiohttp_client): """Websocket connection that requires authentication.""" assert loop.run_until_complete( async_setup_component(hass, 'websocket_api', { @@ -42,7 +42,7 @@ def no_auth_websocket_client(hass, loop, test_client): } })) - client = loop.run_until_complete(test_client(hass.http.app)) + client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(wapi.URL)) auth_ok = loop.run_until_complete(ws.receive_json()) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index abe30d80a49..28bb31c8482 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -14,7 +14,7 @@ from tests.common import get_test_home_assistant @pytest.fixture -def camera_client(hass, test_client): +def camera_client(hass, aiohttp_client): """Fixture to fetch camera streams.""" assert hass.loop.run_until_complete(async_setup_component(hass, 'camera', { 'camera': { @@ -23,7 +23,7 @@ def camera_client(hass, test_client): 'mjpeg_url': 'http://example.com/mjpeg_stream', }})) - yield hass.loop.run_until_complete(test_client(hass.http.app)) + yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) class TestHelpersAiohttpClient(unittest.TestCase): diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d661ffba477..e67d5de50d1 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -82,7 +82,8 @@ class AiohttpClientMocker: def create_session(self, loop): """Create a ClientSession that is bound to this mocker.""" session = ClientSession(loop=loop) - session._request = self.match_request + # Setting directly on `session` will raise deprecation warning + object.__setattr__(session, '_request', self.match_request) return session async def match_request(self, method, url, *, data=None, auth=None, From 456ff4e84b6a6b2160921824e62911f5e09573a4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 13:53:59 -0700 Subject: [PATCH 044/924] Tado: don't reference unset hass var (#13237) Tado: don't reference unset hass var --- homeassistant/components/device_tracker/tado.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index 11d12322ff5..ef816338ce9 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner): last_results = [] try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): # Format the URL here, so we can log the template URL if # anything goes wrong without exposing username and password. url = self.tadoapiurl.format( From d13bcf8412a74de965ab3bede8c85483c12207f5 Mon Sep 17 00:00:00 2001 From: Gerard Date: Thu, 15 Mar 2018 22:56:35 +0100 Subject: [PATCH 045/924] Add extra sensors for BMW ConnectedDrive (#12591) * Added extra sensors for BMW ConnectedDrive * Updates based on review of @MartinHjelmare * Updates based on 2nd review of @MartinHjelmare * Changed control flow for updates to support updates triggered by remote services. * updated library version number * Changed order of commands so that the UI looks consistent. State of lock is now set optimisitcally before getting proper update from the server. So that the state does not toggle in the UI. * Added comment on optimistic state * Updated requirements_all.txt * Revert access permission changes * Fix for Travis * Changes based on review by @MartinHjelmare --- .../binary_sensor/bmw_connected_drive.py | 117 ++++++++++++++++++ .../components/bmw_connected_drive.py | 2 +- .../components/lock/bmw_connected_drive.py | 108 ++++++++++++++++ .../components/sensor/bmw_connected_drive.py | 49 +++++--- 4 files changed, 259 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/binary_sensor/bmw_connected_drive.py create mode 100644 homeassistant/components/lock/bmw_connected_drive.py diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py new file mode 100644 index 00000000000..0c848a57fbf --- /dev/null +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -0,0 +1,117 @@ +""" +Reads vehicle status from BMW connected drive portal. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ +""" +import asyncio +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.binary_sensor import BinarySensorDevice + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + +SENSOR_TYPES = { + 'all_lids_closed': ['Doors', 'opening'], + 'all_windows_closed': ['Windows', 'opening'], + 'door_lock_state': ['Door lock state', 'safety'] +} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the BMW sensors.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + for key, value in sorted(SENSOR_TYPES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) + add_devices(devices, True) + + +class BMWConnectedDriveSensor(BinarySensorDevice): + """Representation of a BMW vehicle binary sensor.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name, + device_class): + """Constructor.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = sensor_name + self._device_class = device_class + self._state = None + + @property + def should_poll(self) -> bool: + """Data update is triggered from BMWConnectedDriveEntity.""" + return False + + @property + def name(self): + """Return the name of the binary sensor.""" + return self._name + + @property + def device_class(self): + """Return the class of the binary sensor.""" + return self._device_class + + @property + def is_on(self): + """Return the state of the binary sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + vehicle_state = self._vehicle.state + result = { + 'car': self._vehicle.modelName + } + + if self._attribute == 'all_lids_closed': + for lid in vehicle_state.lids: + result[lid.name] = lid.state.value + elif self._attribute == 'all_windows_closed': + for window in vehicle_state.windows: + result[window.name] = window.state.value + elif self._attribute == 'door_lock_state': + result['door_lock_state'] = vehicle_state.door_lock_state.value + + return result + + def update(self): + """Read new state data from the library.""" + vehicle_state = self._vehicle.state + + # device class opening: On means open, Off means closed + if self._attribute == 'all_lids_closed': + _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) + self._state = not vehicle_state.all_lids_closed + if self._attribute == 'all_windows_closed': + self._state = not vehicle_state.all_windows_closed + # device class safety: On means unsafe, Off means safe + if self._attribute == 'door_lock_state': + # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED + self._state = bool(vehicle_state.door_lock_state.value + in ('SELECTIVELOCKED', 'UNLOCKED')) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + @asyncio.coroutine + def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py index 86048a56e22..9e9e2bafac5 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive.py @@ -37,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -BMW_COMPONENTS = ['device_tracker', 'sensor'] +BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor'] UPDATE_INTERVAL = 5 # in minutes diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py new file mode 100644 index 00000000000..4592fd7cae9 --- /dev/null +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -0,0 +1,108 @@ +""" +Support for BMW cars with BMW ConnectedDrive. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/lock.bmw_connected_drive/ +""" +import asyncio +import logging + +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN +from homeassistant.components.lock import LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED + +DEPENDENCIES = ['bmw_connected_drive'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the BMW Connected Drive lock.""" + accounts = hass.data[BMW_DOMAIN] + _LOGGER.debug('Found BMW accounts: %s', + ', '.join([a.name for a in accounts])) + devices = [] + for account in accounts: + for vehicle in account.account.vehicles: + device = BMWLock(account, vehicle, 'lock', 'BMW lock') + devices.append(device) + add_devices(devices, True) + + +class BMWLock(LockDevice): + """Representation of a BMW vehicle lock.""" + + def __init__(self, account, vehicle, attribute: str, sensor_name): + """Initialize the lock.""" + self._account = account + self._vehicle = vehicle + self._attribute = attribute + self._name = sensor_name + self._state = None + + @property + def should_poll(self): + """Do not poll this class. + + Updates are triggered from BMWConnectedDriveAccount. + """ + return False + + @property + def name(self): + """Return the name of the lock.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the lock.""" + vehicle_state = self._vehicle.state + return { + 'car': self._vehicle.modelName, + 'door_lock_state': vehicle_state.door_lock_state.value + } + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + def lock(self, **kwargs): + """Lock the car.""" + _LOGGER.debug("%s: locking doors", self._vehicle.modelName) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_LOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_lock() + + def unlock(self, **kwargs): + """Unlock the car.""" + _LOGGER.debug("%s: unlocking doors", self._vehicle.modelName) + # Optimistic state set here because it takes some time before the + # update callback response + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + self._vehicle.remote_services.trigger_remote_door_unlock() + + def update(self): + """Update state of the lock.""" + _LOGGER.debug("%s: updating data for %s", self._vehicle.modelName, + self._attribute) + vehicle_state = self._vehicle.state + + # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED + self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value + in ('LOCKED', 'SECURED') else STATE_UNLOCKED) + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + @asyncio.coroutine + def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 26bfd19e6fc..76719763931 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -14,14 +14,16 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -LENGTH_ATTRIBUTES = [ - 'remaining_range_fuel', - 'mileage', - ] +LENGTH_ATTRIBUTES = { + 'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'], + 'mileage': ['Mileage', 'mdi:speedometer'] +} -VALID_ATTRIBUTES = LENGTH_ATTRIBUTES + [ - 'remaining_fuel', -] +VALID_ATTRIBUTES = { + 'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station'] +} + +VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -32,23 +34,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for sensor in VALID_ATTRIBUTES: - device = BMWConnectedDriveSensor(account, vehicle, sensor) + for key, value in sorted(VALID_ATTRIBUTES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) devices.append(device) - add_devices(devices) + add_devices(devices, True) class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str): + def __init__(self, account, vehicle, attribute: str, sensor_name, icon): """Constructor.""" self._vehicle = vehicle self._account = account self._attribute = attribute self._state = None self._unit_of_measurement = None - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = sensor_name + self._icon = icon @property def should_poll(self) -> bool: @@ -60,6 +64,11 @@ class BMWConnectedDriveSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + @property def state(self): """Return the state of the sensor. @@ -74,9 +83,16 @@ class BMWConnectedDriveSensor(Entity): """Get the unit of measurement.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return { + 'car': self._vehicle.modelName + } + def update(self) -> None: """Read new state data from the library.""" - _LOGGER.debug('Updating %s', self.entity_id) + _LOGGER.debug('Updating %s', self._vehicle.modelName) vehicle_state = self._vehicle.state self._state = getattr(vehicle_state, self._attribute) @@ -87,7 +103,9 @@ class BMWConnectedDriveSensor(Entity): else: self._unit_of_measurement = None - self.schedule_update_ha_state() + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) @asyncio.coroutine def async_added_to_hass(self): @@ -95,5 +113,4 @@ class BMWConnectedDriveSensor(Entity): Show latest data after startup. """ - self._account.add_update_listener(self.update) - yield from self.hass.async_add_job(self.update) + self._account.add_update_listener(self.update_callback) From de1ff1e9522ec04905e4784ac71281b489e21c9c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 16 Mar 2018 00:12:43 +0100 Subject: [PATCH 046/924] Fix Sonos join/unjoin in scripts (#13248) --- homeassistant/components/media_player/sonos.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index e124fbd0443..34f30b5c2f4 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -188,13 +188,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): master = [device for device in hass.data[DATA_SONOS].devices if device.entity_id == service.data[ATTR_MASTER]] if master: - master[0].join(devices) + with hass.data[DATA_SONOS].topology_lock: + master[0].join(devices) + return + + if service.service == SERVICE_UNJOIN: + with hass.data[DATA_SONOS].topology_lock: + for device in devices: + device.unjoin() return for device in devices: - if service.service == SERVICE_UNJOIN: - device.unjoin() - elif service.service == SERVICE_SNAPSHOT: + if service.service == SERVICE_SNAPSHOT: device.snapshot(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: device.restore(service.data[ATTR_WITH_GROUP]) @@ -887,16 +892,19 @@ class SonosDevice(MediaPlayerDevice): def join(self, slaves): """Form a group with other players.""" if self._coordinator: - self.soco.unjoin() + self.unjoin() for slave in slaves: if slave.unique_id != self.unique_id: slave.soco.join(self.soco) + # pylint: disable=protected-access + slave._coordinator = self @soco_error() def unjoin(self): """Unjoin the player from a group.""" self.soco.unjoin() + self._coordinator = None @soco_error() def snapshot(self, with_group=True): From 2350ce96a606205cb9a57513fbf90019a476cf46 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 16 Mar 2018 01:05:28 +0100 Subject: [PATCH 047/924] Homekit: New supported devices (#13244) * Fixed log message * Added support for scripts * Added support for lights * Small refactoring * Added support for humidity sensor * Added tests --- homeassistant/components/homekit/__init__.py | 15 +- .../components/homekit/accessories.py | 11 + homeassistant/components/homekit/const.py | 14 +- .../components/homekit/type_covers.py | 14 +- .../components/homekit/type_lights.py | 209 ++++++++++++++++++ .../homekit/type_security_systems.py | 12 +- .../components/homekit/type_sensors.py | 59 +++-- .../components/homekit/type_switches.py | 9 - .../components/homekit/type_thermostats.py | 12 +- .../homekit/test_get_accessories.py | 13 ++ tests/components/homekit/test_type_lights.py | 160 ++++++++++++++ tests/components/homekit/test_type_sensors.py | 31 ++- 12 files changed, 495 insertions(+), 64 deletions(-) create mode 100644 homeassistant/components/homekit/type_lights.py create mode 100644 tests/components/homekit/test_type_lights.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 63013bd8fc9..b74171b08f7 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,7 +73,8 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('%s: ') + _LOGGER.debug('', + state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -87,6 +88,11 @@ def get_accessory(hass, state, aid, config): state.entity_id, 'TemperatureSensor') return TYPES['TemperatureSensor'](hass, state.entity_id, state.name, aid=aid) + elif unit == '%': + _LOGGER.debug('Add "%s" as %s"', + state.entity_id, 'HumiditySensor') + return TYPES['HumiditySensor'](hass, state.entity_id, state.name, + aid=aid) elif state.domain == 'cover': # Only add covers that support set_cover_position @@ -114,8 +120,11 @@ def get_accessory(hass, state, aid, config): return TYPES['Thermostat'](hass, state.entity_id, state.name, support_auto, aid=aid) + elif state.domain == 'light': + return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean': + or state.domain == 'input_boolean' or state.domain == 'script': _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) @@ -175,7 +184,7 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_security_systems, type_sensors, + type_covers, type_lights, type_security_systems, type_sensors, type_switches, type_thermostats) for state in self._hass.states.all(): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 0af25bc4453..4c4409e6dfc 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,6 +4,8 @@ import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver +from homeassistant.helpers.event import async_track_state_change + from .const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, @@ -49,6 +51,8 @@ def override_properties(char, properties=None, valid_values=None): class HomeAccessory(Accessory): """Adapter class for Accessory.""" + # pylint: disable=no-member + def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, category='OTHER', **kwargs): """Initialize a Accessory object.""" @@ -59,6 +63,13 @@ class HomeAccessory(Accessory): def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) + def run(self): + """Method called by accessory after driver is started.""" + state = self._hass.states.get(self._entity_id) + self.update_state(new_state=state) + async_track_state_change( + self._hass, self._entity_id, self.update_state) + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d2b1caffe53..a45c8298b78 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,10 +23,18 @@ BRIDGE_MODEL = 'homekit.bridge' BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' +# #### Categories #### +CATEGORY_LIGHT = 'LIGHTBULB' +CATEGORY_SENSOR = 'SENSOR' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_BRIDGING_STATE = 'BridgingState' +SERV_HUMIDITY_SENSOR = 'HumiditySensor' +# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, +# StatusLowBattery, Name +SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' @@ -36,20 +44,24 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' +CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' +CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_NAME = 'Name' -CHAR_ON = 'On' +CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_REACHABLE = 'Reachable' +CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 0110bff3185..36cfa4d635a 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -2,7 +2,6 @@ import logging from homeassistant.components.cover import ATTR_CURRENT_POSITION -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -22,7 +21,7 @@ class WindowCovering(HomeAccessory): """ def __init__(self, hass, entity_id, display_name, *args, **kwargs): - """Initialize a Window accessory object.""" + """Initialize a WindowCovering accessory object.""" super().__init__(display_name, entity_id, 'WINDOW_COVERING', *args, **kwargs) @@ -45,14 +44,6 @@ class WindowCovering(HomeAccessory): self.char_target_position.setter_callback = self.move_cover - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_cover_position(new_state=state) - - async_track_state_change( - self._hass, self._entity_id, self.update_cover_position) - def move_cover(self, value): """Move cover to value if call came from HomeKit.""" if value != self.current_position: @@ -65,8 +56,7 @@ class WindowCovering(HomeAccessory): self._hass.components.cover.set_cover_position( value, self._entity_id) - def update_cover_position(self, entity_id=None, old_state=None, - new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update cover position after state changed.""" if new_state is None: return diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py new file mode 100644 index 00000000000..107ad1db1e4 --- /dev/null +++ b/homeassistant/components/homekit/type_lights.py @@ -0,0 +1,209 @@ +"""Class to hold all light accessories.""" +import logging + +from homeassistant.components.light import ( + ATTR_RGB_COLOR, ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + +from . import TYPES +from .accessories import HomeAccessory, add_preload_service +from .const import ( + CATEGORY_LIGHT, SERV_LIGHTBULB, + CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + +_LOGGER = logging.getLogger(__name__) + +RGB_COLOR = 'rgb_color' + + +class Color: + """Class to handle color conversions.""" + + # pylint: disable=invalid-name + + def __init__(self, hue=None, saturation=None): + """Initialize a new Color object.""" + self.hue = hue # [0, 360] + self.saturation = saturation # [0, 1] + + def calc_hsv_to_rgb(self): + """Convert hsv_color value to rgb_color.""" + if not self.hue or not self.saturation: + return [None] * 3 + + i = int(self.hue / 60) + f = self.hue / 60 - i + v = 1 + p = 1 - self.saturation + q = 1 - self.saturation * f + t = 1 - self.saturation * (1 - f) + + rgb = [] + if i in [0, 6]: + rgb = [v, t, p] + elif i == 1: + rgb = [q, v, p] + elif i == 2: + rgb = [p, v, t] + elif i == 3: + rgb = [p, q, v] + elif i == 4: + rgb = [t, p, v] + elif i == 5: + rgb = [v, p, q] + + return [round(c * 255) for c in rgb] + + @classmethod + def calc_rgb_to_hsv(cls, rgb_color): + """Convert a give rgb_color back to a hsv_color.""" + rgb_color = [c / 255 for c in rgb_color] + c_max = max(rgb_color) + c_min = min(rgb_color) + c_diff = c_max - c_min + r, g, b = rgb_color + + hue, saturation = 0, 0 + if c_max == r: + hue = 60 * (0 + (g - b) / c_diff) + elif c_max == g: + hue = 60 * (2 + (b - r) / c_diff) + elif c_max == b: + hue = 60 * (4 + (r - g) / c_diff) + + hue = round(hue + 360) if hue < 0 else round(hue) + + if c_max != 0: + saturation = round((c_max - c_min) / c_max * 100) + + return (hue, saturation) + + +@TYPES.register('Light') +class Light(HomeAccessory): + """Generate a Light accessory for a light entity. + + Currently supports: state, brightness, rgb_color. + """ + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a new Light accessory object.""" + super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, + CHAR_HUE: False, CHAR_SATURATION: False, + RGB_COLOR: False} + + self.color = Color() + + self.chars = [] + self._features = self._hass.states.get(self._entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if self._features & SUPPORT_BRIGHTNESS: + self.chars.append(CHAR_BRIGHTNESS) + if self._features & SUPPORT_RGB_COLOR: + self.chars.append(CHAR_HUE) + self.chars.append(CHAR_SATURATION) + + serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.get_characteristic(CHAR_ON) + self.char_on.setter_callback = self.set_state + self.char_on.value = 0 + + if CHAR_BRIGHTNESS in self.chars: + self.char_brightness = serv_light \ + .get_characteristic(CHAR_BRIGHTNESS) + self.char_brightness.setter_callback = self.set_brightness + self.char_brightness.value = 0 + if CHAR_HUE in self.chars: + self.char_hue = serv_light.get_characteristic(CHAR_HUE) + self.char_hue.setter_callback = self.set_hue + self.char_hue.value = 0 + if CHAR_SATURATION in self.chars: + self.char_saturation = serv_light \ + .get_characteristic(CHAR_SATURATION) + self.char_saturation.setter_callback = self.set_saturation + self.char_saturation.value = 75 + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._flag[CHAR_BRIGHTNESS]: + return + + _LOGGER.debug('%s: Set state to %d', self._entity_id, value) + self._flag[CHAR_ON] = True + + if value == 1: + self._hass.components.light.turn_on(self._entity_id) + elif value == 0: + self._hass.components.light.turn_off(self._entity_id) + + def set_brightness(self, value): + """Set brightness if call came from HomeKit.""" + _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) + self._flag[CHAR_BRIGHTNESS] = True + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + + def set_saturation(self, value): + """Set saturation if call came from HomeKit.""" + _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) + self._flag[CHAR_SATURATION] = True + self.color.saturation = value / 100 + self.set_color() + + def set_hue(self, value): + """Set hue if call came from HomeKit.""" + _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) + self._flag[CHAR_HUE] = True + self.color.hue = value + self.set_color() + + def set_color(self): + """Set color if call came from HomeKit.""" + # Handle RGB Color + if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ + self._flag[CHAR_SATURATION]: + color = self.color.calc_hsv_to_rgb() + _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) + self._flag.update({ + CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) + self._hass.components.light.turn_on( + self._entity_id, rgb_color=color) + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update light after state change.""" + if not new_state: + return + + # Handle State + state = new_state.state + if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ + self.char_on.value != (state == STATE_ON): + self.char_on.set_value(state == STATE_ON, should_callback=False) + self._flag[CHAR_ON] = False + + # Handle Brightness + if CHAR_BRIGHTNESS in self.chars: + brightness = new_state.attributes.get(ATTR_BRIGHTNESS) + if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): + brightness = round(brightness / 255 * 100, 0) + if self.char_brightness.value != brightness: + self.char_brightness.set_value(brightness, + should_callback=False) + self._flag[CHAR_BRIGHTNESS] = False + + # Handle RGB Color + if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: + rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) + if not self._flag[RGB_COLOR] and \ + isinstance(rgb_color, (list, tuple)) and \ + list(rgb_color) != self.color.calc_hsv_to_rgb(): + hue, saturation = Color.calc_rgb_to_hsv(rgb_color) + self.char_hue.set_value(hue, should_callback=False) + self.char_saturation.set_value(saturation, + should_callback=False) + self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 02742acb75d..1d47160f9d2 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -5,7 +5,6 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ATTR_ENTITY_ID, ATTR_CODE) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -50,14 +49,6 @@ class SecuritySystem(HomeAccessory): self.char_target_state.setter_callback = self.set_security_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_security_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_security_state) - def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set security state to %d', @@ -69,8 +60,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} self._hass.services.call('alarm_control_panel', service, params) - def update_security_state(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 286862343f4..759fda08a02 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -3,13 +3,13 @@ import logging from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import ( HomeAccessory, add_preload_service, override_properties) from .const import ( - SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) + CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -29,6 +29,14 @@ def calc_temperature(state, unit=TEMP_CELSIUS): return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value +def calc_humidity(state): + """Calculate humidity from state.""" + try: + return float(state) + except ValueError: + return None + + @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -36,9 +44,9 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, name, *args, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(display_name, entity_id, 'SENSOR', *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) self._hass = hass self._entity_id = entity_id @@ -49,23 +57,42 @@ class TemperatureSensor(HomeAccessory): self.char_temp.value = 0 self.unit = None - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_temperature(new_state=state) - - async_track_state_change( - self._hass, self._entity_id, self.update_temperature) - - def update_temperature(self, entity_id=None, old_state=None, - new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update temperature after state changed.""" if new_state is None: return unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] temperature = calc_temperature(new_state.state, unit) - if temperature is not None: - self.char_temp.set_value(temperature) + if temperature: + self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) + + +@TYPES.register('HumiditySensor') +class HumiditySensor(HomeAccessory): + """Generate a HumiditySensor accessory as humidity sensor.""" + + def __init__(self, hass, entity_id, name, *args, **kwargs): + """Initialize a HumiditySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + + self._hass = hass + self._entity_id = entity_id + + serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity \ + .get_characteristic(CHAR_CURRENT_HUMIDITY) + self.char_humidity.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + humidity = calc_humidity(new_state.state) + if humidity: + self.char_humidity.set_value(humidity, should_callback=False) + _LOGGER.debug('%s: Current humidity set to %d%%', + self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 989bf4e19f5..fd3291ffe23 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -4,7 +4,6 @@ import logging from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -32,14 +31,6 @@ class Switch(HomeAccessory): self.char_on.value = False self.char_on.setter_callback = self.set_state - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_state(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_state) - def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 6e720c2214e..b73b492ba74 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -8,7 +8,6 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from homeassistant.helpers.event import async_track_state_change from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -96,14 +95,6 @@ class Thermostat(HomeAccessory): self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None - def run(self): - """Method called be object after driver is started.""" - state = self._hass.states.get(self._entity_id) - self.update_thermostat(new_state=state) - - async_track_state_change(self._hass, self._entity_id, - self.update_thermostat) - def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -142,8 +133,7 @@ class Thermostat(HomeAccessory): self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) - def update_thermostat(self, entity_id=None, - old_state=None, new_state=None): + def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" if new_state is None: return diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6e1c67cf282..e6dbe1ff729 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -53,6 +53,13 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) get_accessory(None, state, 2, {}) + def test_sensor_humidity(self): + """Test humidity sensor with % as unit.""" + with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): + state = State('sensor.humidity', '20', + {ATTR_UNIT_OF_MEASUREMENT: '%'}) + get_accessory(None, state, 2, {}) + def test_cover_set_position(self): """Test cover with support for set_cover_position.""" with patch.dict(TYPES, {'WindowCovering': self.mock_type}): @@ -81,6 +88,12 @@ class TestGetAccessories(unittest.TestCase): self.assertEqual( self.mock_type.call_args[0][-1], False) # support_auto + def test_light(self): + """Test light devices.""" + with patch.dict(TYPES, {'Light': self.mock_type}): + state = State('light.test', 'on') + get_accessory(None, state, 2, {}) + def test_climate_support_auto(self): """Test climate devices with support for auto mode.""" with patch.dict(TYPES, {'Thermostat': self.mock_type}): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py new file mode 100644 index 00000000000..0e102c53860 --- /dev/null +++ b/tests/components/homekit/test_type_lights.py @@ -0,0 +1,160 @@ +"""Test different accessory types: Lights.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.homekit.type_lights import Light, Color +from homeassistant.components.light import ( + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) +from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, + ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + +from tests.common import get_test_home_assistant + + +def test_calc_hsv_to_rgb(): + """Test conversion hsv to rgb.""" + color = Color(43, 23 / 100) + assert color.calc_hsv_to_rgb() == [255, 238, 196] + + color.hue, color.saturation = (79, 12 / 100) + assert color.calc_hsv_to_rgb() == [245, 255, 224] + + color.hue, color.saturation = (177, 2 / 100) + assert color.calc_hsv_to_rgb() == [250, 255, 255] + + color.hue, color.saturation = (212, 26 / 100) + assert color.calc_hsv_to_rgb() == [189, 220, 255] + + color.hue, color.saturation = (271, 93 / 100) + assert color.calc_hsv_to_rgb() == [140, 18, 255] + + color.hue, color.saturation = (355, 100 / 100) + assert color.calc_hsv_to_rgb() == [255, 0, 21] + + +def test_calc_rgb_to_hsv(): + """Test conversion rgb to hsv.""" + assert Color.calc_rgb_to_hsv([255, 0, 21]) == (355, 100) + assert Color.calc_rgb_to_hsv([245, 255, 224]) == (79, 12) + assert Color.calc_rgb_to_hsv([189, 220, 255]) == (212, 26) + + +class TestHomekitLights(unittest.TestCase): + """Test class for all accessory types regarding lights.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_light_basic(self): + """Test light with char state.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 5) # Lightbulb + self.assertEqual(acc.char_on.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 1) + + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_on.value, 0) + + # Set from HomeKit + acc.char_on.set_value(True) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + + acc.char_on.set_value(False) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + # Remove entity + self.hass.states.remove(entity_id) + self.hass.block_till_done() + + def test_light_brightness(self): + """Test light with brightness.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_brightness.value, 0) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 100) + + self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + self.hass.block_till_done() + self.assertEqual(acc.char_brightness.value, 40) + + # Set from HomeKit + acc.char_brightness.set_value(20) + acc.char_on.set_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + print(self.events[0].data) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + + def test_light_rgb_color(self): + """Test light with rgb_color.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, + ATTR_RGB_COLOR: (120, 20, 300)}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_hue.value, 0) + self.assertEqual(acc.char_saturation.value, 75) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_hue.value, 261) + self.assertEqual(acc.char_saturation.value, 93) + + # Set from HomeKit + acc.char_hue.set_value(145) + acc.char_saturation.set_value(75) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [64, 255, 143]}) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index f9a14f6b8cf..b533c896019 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,7 +3,7 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, calc_temperature) + TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -22,6 +22,15 @@ def test_calc_temperature(): assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 +def test_calc_humidity(): + """Test if humidity is a integer.""" + assert calc_humidity(STATE_UNKNOWN) is None + assert calc_humidity('test') is None + + assert calc_humidity('20') == 20 + assert calc_humidity('75.2') == 75.2 + + class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" @@ -60,3 +69,23 @@ class TestHomekitSensors(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) self.hass.block_till_done() self.assertEqual(acc.char_temp.value, 24) + + def test_humidity(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' + + acc = HumiditySensor(self.hass, entity_id, 'Humidity', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_humidity.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + + self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.block_till_done() + self.assertEqual(acc.char_humidity.value, 20) From 0deef34881d7f8e46b5102e794e35bd8c110fef3 Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Fri, 16 Mar 2018 03:50:58 +0100 Subject: [PATCH 048/924] Adding Foobot device sensor (#12417) * Added Foobot device sensor * Added error handling tests * Corrections after PR review. * Migrated to async/await syntax * lint fixes * stop raising HomeAssistantError * debug log for number of requests * Moved shared data between sensors from a class attribute to a separate class * Made test more async-aware disabled setup error test for now as it's not working * Working failure scenarios tests --- .coveragerc | 1 + homeassistant/components/sensor/foobot.py | 158 ++++++++++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sensor/test_foobot.py | 81 +++++++++++ tests/fixtures/foobot_data.json | 34 +++++ tests/fixtures/foobot_devices.json | 8 ++ 8 files changed, 289 insertions(+) create mode 100644 homeassistant/components/sensor/foobot.py create mode 100644 tests/components/sensor/test_foobot.py create mode 100644 tests/fixtures/foobot_data.json create mode 100644 tests/fixtures/foobot_devices.json diff --git a/.coveragerc b/.coveragerc index 5fd43d5aec7..b0f85b14c06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -578,6 +578,7 @@ omit = homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/folder.py + homeassistant/components/sensor/foobot.py homeassistant/components/sensor/fritzbox_callmonitor.py homeassistant/components/sensor/fritzbox_netmonitor.py homeassistant/components/sensor/gearbest.py diff --git a/homeassistant/components/sensor/foobot.py b/homeassistant/components/sensor/foobot.py new file mode 100644 index 00000000000..8f65a335872 --- /dev/null +++ b/homeassistant/components/sensor/foobot.py @@ -0,0 +1,158 @@ +""" +Support for the Foobot indoor air quality monitor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.foobot/ +""" +import asyncio +import logging +from datetime import timedelta + +import aiohttp +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady +from homeassistant.const import ( + ATTR_TIME, ATTR_TEMPERATURE, CONF_TOKEN, CONF_USERNAME, TEMP_CELSIUS) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + + +REQUIREMENTS = ['foobot_async==0.3.0'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_HUMIDITY = 'humidity' +ATTR_PM2_5 = 'PM2.5' +ATTR_CARBON_DIOXIDE = 'CO2' +ATTR_VOLATILE_ORGANIC_COMPOUNDS = 'VOC' +ATTR_FOOBOT_INDEX = 'index' + +SENSOR_TYPES = {'time': [ATTR_TIME, 's'], + 'pm': [ATTR_PM2_5, 'µg/m3', 'mdi:cloud'], + 'tmp': [ATTR_TEMPERATURE, TEMP_CELSIUS, 'mdi:thermometer'], + 'hum': [ATTR_HUMIDITY, '%', 'mdi:water-percent'], + 'co2': [ATTR_CARBON_DIOXIDE, 'ppm', + 'mdi:periodic-table-co2'], + 'voc': [ATTR_VOLATILE_ORGANIC_COMPOUNDS, 'ppb', + 'mdi:cloud'], + 'allpollu': [ATTR_FOOBOT_INDEX, '%', 'mdi:percent']} + +SCAN_INTERVAL = timedelta(minutes=10) +PARALLEL_UPDATES = 1 + +TIMEOUT = 10 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the devices associated with the account.""" + from foobot_async import FoobotClient + + token = config.get(CONF_TOKEN) + username = config.get(CONF_USERNAME) + + client = FoobotClient(token, username, + async_get_clientsession(hass), + timeout=TIMEOUT) + dev = [] + try: + devices = await client.get_devices() + _LOGGER.debug("The following devices were found: %s", devices) + for device in devices: + foobot_data = FoobotData(client, device['uuid']) + for sensor_type in SENSOR_TYPES: + if sensor_type == 'time': + continue + foobot_sensor = FoobotSensor(foobot_data, device, sensor_type) + dev.append(foobot_sensor) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, FoobotClient.TooManyRequests, + FoobotClient.InternalError): + _LOGGER.exception('Failed to connect to foobot servers.') + raise PlatformNotReady + except FoobotClient.ClientError: + _LOGGER.error('Failed to fetch data from foobot servers.') + return + async_add_devices(dev, True) + + +class FoobotSensor(Entity): + """Implementation of a Foobot sensor.""" + + def __init__(self, data, device, sensor_type): + """Initialize the sensor.""" + self._uuid = device['uuid'] + self.foobot_data = data + self._name = 'Foobot {} {}'.format(device['name'], + SENSOR_TYPES[sensor_type][0]) + self.type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Icon to use in the frontend.""" + return SENSOR_TYPES[self.type][2] + + @property + def state(self): + """Return the state of the device.""" + try: + data = self.foobot_data.data[self.type] + except(KeyError, TypeError): + data = None + return data + + @property + def unique_id(self): + """Return the unique id of this entity.""" + return "{}_{}".format(self._uuid, self.type) + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return self._unit_of_measurement + + async def async_update(self): + """Get the latest data.""" + await self.foobot_data.async_update() + + +class FoobotData(Entity): + """Get data from Foobot API.""" + + def __init__(self, client, uuid): + """Initialize the data object.""" + self._client = client + self._uuid = uuid + self.data = {} + + @Throttle(SCAN_INTERVAL) + async def async_update(self): + """Get the data from Foobot API.""" + interval = SCAN_INTERVAL.total_seconds() + try: + response = await self._client.get_last_data(self._uuid, + interval, + interval + 1) + except (aiohttp.client_exceptions.ClientConnectorError, + asyncio.TimeoutError, self._client.TooManyRequests, + self._client.InternalError): + _LOGGER.debug("Couldn't fetch data") + return False + _LOGGER.debug("The data response is: %s", response) + self.data = {k: round(v, 1) for k, v in response[0].items()} + return True diff --git a/requirements_all.txt b/requirements_all.txt index 608618eb166..f2919eb9bc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -287,6 +287,9 @@ fixerio==0.1.1 # homeassistant.components.light.flux_led flux_led==0.21 +# homeassistant.components.sensor.foobot +foobot_async==0.3.0 + # homeassistant.components.notify.free_mobile freesms==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9def3a7b301..69b56eabc5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,6 +62,9 @@ evohomeclient==0.2.5 # homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 +# homeassistant.components.sensor.foobot +foobot_async==0.3.0 + # homeassistant.components.tts.google gTTS-token==1.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a7704088e26..d8fc7b1ed60 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -47,6 +47,7 @@ TEST_REQUIREMENTS = ( 'ephem', 'evohomeclient', 'feedparser', + 'foobot_async', 'gTTS-token', 'HAP-python', 'ha-ffmpeg', diff --git a/tests/components/sensor/test_foobot.py b/tests/components/sensor/test_foobot.py new file mode 100644 index 00000000000..322f2b3f2a8 --- /dev/null +++ b/tests/components/sensor/test_foobot.py @@ -0,0 +1,81 @@ +"""The tests for the Foobot sensor platform.""" + +import re +import asyncio +from unittest.mock import MagicMock +import pytest + + +import homeassistant.components.sensor as sensor +from homeassistant.components.sensor import foobot +from homeassistant.const import (TEMP_CELSIUS) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.setup import async_setup_component +from tests.common import load_fixture + +VALID_CONFIG = { + 'platform': 'foobot', + 'token': 'adfdsfasd', + 'username': 'example@example.com', +} + + +async def test_default_setup(hass, aioclient_mock): + """Test the default setup.""" + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + text=load_fixture('foobot_devices.json')) + aioclient_mock.get(re.compile('api.foobot.io/v2/device/.*'), + text=load_fixture('foobot_data.json')) + assert await async_setup_component(hass, sensor.DOMAIN, + {'sensor': VALID_CONFIG}) + + metrics = {'co2': ['1232.0', 'ppm'], + 'temperature': ['21.1', TEMP_CELSIUS], + 'humidity': ['49.5', '%'], + 'pm25': ['144.8', 'µg/m3'], + 'voc': ['340.7', 'ppb'], + 'index': ['138.9', '%']} + + for name, value in metrics.items(): + state = hass.states.get('sensor.foobot_happybot_%s' % name) + assert state.state == value[0] + assert state.attributes.get('unit_of_measurement') == value[1] + + +async def test_setup_timeout_error(hass, aioclient_mock): + """Expected failures caused by a timeout in API response.""" + fake_async_add_devices = MagicMock() + + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + exc=asyncio.TimeoutError()) + with pytest.raises(PlatformNotReady): + await foobot.async_setup_platform(hass, {'sensor': VALID_CONFIG}, + fake_async_add_devices) + + +async def test_setup_permanent_error(hass, aioclient_mock): + """Expected failures caused by permanent errors in API response.""" + fake_async_add_devices = MagicMock() + + errors = [400, 401, 403] + for error in errors: + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + status=error) + result = await foobot.async_setup_platform(hass, + {'sensor': VALID_CONFIG}, + fake_async_add_devices) + assert result is None + + +async def test_setup_temporary_error(hass, aioclient_mock): + """Expected failures caused by temporary errors in API response.""" + fake_async_add_devices = MagicMock() + + errors = [429, 500] + for error in errors: + aioclient_mock.get(re.compile('api.foobot.io/v2/owner/.*'), + status=error) + with pytest.raises(PlatformNotReady): + await foobot.async_setup_platform(hass, + {'sensor': VALID_CONFIG}, + fake_async_add_devices) diff --git a/tests/fixtures/foobot_data.json b/tests/fixtures/foobot_data.json new file mode 100644 index 00000000000..93518614c42 --- /dev/null +++ b/tests/fixtures/foobot_data.json @@ -0,0 +1,34 @@ +{ + "uuid": "32463564765421243", + "start": 1518134963, + "end": 1518134963, + "sensors": [ + "time", + "pm", + "tmp", + "hum", + "co2", + "voc", + "allpollu" + ], + "units": [ + "s", + "ugm3", + "C", + "pc", + "ppm", + "ppb", + "%" + ], + "datapoints": [ + [ + 1518134963, + 144.76668, + 21.064333, + 49.474, + 1232.0, + 340.66666, + 138.93651 + ] + ] +} diff --git a/tests/fixtures/foobot_devices.json b/tests/fixtures/foobot_devices.json new file mode 100644 index 00000000000..fffc8e151cc --- /dev/null +++ b/tests/fixtures/foobot_devices.json @@ -0,0 +1,8 @@ +[ + { + "uuid": "231425657665645342", + "userId": 6545342, + "mac": "A2D3F1", + "name": "Happybot" + } +] From b1079cb49390cf552155c37f196d2df7eb430a75 Mon Sep 17 00:00:00 2001 From: karlkar Date: Fri, 16 Mar 2018 04:30:41 +0100 Subject: [PATCH 049/924] Fix for not setting up the camera if it is offline during setup phase (#13082) * Fix for not setting up the camera if it is offline during setup phase * async/await and modified service creation * Properly handle not supported PTZ * setup platform made synchronous as ONVIFService constructors do I/O * Fix intendation issue --- homeassistant/components/camera/onvif.py | 113 +++++++++++++++-------- 1 file changed, 75 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/camera/onvif.py b/homeassistant/components/camera/onvif.py index d48f06539f4..3ae47ba5dee 100644 --- a/homeassistant/components/camera/onvif.py +++ b/homeassistant/components/camera/onvif.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/camera.onvif/ """ import asyncio import logging +import os import voluptuous as vol @@ -103,92 +104,128 @@ class ONVIFHassCamera(Camera): def __init__(self, hass, config): """Initialize a ONVIF camera.""" - from onvif import ONVIFCamera, exceptions super().__init__() + import onvif + self._username = config.get(CONF_USERNAME) + self._password = config.get(CONF_PASSWORD) + self._host = config.get(CONF_HOST) + self._port = config.get(CONF_PORT) self._name = config.get(CONF_NAME) self._ffmpeg_arguments = config.get(CONF_EXTRA_ARGUMENTS) + self._profile_index = config.get(CONF_PROFILE) self._input = None - camera = None + self._media_service = \ + onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( + self._host, self._port), + self._username, self._password, + '{}/wsdl/media.wsdl'.format(os.path.dirname( + onvif.__file__))) + + self._ptz_service = \ + onvif.ONVIFService('http://{}:{}/onvif/device_service'.format( + self._host, self._port), + self._username, self._password, + '{}/wsdl/ptz.wsdl'.format(os.path.dirname( + onvif.__file__))) + + def obtain_input_uri(self): + """Set the input uri for the camera.""" + from onvif import exceptions + _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", + self._host, self._port) + try: - _LOGGER.debug("Connecting with ONVIF Camera: %s on port %s", - config.get(CONF_HOST), config.get(CONF_PORT)) - camera = ONVIFCamera( - config.get(CONF_HOST), config.get(CONF_PORT), - config.get(CONF_USERNAME), config.get(CONF_PASSWORD) - ) - media_service = camera.create_media_service() - self._profiles = media_service.GetProfiles() - self._profile_index = config.get(CONF_PROFILE) - if self._profile_index >= len(self._profiles): + profiles = self._media_service.GetProfiles() + + if self._profile_index >= len(profiles): _LOGGER.warning("ONVIF Camera '%s' doesn't provide profile %d." " Using the last profile.", self._name, self._profile_index) self._profile_index = -1 - req = media_service.create_type('GetStreamUri') + + req = self._media_service.create_type('GetStreamUri') + # pylint: disable=protected-access - req.ProfileToken = self._profiles[self._profile_index]._token - self._input = media_service.GetStreamUri(req).Uri.replace( - 'rtsp://', 'rtsp://{}:{}@'.format( - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)), 1) + req.ProfileToken = profiles[self._profile_index]._token + uri_no_auth = self._media_service.GetStreamUri(req).Uri + uri_for_log = uri_no_auth.replace( + 'rtsp://', 'rtsp://:@', 1) + self._input = uri_no_auth.replace( + 'rtsp://', 'rtsp://{}:{}@'.format(self._username, + self._password), 1) _LOGGER.debug( "ONVIF Camera Using the following URL for %s: %s", - self._name, self._input) - except Exception as err: - _LOGGER.error("Unable to communicate with ONVIF Camera: %s", err) - raise - try: - self._ptz = camera.create_ptz_service() + self._name, uri_for_log) + # we won't need the media service anymore + self._media_service = None except exceptions.ONVIFError as err: - self._ptz = None - _LOGGER.warning("Unable to setup PTZ for ONVIF Camera: %s", err) + _LOGGER.debug("Couldn't setup camera '%s'. Error: %s", + self._name, err) + return def perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" - if self._ptz: + from onvif import exceptions + if self._ptz_service: pan_val = 1 if pan == DIR_RIGHT else -1 if pan == DIR_LEFT else 0 tilt_val = 1 if tilt == DIR_UP else -1 if tilt == DIR_DOWN else 0 zoom_val = 1 if zoom == ZOOM_IN else -1 if zoom == ZOOM_OUT else 0 req = {"Velocity": { "PanTilt": {"_x": pan_val, "_y": tilt_val}, "Zoom": {"_x": zoom_val}}} - self._ptz.ContinuousMove(req) + try: + self._ptz_service.ContinuousMove(req) + except exceptions.ONVIFError as err: + if "Bad Request" in err.reason: + self._ptz_service = None + _LOGGER.debug("Camera '%s' doesn't support PTZ.", + self._name) + else: + _LOGGER.debug("Camera '%s' doesn't support PTZ.", self._name) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Callback when entity is added to hass.""" if ONVIF_DATA not in self.hass.data: self.hass.data[ONVIF_DATA] = {} self.hass.data[ONVIF_DATA][ENTITIES] = [] self.hass.data[ONVIF_DATA][ENTITIES].append(self) - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg import ImageFrame, IMAGE_JPEG + + if not self._input: + await self.hass.async_add_job(self.obtain_input_uri) + if not self._input: + return None + ffmpeg = ImageFrame( self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - image = yield from asyncio.shield(ffmpeg.get_image( + image = await asyncio.shield(ffmpeg.get_image( self._input, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments), loop=self.hass.loop) return image - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" from haffmpeg import CameraMjpeg + if not self._input: + await self.hass.async_add_job(self.obtain_input_uri) + if not self._input: + return None + stream = CameraMjpeg(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - yield from stream.open_camera( + await stream.open_camera( self._input, extra_cmd=self._ffmpeg_arguments) - yield from async_aiohttp_proxy_stream( + await async_aiohttp_proxy_stream( self.hass, request, stream, 'multipart/x-mixed-replace;boundary=ffserver') - yield from stream.close() + await stream.close() @property def name(self): From 99f7e2bd979e20ac7847a1b1c727452b06177f66 Mon Sep 17 00:00:00 2001 From: BioSehnsucht Date: Thu, 15 Mar 2018 22:36:03 -0500 Subject: [PATCH 050/924] Added Stride notification component (#13221) * Added Stride notification component * Fix trailing whitespace in Stride notify * More whitespace fixes and rogue comment for Stride notify * More whitespace fixing for Stride notify * Correcting hanging indents for Stride notify --- .coveragerc | 1 + homeassistant/components/notify/stride.py | 102 ++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 106 insertions(+) create mode 100644 homeassistant/components/notify/stride.py diff --git a/.coveragerc b/.coveragerc index b0f85b14c06..4da5343bf4f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -519,6 +519,7 @@ omit = homeassistant/components/notify/sendgrid.py homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py + homeassistant/components/notify/stride.py homeassistant/components/notify/smtp.py homeassistant/components/notify/synology_chat.py homeassistant/components/notify/syslog.py diff --git a/homeassistant/components/notify/stride.py b/homeassistant/components/notify/stride.py new file mode 100644 index 00000000000..f31e50a5886 --- /dev/null +++ b/homeassistant/components/notify/stride.py @@ -0,0 +1,102 @@ +""" +Stride platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.stride/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_TOKEN, CONF_ROOM + +REQUIREMENTS = ['pystride==0.1.7'] + +_LOGGER = logging.getLogger(__name__) + +CONF_PANEL = 'panel' +CONF_CLOUDID = 'cloudid' + +DEFAULT_PANEL = None + +VALID_PANELS = {'info', 'note', 'tip', 'warning', None} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_CLOUDID): cv.string, + vol.Required(CONF_ROOM): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS), +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Stride notification service.""" + return StrideNotificationService( + config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], + config[CONF_CLOUDID]) + + +class StrideNotificationService(BaseNotificationService): + """Implement the notification service for Stride.""" + + def __init__(self, token, default_room, default_panel, cloudid): + """Initialize the service.""" + self._token = token + self._default_room = default_room + self._default_panel = default_panel + self._cloudid = cloudid + + from stride import Stride + self._stride = Stride(self._cloudid, access_token=self._token) + + def send_message(self, message="", **kwargs): + """Send a message.""" + panel = self._default_panel + + if kwargs.get(ATTR_DATA) is not None: + data = kwargs.get(ATTR_DATA) + if ((data.get(CONF_PANEL) is not None) + and (data.get(CONF_PANEL) in VALID_PANELS)): + panel = data.get(CONF_PANEL) + + message_text = { + 'type': 'paragraph', + 'content': [ + { + 'type': 'text', + 'text': message + } + ] + } + panel_text = message_text + if panel is not None: + panel_text = { + 'type': 'panel', + 'attrs': + { + 'panelType': panel + }, + 'content': + [ + message_text, + ] + } + + message_doc = { + 'body': { + 'version': 1, + 'type': 'doc', + 'content': + [ + panel_text, + ] + } + } + + targets = kwargs.get(ATTR_TARGET, [self._default_room]) + + for target in targets: + self._stride.message_room(target, message_doc) diff --git a/requirements_all.txt b/requirements_all.txt index f2919eb9bc3..b30b77307c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -891,6 +891,9 @@ pysma==0.2 # homeassistant.components.switch.snmp pysnmp==4.4.4 +# homeassistant.components.notify.stride +pystride==0.1.7 + # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.3 From f6ae2d338d6223a8715fcb434ad75caad687340a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 16 Mar 2018 11:38:44 +0100 Subject: [PATCH 051/924] Homekit: Use util functions for unit conversion (#13253) * Updated to util/color for conversion * Updated temperature sensor to use util/temperature conversion --- .../components/homekit/type_lights.py | 80 +++---------------- .../components/homekit/type_sensors.py | 3 +- tests/components/homekit/test_type_lights.py | 38 ++------- tests/components/homekit/test_type_sensors.py | 2 - 4 files changed, 17 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 107ad1db1e4..db7172bef17 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -5,6 +5,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF +from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -17,69 +18,6 @@ _LOGGER = logging.getLogger(__name__) RGB_COLOR = 'rgb_color' -class Color: - """Class to handle color conversions.""" - - # pylint: disable=invalid-name - - def __init__(self, hue=None, saturation=None): - """Initialize a new Color object.""" - self.hue = hue # [0, 360] - self.saturation = saturation # [0, 1] - - def calc_hsv_to_rgb(self): - """Convert hsv_color value to rgb_color.""" - if not self.hue or not self.saturation: - return [None] * 3 - - i = int(self.hue / 60) - f = self.hue / 60 - i - v = 1 - p = 1 - self.saturation - q = 1 - self.saturation * f - t = 1 - self.saturation * (1 - f) - - rgb = [] - if i in [0, 6]: - rgb = [v, t, p] - elif i == 1: - rgb = [q, v, p] - elif i == 2: - rgb = [p, v, t] - elif i == 3: - rgb = [p, q, v] - elif i == 4: - rgb = [t, p, v] - elif i == 5: - rgb = [v, p, q] - - return [round(c * 255) for c in rgb] - - @classmethod - def calc_rgb_to_hsv(cls, rgb_color): - """Convert a give rgb_color back to a hsv_color.""" - rgb_color = [c / 255 for c in rgb_color] - c_max = max(rgb_color) - c_min = min(rgb_color) - c_diff = c_max - c_min - r, g, b = rgb_color - - hue, saturation = 0, 0 - if c_max == r: - hue = 60 * (0 + (g - b) / c_diff) - elif c_max == g: - hue = 60 * (2 + (b - r) / c_diff) - elif c_max == b: - hue = 60 * (4 + (r - g) / c_diff) - - hue = round(hue + 360) if hue < 0 else round(hue) - - if c_max != 0: - saturation = round((c_max - c_min) / c_max * 100) - - return (hue, saturation) - - @TYPES.register('Light') class Light(HomeAccessory): """Generate a Light accessory for a light entity. @@ -97,8 +35,6 @@ class Light(HomeAccessory): CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} - self.color = Color() - self.chars = [] self._features = self._hass.states.get(self._entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) @@ -107,6 +43,8 @@ class Light(HomeAccessory): if self._features & SUPPORT_RGB_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) + self._hue = None + self._saturation = None serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) self.char_on = serv_light.get_characteristic(CHAR_ON) @@ -152,14 +90,14 @@ class Light(HomeAccessory): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) self._flag[CHAR_SATURATION] = True - self.color.saturation = value / 100 + self._saturation = value self.set_color() def set_hue(self, value): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) self._flag[CHAR_HUE] = True - self.color.hue = value + self._hue = value self.set_color() def set_color(self): @@ -167,7 +105,7 @@ class Light(HomeAccessory): # Handle RGB Color if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: - color = self.color.calc_hsv_to_rgb() + color = color_hsv_to_RGB(self._hue, self._saturation, 100) _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) @@ -199,10 +137,12 @@ class Light(HomeAccessory): # Handle RGB Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) + current_color = color_hsv_to_RGB(self._hue, self._saturation, 100)\ + if self._hue and self._saturation else [None] * 3 if not self._flag[RGB_COLOR] and \ isinstance(rgb_color, (list, tuple)) and \ - list(rgb_color) != self.color.calc_hsv_to_rgb(): - hue, saturation = Color.calc_rgb_to_hsv(rgb_color) + tuple(rgb_color) != current_color: + hue, saturation, _ = color_RGB_to_hsv(*rgb_color) self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 759fda08a02..7575acb5c35 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -3,6 +3,7 @@ import logging from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) +from homeassistant.util.temperature import fahrenheit_to_celsius from . import TYPES from .accessories import ( @@ -26,7 +27,7 @@ def calc_temperature(state, unit=TEMP_CELSIUS): except ValueError: return None - return round((value - 32) / 1.8, 2) if unit == TEMP_FAHRENHEIT else value + return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value def calc_humidity(state): diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 0e102c53860..83456f459cd 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -2,7 +2,7 @@ import unittest from homeassistant.core import callback -from homeassistant.components.homekit.type_lights import Light, Color +from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) @@ -14,34 +14,6 @@ from homeassistant.const import ( from tests.common import get_test_home_assistant -def test_calc_hsv_to_rgb(): - """Test conversion hsv to rgb.""" - color = Color(43, 23 / 100) - assert color.calc_hsv_to_rgb() == [255, 238, 196] - - color.hue, color.saturation = (79, 12 / 100) - assert color.calc_hsv_to_rgb() == [245, 255, 224] - - color.hue, color.saturation = (177, 2 / 100) - assert color.calc_hsv_to_rgb() == [250, 255, 255] - - color.hue, color.saturation = (212, 26 / 100) - assert color.calc_hsv_to_rgb() == [189, 220, 255] - - color.hue, color.saturation = (271, 93 / 100) - assert color.calc_hsv_to_rgb() == [140, 18, 255] - - color.hue, color.saturation = (355, 100 / 100) - assert color.calc_hsv_to_rgb() == [255, 0, 21] - - -def test_calc_rgb_to_hsv(): - """Test conversion rgb to hsv.""" - assert Color.calc_rgb_to_hsv([255, 0, 21]) == (355, 100) - assert Color.calc_rgb_to_hsv([245, 255, 224]) == (79, 12) - assert Color.calc_rgb_to_hsv([189, 220, 255]) == (212, 26) - - class TestHomekitLights(unittest.TestCase): """Test class for all accessory types regarding lights.""" @@ -137,15 +109,15 @@ class TestHomekitLights(unittest.TestCase): entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, - ATTR_RGB_COLOR: (120, 20, 300)}) + ATTR_RGB_COLOR: (120, 20, 255)}) acc = Light(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) acc.run() self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 261) - self.assertEqual(acc.char_saturation.value, 93) + self.assertEqual(acc.char_hue.value, 265.532) + self.assertEqual(acc.char_saturation.value, 92.157) # Set from HomeKit acc.char_hue.set_value(145) @@ -157,4 +129,4 @@ class TestHomekitLights(unittest.TestCase): self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: [64, 255, 143]}) + ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (63, 255, 143)}) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index b533c896019..551dfc6780d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -17,9 +17,7 @@ def test_calc_temperature(): assert calc_temperature('20') == 20 assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - assert calc_temperature('-20.6', TEMP_FAHRENHEIT) == -29.22 def test_calc_humidity(): From 78144bc6dedea0bf53ff8f77f9f5f74ffab858e9 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 16 Mar 2018 12:14:21 +0100 Subject: [PATCH 052/924] Use the first, not the last volume controller when multiple are available on songpal (#13222) * use the first, not the last volume controller * Do not mutate the list but simply pick the first by index --- homeassistant/components/media_player/songpal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index b1dc7df3319..e43f5951db7 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -154,7 +154,7 @@ class SongpalDevice(MediaPlayerDevice): _LOGGER.warning("Got %s volume controls, using the first one", volumes) - volume = volumes.pop() + volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) self._volume_max = volume.maxVolume From f013619e6990181d35ce414f5a95667b96cbb1a8 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 19:58:03 +0100 Subject: [PATCH 053/924] Xiaomi MiIO Switch: Power Strip support improved (#12917) * Xiaomi MiIO Switch: Power Strip support improved. * New service descriptions added. * Make hound happy. * Pylint fixed. * Use Async / await syntax. * Missed method fixed. * Make hound happy. * Don't abuse the system property supported_features anymore. * Check the correct method. * Refactoring. * Make hound happy. * pythion-miio version bumped. * Clean-up. * Unique id added. * Filter service calls. Device unavailable handling improved. --- homeassistant/components/switch/services.yaml | 31 +++ .../components/switch/xiaomi_miio.py | 260 ++++++++++++++---- 2 files changed, 239 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/switch/services.yaml b/homeassistant/components/switch/services.yaml index f52b197d432..46b1237f57c 100644 --- a/homeassistant/components/switch/services.yaml +++ b/homeassistant/components/switch/services.yaml @@ -30,3 +30,34 @@ mysensors_send_ir_code: V_IR_SEND: description: IR code to send. example: '0xC284' + +xiaomi_miio_set_wifi_led_on: + description: Turn the wifi led on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' +xiaomi_miio_set_wifi_led_off: + description: Turn the wifi led off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' +xiaomi_miio_set_power_price: + description: Set the power price. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power price, between 0 and 999. + example: 31 +xiaomi_miio_set_power_mode: + description: Set the power mode. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'switch.xiaomi_miio_device' + mode: + description: Power mode, valid values are 'normal' and 'green'. + example: 'green' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 1a8feb5811d..9f0f163df69 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -11,15 +11,19 @@ import logging import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, ) -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, ) +from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA, + DOMAIN, ) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, + ATTR_ENTITY_ID, ) from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Miio Switch' +DATA_KEY = 'switch.xiaomi_miio' CONF_MODEL = 'model' +MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -39,14 +43,63 @@ ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' ATTR_LOAD_POWER = 'load_power' ATTR_MODEL = 'model' +ATTR_MODE = 'mode' +ATTR_POWER_MODE = 'power_mode' +ATTR_WIFI_LED = 'wifi_led' +ATTR_POWER_PRICE = 'power_price' +ATTR_PRICE = 'price' + SUCCESS = ['ok'] +SUPPORT_SET_POWER_MODE = 1 +SUPPORT_SET_WIFI_LED = 2 +SUPPORT_SET_POWER_PRICE = 4 + +ADDITIONAL_SUPPORT_FLAGS_GENERIC = 0 + +ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 = (SUPPORT_SET_POWER_MODE | + SUPPORT_SET_WIFI_LED | + SUPPORT_SET_POWER_PRICE) + +ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 = (SUPPORT_SET_WIFI_LED | + SUPPORT_SET_POWER_PRICE) + +SERVICE_SET_WIFI_LED_ON = 'xiaomi_miio_set_wifi_led_on' +SERVICE_SET_WIFI_LED_OFF = 'xiaomi_miio_set_wifi_led_off' +SERVICE_SET_POWER_MODE = 'xiaomi_miio_set_power_mode' +SERVICE_SET_POWER_PRICE = 'xiaomi_miio_set_power_price' + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + +SERVICE_SCHEMA_POWER_MODE = SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): vol.All(vol.In(['green', 'normal'])), +}) + +SERVICE_SCHEMA_POWER_PRICE = SERVICE_SCHEMA.extend({ + vol.Required(ATTR_PRICE): vol.All(vol.Coerce(float), vol.Range(min=0)) +}) + +SERVICE_TO_METHOD = { + SERVICE_SET_WIFI_LED_ON: {'method': 'async_set_wifi_led_on'}, + SERVICE_SET_WIFI_LED_OFF: {'method': 'async_set_wifi_led_off'}, + SERVICE_SET_POWER_MODE: { + 'method': 'async_set_power_mode', + 'schema': SERVICE_SCHEMA_POWER_MODE}, + SERVICE_SET_POWER_PRICE: { + 'method': 'async_set_power_price', + 'schema': SERVICE_SCHEMA_POWER_PRICE}, +} + # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the switch from config.""" from miio import Device, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -56,12 +109,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) devices = [] + unique_id = None if model is None: try: miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) _LOGGER.info("%s %s %s detected", model, device_info.firmware_version, @@ -77,21 +132,24 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # A switch device per channel will be created. for channel_usb in [True, False]: device = ChuangMiPlugV1Switch( - name, plug, model, channel_usb) + name, plug, model, unique_id, channel_usb) devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']: from miio import PowerStrip plug = PowerStrip(host, token) - device = XiaomiPowerStripSwitch(name, plug, model) + device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) + hass.data[DATA_KEY][host] = device elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']: from miio import Plug plug = Plug(host, token) - device = XiaomiPlugGenericSwitch(name, plug, model) + device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' @@ -101,22 +159,52 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(devices, update_before_add=True) + async def async_service_handler(service): + """Map services to methods on XiaomiPlugGenericSwitch.""" + method = SERVICE_TO_METHOD.get(service.service) + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + devices = [device for device in hass.data[DATA_KEY].values() if + device.entity_id in entity_ids] + else: + devices = hass.data[DATA_KEY].values() + + update_tasks = [] + for device in devices: + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) + update_tasks.append(device.async_update_ha_state(True)) + + if update_tasks: + await asyncio.wait(update_tasks, loop=hass.loop) + + for plug_service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[plug_service].get('schema', SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, plug_service, async_service_handler, schema=schema) + class XiaomiPlugGenericSwitch(SwitchDevice): """Representation of a Xiaomi Plug Generic.""" - def __init__(self, name, plug, model): + def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" self._name = name - self._icon = 'mdi:power-socket' - self._model = model - self._plug = plug + self._model = model + self._unique_id = unique_id + + self._icon = 'mdi:power-socket' + self._available = False self._state = None self._state_attrs = { ATTR_TEMPERATURE: None, ATTR_MODEL: self._model, } + self._additional_supported_features = ADDITIONAL_SUPPORT_FLAGS_GENERIC self._skip_update = False @property @@ -124,6 +212,11 @@ class XiaomiPlugGenericSwitch(SwitchDevice): """Poll the plug.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -137,7 +230,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -149,12 +242,11 @@ class XiaomiPlugGenericSwitch(SwitchDevice): """Return true if switch is on.""" return self._state - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a plug command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) _LOGGER.debug("Response received from plug: %s", result) @@ -162,30 +254,28 @@ class XiaomiPlugGenericSwitch(SwitchDevice): return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the plug on.""" - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the plug off.""" - result = yield from self._try_command( + result = await self._try_command( "Turning the plug off failed.", self._plug.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -195,34 +285,75 @@ class XiaomiPlugGenericSwitch(SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._state_attrs.update({ ATTR_TEMPERATURE: state.temperature }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + async def async_set_wifi_led_on(self): + """Turn the wifi led on.""" + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + return -class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice): + await self._try_command( + "Turning the wifi led on failed.", + self._plug.set_wifi_led, True) + + async def async_set_wifi_led_off(self): + """Turn the wifi led on.""" + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + return + + await self._try_command( + "Turning the wifi led off failed.", + self._plug.set_wifi_led, False) + + async def async_set_power_price(self, price: int): + """Set the power price.""" + if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 0: + return + + await self._try_command( + "Setting the power price of the power strip failed.", + self._plug.set_power_price, price) + + +class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): """Representation of a Xiaomi Power Strip.""" - def __init__(self, name, plug, model): + def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" - XiaomiPlugGenericSwitch.__init__(self, name, plug, model) + XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) - self._state_attrs = { - ATTR_TEMPERATURE: None, + if self._model == MODEL_POWER_STRIP_V2: + self._additional_supported_features = \ + ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 + else: + self._additional_supported_features = \ + ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 + + self._state_attrs.update({ ATTR_LOAD_POWER: None, - ATTR_MODEL: self._model, - } + }) - @asyncio.coroutine - def async_update(self): + if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 1: + self._state_attrs[ATTR_POWER_MODE] = None + + if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 1: + self._state_attrs[ATTR_WIFI_LED] = None + + if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 1: + self._state_attrs[ATTR_POWER_PRICE] = None + + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -232,60 +363,84 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch, SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._state_attrs.update({ ATTR_TEMPERATURE: state.temperature, - ATTR_LOAD_POWER: state.load_power + ATTR_LOAD_POWER: state.load_power, }) + if self._additional_supported_features & \ + SUPPORT_SET_POWER_MODE == 1 and state.mode: + self._state_attrs[ATTR_POWER_MODE] = state.mode.value + + if self._additional_supported_features & \ + SUPPORT_SET_WIFI_LED == 1 and state.wifi_led: + self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + + if self._additional_supported_features & \ + SUPPORT_SET_POWER_PRICE == 1 and state.power_price: + self._state_attrs[ATTR_POWER_PRICE] = state.power_price + except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + async def async_set_power_mode(self, mode: str): + """Set the power mode.""" + if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 0: + return -class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): + from miio.powerstrip import PowerMode + + await self._try_command( + "Setting the power mode of the power strip failed.", + self._plug.set_power_mode, PowerMode(mode)) + + +class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): """Representation of a Chuang Mi Plug V1.""" - def __init__(self, name, plug, model, channel_usb): + def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" name = '{} USB'.format(name) if channel_usb else name - XiaomiPlugGenericSwitch.__init__(self, name, plug, model) + if unique_id is not None and channel_usb: + unique_id = "{}-{}".format(unique_id, 'usb') + + XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) self._channel_usb = channel_usb - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn a channel on.""" if self._channel_usb: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.usb_on) else: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn a channel off.""" if self._channel_usb: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.usb_off) else: - result = yield from self._try_command( + result = await self._try_command( "Turning the plug on failed.", self._plug.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -295,9 +450,10 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): return try: - state = yield from self.hass.async_add_job(self._plug.status) + state = await self.hass.async_add_job(self._plug.status) _LOGGER.debug("Got new state: %s", state) + self._available = True if self._channel_usb: self._state = state.usb_power else: @@ -308,5 +464,5 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch, SwitchDevice): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) From fe7012549eec44725144476dc59d10fb52e390f6 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 20:59:18 +0100 Subject: [PATCH 054/924] Xiaomi MiIO light: Philips Eyecare Smart Lamp 2 integration (#12883) * Xiaomi Philips Eyecare Smart Lamp 2 support added. * Blank lines removed. * Pylint errors fixed. * Abstract light introduced. * Smart night light mode renamed. * Use the conventional power on/off methods again. * Eyecare mode service added. * Eyecare mode attribute added. * Name of the ambient light entity fixed. * Reuse of the same local variable name within the same scope fixed. * Use Async / await syntax. * Missed method fixed. * Make hound happy. * Don't abuse the system property supported_features anymore. * Make hound happy. * Wrong hanging indentation fixed. Unnecessary parens after 'return' keyword fixed. * Refactoring. * Additional supported features bit mask removed as long as the differences of the supported devices are simple. * Support for Xiaomi Philips Zhirui Smart LED Bulb E14 Candle Lamp added. * Docstrings updated. Refactoring. * Unique id added. * Filter service calls. Dummy methods removed. * Device available handling improved. * super() used for calling the parent class * Self removed from super(). --- homeassistant/components/light/xiaomi_miio.py | 402 ++++++++++++++---- 1 file changed, 331 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 77b02600f33..a21c86f49c0 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -37,7 +37,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ['philips.light.sread1', 'philips.light.ceiling', 'philips.light.zyceiling', - 'philips.light.bulb']), + 'philips.light.bulb', + 'philips.light.candle2']), }) REQUIREMENTS = ['python-miio==0.3.8'] @@ -46,16 +47,27 @@ REQUIREMENTS = ['python-miio==0.3.8'] CCT_MIN = 1 CCT_MAX = 100 -DELAYED_TURN_OFF_MAX_DEVIATION = 4 +DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS = 4 +DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES = 1 SUCCESS = ['ok'] ATTR_MODEL = 'model' ATTR_SCENE = 'scene' ATTR_DELAYED_TURN_OFF = 'delayed_turn_off' ATTR_TIME_PERIOD = 'time_period' +ATTR_NIGHT_LIGHT_MODE = 'night_light_mode' +ATTR_AUTOMATIC_COLOR_TEMPERATURE = 'automatic_color_temperature' +ATTR_REMINDER = 'reminder' +ATTR_EYECARE_MODE = 'eyecare_mode' SERVICE_SET_SCENE = 'xiaomi_miio_set_scene' SERVICE_SET_DELAYED_TURN_OFF = 'xiaomi_miio_set_delayed_turn_off' +SERVICE_REMINDER_ON = 'xiaomi_miio_reminder_on' +SERVICE_REMINDER_OFF = 'xiaomi_miio_reminder_off' +SERVICE_NIGHT_LIGHT_MODE_ON = 'xiaomi_miio_night_light_mode_on' +SERVICE_NIGHT_LIGHT_MODE_OFF = 'xiaomi_miio_night_light_mode_off' +SERVICE_EYECARE_MODE_ON = 'xiaomi_miio_eyecare_mode_on' +SERVICE_EYECARE_MODE_OFF = 'xiaomi_miio_eyecare_mode_off' XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -78,12 +90,18 @@ SERVICE_TO_METHOD = { SERVICE_SET_SCENE: { 'method': 'async_set_scene', 'schema': SERVICE_SCHEMA_SET_SCENE}, + SERVICE_REMINDER_ON: {'method': 'async_reminder_on'}, + SERVICE_REMINDER_OFF: {'method': 'async_reminder_off'}, + SERVICE_NIGHT_LIGHT_MODE_ON: {'method': 'async_night_light_mode_on'}, + SERVICE_NIGHT_LIGHT_MODE_OFF: {'method': 'async_night_light_mode_off'}, + SERVICE_EYECARE_MODE_ON: {'method': 'async_eyecare_mode_on'}, + SERVICE_EYECARE_MODE_OFF: {'method': 'async_eyecare_mode_off'}, } # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the light from config.""" from miio import Device, DeviceException if DATA_KEY not in hass.data: @@ -96,11 +114,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + devices = [] + unique_id = None + if model is None: try: miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) _LOGGER.info("%s %s %s detected", model, device_info.firmware_version, @@ -111,27 +133,38 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if model == 'philips.light.sread1': from miio import PhilipsEyecare light = PhilipsEyecare(host, token) - device = XiaomiPhilipsEyecareLamp(name, light, model) + primary_device = XiaomiPhilipsEyecareLamp( + name, light, model, unique_id) + devices.append(primary_device) + hass.data[DATA_KEY][host] = primary_device + + secondary_device = XiaomiPhilipsEyecareLampAmbientLight( + name, light, model, unique_id) + devices.append(secondary_device) + # The ambient light doesn't expose additional services. + # A hass.data[DATA_KEY] entry isn't needed. elif model in ['philips.light.ceiling', 'philips.light.zyceiling']: from miio import Ceil light = Ceil(host, token) - device = XiaomiPhilipsCeilingLamp(name, light, model) - elif model == 'philips.light.bulb': + device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device + elif model in ['philips.light.bulb', 'philips.light.candle2']: from miio import PhilipsBulb light = PhilipsBulb(host, token) - device = XiaomiPhilipsLightBall(name, light, model) + device = XiaomiPhilipsBulb(name, light, model, unique_id) + devices.append(device) + hass.data[DATA_KEY][host] = device else: _LOGGER.error( 'Unsupported device found! Please create an issue at ' - 'https://github.com/rytilahti/python-miio/issues ' + 'https://github.com/syssi/philipslight/issues ' 'and provide the following data: %s', model) return False - hass.data[DATA_KEY][host] = device - async_add_devices([device], update_before_add=True) + async_add_devices(devices, update_before_add=True) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on Xiaomi Philips Lights.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() @@ -145,11 +178,13 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): update_tasks = [] for target_device in target_devices: - yield from getattr(target_device, method['method'])(**params) + if not hasattr(target_device, method['method']): + continue + await getattr(target_device, method['method'])(**params) update_tasks.append(target_device.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for xiaomi_miio_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[xiaomi_miio_service].get( @@ -158,23 +193,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema) -class XiaomiPhilipsGenericLight(Light): - """Representation of a Xiaomi Philips Light.""" +class XiaomiPhilipsAbstractLight(Light): + """Representation of a Abstract Xiaomi Philips Light.""" - def __init__(self, name, light, model): + def __init__(self, name, light, model, unique_id): """Initialize the light device.""" self._name = name + self._light = light self._model = model + self._unique_id = unique_id self._brightness = None - self._color_temp = None - self._light = light + self._available = False self._state = None self._state_attrs = { ATTR_MODEL: self._model, - ATTR_SCENE: None, - ATTR_DELAYED_TURN_OFF: None, } @property @@ -182,6 +216,11 @@ class XiaomiPhilipsGenericLight(Light): """Poll the light.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -190,7 +229,7 @@ class XiaomiPhilipsGenericLight(Light): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -212,12 +251,11 @@ class XiaomiPhilipsGenericLight(Light): """Return the supported features.""" return SUPPORT_BRIGHTNESS - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): + async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) _LOGGER.debug("Response received from light: %s", result) @@ -225,10 +263,10 @@ class XiaomiPhilipsGenericLight(Light): return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -238,30 +276,57 @@ class XiaomiPhilipsGenericLight(Light): "Setting brightness: %s %s%%", brightness, percent_brightness) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness failed: %s", self._light.set_brightness, percent_brightness) if result: self._brightness = brightness else: - yield from self._try_command( + await self._try_command( "Turning the light on failed.", self._light.on) - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the light off.""" - yield from self._try_command( + await self._try_command( "Turning the light off failed.", self._light.off) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException try: - state = yield from self.hass.async_add_job(self._light.status) + state = await self.hass.async_add_job(self._light.status) _LOGGER.debug("Got new state: %s", state) + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): + """Representation of a Generic Xiaomi Philips Light.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_SCENE: None, + ATTR_DELAYED_TURN_OFF: None, + }) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) @@ -276,45 +341,35 @@ class XiaomiPhilipsGenericLight(Light): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) - @asyncio.coroutine - def async_set_scene(self, scene: int = 1): + async def async_set_scene(self, scene: int = 1): """Set the fixed scene.""" - yield from self._try_command( + await self._try_command( "Setting a fixed scene failed.", self._light.set_scene, scene) - @asyncio.coroutine - def async_set_delayed_turn_off(self, time_period: timedelta): - """Set delay off. The unit is different per device.""" - yield from self._try_command( - "Setting the delay off failed.", + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off.""" + await self._try_command( + "Setting the turn off delay failed.", self._light.delay_off, time_period.total_seconds()) - @staticmethod - def translate(value, left_min, left_max, right_min, right_max): - """Map a value from left span to right span.""" - left_span = left_max - left_min - right_span = right_max - right_min - value_scaled = float(value - left_min) / float(left_span) - return int(right_min + (value_scaled * right_span)) - @staticmethod def delayed_turn_off_timestamp(countdown: int, current: datetime, previous: datetime): """Update the turn off timestamp only if necessary.""" - if countdown > 0: + if countdown is not None and countdown > 0: new = current.replace(microsecond=0) + \ timedelta(seconds=countdown) if previous is None: return new - lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION) - upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION) + lower = timedelta(seconds=-DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) + upper = timedelta(seconds=DELAYED_TURN_OFF_MAX_DEVIATION_SECONDS) diff = previous - new if lower < diff < upper: return previous @@ -324,8 +379,14 @@ class XiaomiPhilipsGenericLight(Light): return None -class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): - """Representation of a Xiaomi Philips Light Ball.""" +class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): + """Representation of a Xiaomi Philips Bulb.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._color_temp = None @property def color_temp(self): @@ -347,8 +408,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): """Return the supported features.""" return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -367,7 +427,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): brightness, percent_brightness, color_temp, percent_color_temp) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness and color temperature failed: " "%s bri, %s cct", self._light.set_brightness_and_color_temperature, @@ -383,7 +443,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): "%s mireds, %s%% cct", color_temp, percent_color_temp) - result = yield from self._try_command( + result = await self._try_command( "Setting color temperature failed: %s cct", self._light.set_color_temperature, percent_color_temp) @@ -398,7 +458,7 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): "Setting brightness: %s %s%%", brightness, percent_brightness) - result = yield from self._try_command( + result = await self._try_command( "Setting brightness failed: %s", self._light.set_brightness, percent_brightness) @@ -406,17 +466,17 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): self._brightness = brightness else: - yield from self._try_command( + await self._try_command( "Turning the light on failed.", self._light.on) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException try: - state = yield from self.hass.async_add_job(self._light.status) + state = await self.hass.async_add_job(self._light.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on self._brightness = ceil((255 / 100.0) * state.brightness) self._color_temp = self.translate( @@ -435,13 +495,30 @@ class XiaomiPhilipsLightBall(XiaomiPhilipsGenericLight, Light): }) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + @staticmethod + def translate(value, left_min, left_max, right_min, right_max): + """Map a value from left span to right span.""" + left_span = left_max - left_min + right_span = right_max - right_min + value_scaled = float(value - left_min) / float(left_span) + return int(right_min + (value_scaled * right_span)) -class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light): + +class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Ceiling Lamp.""" + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_NIGHT_LIGHT_MODE: None, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: None, + }) + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" @@ -452,8 +529,191 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsLightBall, Light): """Return the warmest color_temp that this light supports.""" return 370 + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) -class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight, Light): + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + self._color_temp = self.translate( + state.color_temperature, + CCT_MIN, CCT_MAX, + self.max_mireds, self.min_mireds) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_AUTOMATIC_COLOR_TEMPERATURE: + state.automatic_color_temperature, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + +class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Eyecare Lamp 2.""" - pass + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + super().__init__(name, light, model, unique_id) + + self._state_attrs.update({ + ATTR_REMINDER: None, + ATTR_NIGHT_LIGHT_MODE: None, + ATTR_EYECARE_MODE: None, + }) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.is_on + self._brightness = ceil((255 / 100.0) * state.brightness) + + delayed_turn_off = self.delayed_turn_off_timestamp( + state.delay_off_countdown, + dt.utcnow(), + self._state_attrs[ATTR_DELAYED_TURN_OFF]) + + self._state_attrs.update({ + ATTR_SCENE: state.scene, + ATTR_DELAYED_TURN_OFF: delayed_turn_off, + ATTR_REMINDER: state.reminder, + ATTR_NIGHT_LIGHT_MODE: state.smart_night_light, + ATTR_EYECARE_MODE: state.eyecare, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + async def async_set_delayed_turn_off(self, time_period: timedelta): + """Set delayed turn off.""" + await self._try_command( + "Setting the turn off delay failed.", + self._light.delay_off, round(time_period.total_seconds() / 60)) + + async def async_reminder_on(self): + """Enable the eye fatigue notification.""" + await self._try_command( + "Turning on the reminder failed.", + self._light.reminder_on) + + async def async_reminder_off(self): + """Disable the eye fatigue notification.""" + await self._try_command( + "Turning off the reminder failed.", + self._light.reminder_off) + + async def async_night_light_mode_on(self): + """Turn the smart night light mode on.""" + await self._try_command( + "Turning on the smart night light mode failed.", + self._light.smart_night_light_on) + + async def async_night_light_mode_off(self): + """Turn the smart night light mode off.""" + await self._try_command( + "Turning off the smart night light mode failed.", + self._light.smart_night_light_off) + + async def async_eyecare_mode_on(self): + """Turn the eyecare mode on.""" + await self._try_command( + "Turning on the eyecare mode failed.", + self._light.eyecare_on) + + async def async_eyecare_mode_off(self): + """Turn the eyecare mode off.""" + await self._try_command( + "Turning off the eyecare mode failed.", + self._light.eyecare_off) + + @staticmethod + def delayed_turn_off_timestamp(countdown: int, + current: datetime, + previous: datetime): + """Update the turn off timestamp only if necessary.""" + if countdown is not None and countdown > 0: + new = current.replace(second=0, microsecond=0) + \ + timedelta(minutes=countdown) + + if previous is None: + return new + + lower = timedelta(minutes=-DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) + upper = timedelta(minutes=DELAYED_TURN_OFF_MAX_DEVIATION_MINUTES) + diff = previous - new + if lower < diff < upper: + return previous + + return new + + return None + + +class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): + """Representation of a Xiaomi Philips Eyecare Lamp Ambient Light.""" + + def __init__(self, name, light, model, unique_id): + """Initialize the light device.""" + name = '{} Ambient Light'.format(name) + if unique_id is not None: + unique_id = "{}-{}".format(unique_id, 'ambient') + super().__init__(name, light, model, unique_id) + + async def async_turn_on(self, **kwargs): + """Turn the light on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + percent_brightness = ceil(100 * brightness / 255.0) + + _LOGGER.debug( + "Setting brightness of the ambient light: %s %s%%", + brightness, percent_brightness) + + result = await self._try_command( + "Setting brightness of the ambient failed: %s", + self._light.set_ambient_brightness, percent_brightness) + + if result: + self._brightness = brightness + else: + await self._try_command( + "Turning the ambient light on failed.", self._light.ambient_on) + + async def async_turn_off(self, **kwargs): + """Turn the light off.""" + await self._try_command( + "Turning the ambient light off failed.", self._light.ambient_off) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + try: + state = await self.hass.async_add_job(self._light.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.eyecare + self._brightness = ceil((255 / 100.0) * state.ambient_brightness) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) From 88d2a6ab80f39a72dfbbe43cf9ee52f9c569ae5c Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Fri, 16 Mar 2018 21:13:32 +0100 Subject: [PATCH 055/924] Fix guide link in CONTRIBUTING.md (#13272) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9c0c21d0d7..9ad922d7045 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot The process is straight-forward. - - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/devel/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. From ed6cd0ccfaa326e948e6ae9f2da4be6491a40c08 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 21:15:23 +0100 Subject: [PATCH 056/924] Xiaomi MiIO Remote: Unique id added (#13266) * Unique id added. * Provide the exception as "ex" --- .../components/remote/xiaomi_miio.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 30141eaf5e6..91f753391fc 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -18,6 +18,7 @@ from homeassistant.components.remote import ( from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_TOKEN, CONF_TIMEOUT, ATTR_ENTITY_ID, ATTR_HIDDEN, CONF_COMMAND) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow @@ -78,10 +79,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Check that we can communicate with device. try: - device.info() + device_info = device.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) except DeviceException as ex: - _LOGGER.error("Token not accepted by device : %s", ex) - return + _LOGGER.error("Device unavailable or token incorrect: %s", ex) + raise PlatformNotReady if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} @@ -93,9 +100,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hidden = config.get(ATTR_HIDDEN) - xiaomi_miio_remote = XiaomiMiioRemote( - friendly_name, device, slot, timeout, - hidden, config.get(CONF_COMMANDS)) + xiaomi_miio_remote = XiaomiMiioRemote(friendly_name, device, unique_id, + slot, timeout, hidden, + config.get(CONF_COMMANDS)) hass.data[DATA_KEY][host] = xiaomi_miio_remote @@ -158,17 +165,23 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class XiaomiMiioRemote(RemoteDevice): """Representation of a Xiaomi Miio Remote device.""" - def __init__(self, friendly_name, device, + def __init__(self, friendly_name, device, unique_id, slot, timeout, hidden, commands): """Initialize the remote.""" self._name = friendly_name self._device = device + self._unique_id = unique_id self._is_hidden = hidden self._slot = slot self._timeout = timeout self._state = False self._commands = commands + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the remote.""" From d04ba3f86d9aacda0bc1c890068c6ff50463cb17 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 16 Mar 2018 22:13:04 +0100 Subject: [PATCH 057/924] Xiaomi MiIO Sensor: Xiaomi Air Quality Monitor (PM2.5) integration (#13264) * Xiaomi MiIO Sensor: Xiaomi Air Quality Monitor (PM2.5) integration. * Missing newline added. * Use a unique data key per domain. * turn_{on,off} service moved to __init__.py. * All sensors group added. * Sensor is a ToggleEntity now. * is_on property added. * Use Async / await syntax. * Make hound happy. * Unique id added. * Turn on/off service removed from abstract sensor. * Turn on/off methods removed. Device unavailable handling improved. * Unused import removed. * Sensor migrated back to an entity. * Rebased and requirements updated. --- .../components/sensor/xiaomi_miio.py | 168 ++++++++++++++++++ requirements_all.txt | 1 + 2 files changed, 169 insertions(+) create mode 100644 homeassistant/components/sensor/xiaomi_miio.py diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py new file mode 100644 index 00000000000..af7534d9112 --- /dev/null +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -0,0 +1,168 @@ +""" +Support for Xiaomi Mi Air Quality Monitor (PM2.5). + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/sensor.xiaomi_miio/ +""" +from functools import partial +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN) +from homeassistant.exceptions import PlatformNotReady + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Xiaomi Miio Sensor' +DATA_KEY = 'sensor.xiaomi_miio' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + +REQUIREMENTS = ['python-miio==0.3.8'] + +ATTR_POWER = 'power' +ATTR_CHARGING = 'charging' +ATTR_BATTERY_LEVEL = 'battery_level' +ATTR_TIME_STATE = 'time_state' +ATTR_MODEL = 'model' + +SUCCESS = ['ok'] + + +# pylint: disable=unused-argument +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the sensor from config.""" + from miio import AirQualityMonitor, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + + _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + + try: + air_quality_monitor = AirQualityMonitor(host, token) + device_info = air_quality_monitor.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) + device = XiaomiAirQualityMonitor( + name, air_quality_monitor, model, unique_id) + except DeviceException: + raise PlatformNotReady + + hass.data[DATA_KEY][host] = device + async_add_devices([device], update_before_add=True) + + +class XiaomiAirQualityMonitor(Entity): + """Representation of a Xiaomi Air Quality Monitor.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the entity.""" + self._name = name + self._device = device + self._model = model + self._unique_id = unique_id + + self._icon = 'mdi:cloud' + self._unit_of_measurement = 'AQI' + self._available = None + self._state = None + self._state_attrs = { + ATTR_POWER: None, + ATTR_BATTERY_LEVEL: None, + ATTR_CHARGING: None, + ATTR_TIME_STATE: None, + ATTR_MODEL: self._model, + } + + @property + def should_poll(self): + """Poll the miio device.""" + return True + + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of this entity, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the icon to use for device if any.""" + return self._icon + + @property + def available(self): + """Return true when state is known.""" + return self._available + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._state_attrs + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a device command handling error messages.""" + from miio import DeviceException + try: + result = await self.hass.async_add_job( + partial(func, *args, **kwargs)) + + _LOGGER.debug("Response received from miio device: %s", result) + + return result == SUCCESS + except DeviceException as exc: + _LOGGER.error(mask_error, exc) + self._available = False + return False + + async def async_update(self): + """Fetch state from the miio device.""" + from miio import DeviceException + + try: + state = await self.hass.async_add_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.aqi + self._state_attrs.update({ + ATTR_POWER: state.power, + ATTR_CHARGING: state.usb_power, + ATTR_BATTERY_LEVEL: state.battery, + ATTR_TIME_STATE: state.time_state, + }) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index b30b77307c1..732f054f7b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -942,6 +942,7 @@ python-juicenet==0.0.5 # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio # homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio python-miio==0.3.8 From d78e75db66c43683a0711aa501bbb42d14164715 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Mar 2018 15:39:26 -0700 Subject: [PATCH 058/924] Bump frontend to 20180316.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 153d1f6564e..eccc47e05c7 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180315.0'] +REQUIREMENTS = ['home-assistant-frontend==20180316.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 732f054f7b3..f25200ba49e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180315.0 +home-assistant-frontend==20180316.0 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69b56eabc5e..8b3cc8d207a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180315.0 +home-assistant-frontend==20180316.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 5a9013cda58a3a99e1f1e9fa73e18c5895a4f56c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 16 Mar 2018 20:27:05 -0700 Subject: [PATCH 059/924] Refactor Hue: phue -> aiohue (#13043) * phue -> aiohue * Clean up * Fix config * Address comments * Typo * Fix rebase error * Mark light as unavailable when bridge is disconnected * Tests * Make Throttle work with double delay and async * Rework update logic * Don't resolve host to IP * Clarify comment * No longer do unnecessary updates * Add more doc * Another comment update * Wrap up tests * Lint * Fix tests * PyLint does not like mix 'n match async and coroutine * Lint * Update aiohue to 1.2 * Lint * Fix await MagicMock --- homeassistant/components/discovery.py | 16 +- homeassistant/components/hue/__init__.py | 261 ++--- homeassistant/components/light/hue.py | 367 ++++--- homeassistant/components/mqtt/discovery.py | 14 +- homeassistant/components/zwave/__init__.py | 20 +- homeassistant/core.py | 2 +- homeassistant/helpers/discovery.py | 18 +- homeassistant/util/__init__.py | 20 +- requirements_all.txt | 5 +- requirements_test_all.txt | 2 +- tests/components/hue/__init__.py | 1 + tests/components/hue/conftest.py | 17 + tests/components/hue/test_bridge.py | 98 ++ tests/components/hue/test_config_flow.py | 184 ++++ tests/components/hue/test_setup.py | 74 ++ tests/components/light/test_hue.py | 1051 ++++++++++---------- tests/components/test_hue.py | 588 ----------- tests/components/zwave/test_init.py | 14 +- tests/helpers/test_discovery.py | 14 +- tests/util/test_init.py | 8 + 20 files changed, 1289 insertions(+), 1485 deletions(-) create mode 100644 tests/components/hue/__init__.py create mode 100644 tests/components/hue/conftest.py create mode 100644 tests/components/hue/test_bridge.py create mode 100644 tests/components/hue/test_config_flow.py create mode 100644 tests/components/hue/test_setup.py delete mode 100644 tests/components/test_hue.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 21a339602dd..6ab7f42558b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -6,7 +6,6 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. Knows which components handle certain types, will make sure they are loaded before the EVENT_PLATFORM_DISCOVERED is fired. """ -import asyncio import json from datetime import timedelta import logging @@ -84,8 +83,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Start a discovery service.""" from netdisco.discovery import NetworkDiscovery @@ -99,8 +97,7 @@ def async_setup(hass, config): # Platforms ignore by config ignored_platforms = config[DOMAIN][CONF_IGNORE] - @asyncio.coroutine - def new_service_found(service, info): + async def new_service_found(service, info): """Handle a new service if one is found.""" if service in ignored_platforms: logger.info("Ignoring service: %s %s", service, info) @@ -124,15 +121,14 @@ def async_setup(hass, config): component, platform = comp_plat if platform is None: - yield from async_discover(hass, service, info, component, config) + await async_discover(hass, service, info, component, config) else: - yield from async_load_platform( + await async_load_platform( hass, component, platform, info, config) - @asyncio.coroutine - def scan_devices(now): + async def scan_devices(now): """Scan for devices.""" - results = yield from hass.async_add_job(_discover, netdisco) + results = await hass.async_add_job(_discover, netdisco) for result in results: hass.async_add_job(new_service_found(*result)) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index f15052fbd67..2fb55f8f6e0 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -6,22 +6,22 @@ https://home-assistant.io/components/hue/ """ import asyncio import json -from functools import partial +import ipaddress import logging import os -import socket import async_timeout -import requests import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.discovery import SERVICE_HUE from homeassistant.const import CONF_FILENAME, CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries +from homeassistant.util.json import save_json -REQUIREMENTS = ['phue==1.0', 'aiohue==0.3.0'] +REQUIREMENTS = ['aiohue==1.2.0'] _LOGGER = logging.getLogger(__name__) @@ -36,26 +36,23 @@ DEFAULT_ALLOW_UNREACHABLE = False PHUE_CONFIG_FILE = 'phue.conf' -CONF_ALLOW_IN_EMULATED_HUE = "allow_in_emulated_hue" -DEFAULT_ALLOW_IN_EMULATED_HUE = True - CONF_ALLOW_HUE_GROUPS = "allow_hue_groups" DEFAULT_ALLOW_HUE_GROUPS = True -BRIDGE_CONFIG_SCHEMA = vol.Schema([{ - vol.Optional(CONF_HOST): cv.string, +BRIDGE_CONFIG_SCHEMA = vol.Schema({ + # Validate as IP address and then convert back to a string. + vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional(CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE, - default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean, vol.Optional(CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, -}]) +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Optional(CONF_BRIDGES): BRIDGE_CONFIG_SCHEMA, + vol.Optional(CONF_BRIDGES): + vol.All(cv.ensure_list, [BRIDGE_CONFIG_SCHEMA]), }), }, extra=vol.ALLOW_EXTRA) @@ -73,7 +70,7 @@ Press the button on the bridge to register Philips Hue with Home Assistant. """ -def setup(hass, config): +async def async_setup(hass, config): """Set up the Hue platform.""" conf = config.get(DOMAIN) if conf is None: @@ -82,135 +79,130 @@ def setup(hass, config): if DOMAIN not in hass.data: hass.data[DOMAIN] = {} - discovery.listen( - hass, - SERVICE_HUE, - lambda service, discovery_info: - bridge_discovered(hass, service, discovery_info)) + async def async_bridge_discovered(service, discovery_info): + """Dispatcher for Hue discovery events.""" + # Ignore emulated hue + if "HASS Bridge" in discovery_info.get('name', ''): + return + + await async_setup_bridge( + hass, discovery_info['host'], + 'phue-{}.conf'.format(discovery_info['serial'])) + + discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered) # User has configured bridges if CONF_BRIDGES in conf: bridges = conf[CONF_BRIDGES] + # Component is part of config but no bridges specified, discover. elif DOMAIN in config: # discover from nupnp - hosts = requests.get(API_NUPNP).json() - bridges = [{ + websession = aiohttp_client.async_get_clientsession(hass) + + async with websession.get(API_NUPNP) as req: + hosts = await req.json() + + # Run through config schema to populate defaults + bridges = [BRIDGE_CONFIG_SCHEMA({ CONF_HOST: entry['internalipaddress'], CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - } for entry in hosts] + }) for entry in hosts] + else: # Component not specified in config, we're loaded via discovery bridges = [] - for bridge in bridges: - filename = bridge.get(CONF_FILENAME) - allow_unreachable = bridge.get(CONF_ALLOW_UNREACHABLE) - allow_in_emulated_hue = bridge.get(CONF_ALLOW_IN_EMULATED_HUE) - allow_hue_groups = bridge.get(CONF_ALLOW_HUE_GROUPS) + if not bridges: + return True - host = bridge.get(CONF_HOST) - - if host is None: - host = _find_host_from_config(hass, filename) - - if host is None: - _LOGGER.error("No host found in configuration") - return False - - setup_bridge(host, hass, filename, allow_unreachable, - allow_in_emulated_hue, allow_hue_groups) + await asyncio.wait([ + async_setup_bridge( + hass, bridge[CONF_HOST], bridge[CONF_FILENAME], + bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS] + ) for bridge in bridges + ]) return True -def bridge_discovered(hass, service, discovery_info): - """Dispatcher for Hue discovery events.""" - if "HASS Bridge" in discovery_info.get('name', ''): - return - - host = discovery_info.get('host') - serial = discovery_info.get('serial') - - filename = 'phue-{}.conf'.format(serial) - setup_bridge(host, hass, filename) - - -def setup_bridge(host, hass, filename=None, allow_unreachable=False, - allow_in_emulated_hue=True, allow_hue_groups=True, - username=None): +async def async_setup_bridge( + hass, host, filename=None, + allow_unreachable=DEFAULT_ALLOW_UNREACHABLE, + allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS, + username=None): """Set up a given Hue bridge.""" + assert filename or username, 'Need to pass at least a username or filename' + # Only register a device once - if socket.gethostbyname(host) in hass.data[DOMAIN]: + if host in hass.data[DOMAIN]: return + if username is None: + username = await hass.async_add_job( + _find_username_from_config, hass, filename) + bridge = HueBridge(host, hass, filename, username, allow_unreachable, - allow_in_emulated_hue, allow_hue_groups) - bridge.setup() + allow_hue_groups) + await bridge.async_setup() + hass.data[DOMAIN][host] = bridge -def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): - """Attempt to detect host based on existing configuration.""" +def _find_username_from_config(hass, filename): + """Load username from config.""" path = hass.config.path(filename) if not os.path.isfile(path): return None - try: - with open(path) as inp: - return next(iter(json.load(inp).keys())) - except (ValueError, AttributeError, StopIteration): - # ValueError if can't parse as JSON - # AttributeError if JSON value is not a dict - # StopIteration if no keys - return None + with open(path) as inp: + return list(json.load(inp).values())[0]['username'] class HueBridge(object): """Manages a single Hue bridge.""" - def __init__(self, host, hass, filename, username, allow_unreachable=False, - allow_in_emulated_hue=True, allow_hue_groups=True): + def __init__(self, host, hass, filename, username, + allow_unreachable=False, allow_groups=True): """Initialize the system.""" self.host = host - self.bridge_id = socket.gethostbyname(host) self.hass = hass self.filename = filename self.username = username self.allow_unreachable = allow_unreachable - self.allow_in_emulated_hue = allow_in_emulated_hue - self.allow_hue_groups = allow_hue_groups - + self.allow_groups = allow_groups self.available = True - self.bridge = None - self.lights = {} - self.lightgroups = {} - - self.configured = False self.config_request_id = None + self.api = None - hass.data[DOMAIN][self.bridge_id] = self - - def setup(self): + async def async_setup(self): """Set up a phue bridge based on host parameter.""" - import phue + import aiohue + + api = aiohue.Bridge( + self.host, + username=self.username, + websession=aiohttp_client.async_get_clientsession(self.hass) + ) try: - kwargs = {} - if self.username is not None: - kwargs['username'] = self.username - if self.filename is not None: - kwargs['config_file_path'] = \ - self.hass.config.path(self.filename) - self.bridge = phue.Bridge(self.host, **kwargs) - except OSError: # Wrong host was given + with async_timeout.timeout(5): + # Initialize bridge and validate our username + if not self.username: + await api.create_user('home-assistant') + await api.initialize() + except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): + _LOGGER.warning("Connected to Hue at %s but not registered.", + self.host) + self.async_request_configuration() + return + except (asyncio.TimeoutError, aiohue.RequestError): _LOGGER.error("Error connecting to the Hue bridge at %s", self.host) return - except phue.PhueRegistrationException: - _LOGGER.warning("Connected to Hue at %s but not registered.", - self.host) - self.request_configuration() + except aiohue.AiohueException: + _LOGGER.exception('Unknown Hue linking error occurred') + self.async_request_configuration() return except Exception: # pylint: disable=broad-except _LOGGER.exception("Unknown error connecting with Hue bridge at %s", @@ -221,57 +213,77 @@ class HueBridge(object): if self.config_request_id: request_id = self.config_request_id self.config_request_id = None - configurator = self.hass.components.configurator - configurator.request_done(request_id) + self.hass.components.configurator.async_request_done(request_id) - self.configured = True + self.username = api.username - discovery.load_platform( + # Save config file + await self.hass.async_add_job( + save_json, self.hass.config.path(self.filename), + {self.host: {'username': api.username}}) + + self.api = api + + self.hass.async_add_job(discovery.async_load_platform( self.hass, 'light', DOMAIN, - {'bridge_id': self.bridge_id}) + {'host': self.host})) - # create a service for calling run_scene directly on the bridge, - # used to simplify automation rules. - def hue_activate_scene(call): - """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - self.bridge.run_scene(group_name, scene_name) - - self.hass.services.register( - DOMAIN, SERVICE_HUE_SCENE, hue_activate_scene, + self.hass.services.async_register( + DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA) - def request_configuration(self): + @callback + def async_request_configuration(self): """Request configuration steps from the user.""" configurator = self.hass.components.configurator # We got an error if this method is called while we are configuring if self.config_request_id: - configurator.notify_errors( + configurator.async_notify_errors( self.config_request_id, "Failed to register, please try again.") return - self.config_request_id = configurator.request_config( - "Philips Hue", - lambda data: self.setup(), + async def config_callback(data): + """Callback for configurator data.""" + await self.async_setup() + + self.config_request_id = configurator.async_request_config( + "Philips Hue", config_callback, description=CONFIG_INSTRUCTIONS, entity_picture="/static/images/logo_philips_hue.png", submit_caption="I have pressed the button" ) - def get_api(self): - """Return the full api dictionary from phue.""" - return self.bridge.get_api() + async def hue_activate_scene(self, call, updated=False): + """Service to call directly into bridge to set scenes.""" + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] - def set_light(self, light_id, command): - """Adjust properties of one or more lights. See phue for details.""" - return self.bridge.set_light(light_id, command) + group = next( + (group for group in self.api.groups.values() + if group.name == group_name), None) - def set_group(self, light_id, command): - """Change light settings for a group. See phue for detail.""" - return self.bridge.set_group(light_id, command) + scene_id = next( + (scene.id for scene in self.api.scenes.values() + if scene.name == scene_name), None) + + # If we can't find it, fetch latest info. + if not updated and (group is None or scene_id is None): + await self.api.groups.update() + await self.api.scenes.update() + await self.hue_activate_scene(call, updated=True) + return + + if group is None: + _LOGGER.warning('Unable to find group %s', group_name) + return + + if scene_id is None: + _LOGGER.warning('Unable to find scene %s', scene_name) + return + + await group.set_action(scene=scene_id) @config_entries.HANDLERS.register(DOMAIN) @@ -374,7 +386,6 @@ class HueFlowHandler(config_entries.ConfigFlowHandler): async def async_setup_entry(hass, entry): """Set up a bridge for a config entry.""" - await hass.async_add_job(partial( - setup_bridge, entry.data['host'], hass, - username=entry.data['username'])) + await async_setup_bridge(hass, entry.data['host'], + username=entry.data['username']) return True diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 661b7c2b3a1..c45d9c5c44e 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -8,31 +8,23 @@ import asyncio from datetime import timedelta import logging import random -import re -import socket -import voluptuous as vol +import async_timeout import homeassistant.components.hue as hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, - FLASH_LONG, FLASH_SHORT, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) -from homeassistant.const import CONF_FILENAME, CONF_HOST, DEVICE_DEFAULT_NAME -import homeassistant.helpers.config_validation as cv -import homeassistant.util as util -from homeassistant.util import yaml import homeassistant.util.color as color_util DEPENDENCIES = ['hue'] +SCAN_INTERVAL = timedelta(seconds=5) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) - SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) @@ -48,244 +40,232 @@ SUPPORT_HUE = { 'Color temperature light': SUPPORT_HUE_COLOR_TEMP } -ATTR_EMULATED_HUE_HIDDEN = 'emulated_hue_hidden' ATTR_IS_HUE_GROUP = 'is_hue_group' - -# Legacy configuration, will be removed in 0.60 -CONF_ALLOW_UNREACHABLE = 'allow_unreachable' -DEFAULT_ALLOW_UNREACHABLE = False -CONF_ALLOW_IN_EMULATED_HUE = 'allow_in_emulated_hue' -DEFAULT_ALLOW_IN_EMULATED_HUE = True -CONF_ALLOW_HUE_GROUPS = 'allow_hue_groups' -DEFAULT_ALLOW_HUE_GROUPS = True - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_FILENAME): cv.string, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE): cv.boolean, - vol.Optional(CONF_ALLOW_HUE_GROUPS, - default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, -}) - -MIGRATION_ID = 'light_hue_config_migration' -MIGRATION_TITLE = 'Philips Hue Configuration Migration' -MIGRATION_INSTRUCTIONS = """ -Configuration for the Philips Hue component has changed; action required. - -You have configured at least one bridge: - - hue: -{config} - -This configuration is deprecated, please check the -[Hue component](https://home-assistant.io/components/hue/) page for more -information. -""" - -SIGNAL_CALLBACK = 'hue_light_callback_{}_{}' +# Minimum Hue Bridge API version to support groups +# 1.4.0 introduced extended group info +# 1.12 introduced the state object for groups +# 1.13 introduced "any_on" to group state objects +GROUP_MIN_API_VERSION = (1, 13, 0) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Hue lights.""" - if discovery_info is None or 'bridge_id' not in discovery_info: + if discovery_info is None: return - if config is not None and config: - # Legacy configuration, will be removed in 0.60 - config_str = yaml.dump([config]) - # Indent so it renders in a fixed-width font - config_str = re.sub('(?m)^', ' ', config_str) - hass.components.persistent_notification.async_create( - MIGRATION_INSTRUCTIONS.format(config=config_str), - title=MIGRATION_TITLE, - notification_id=MIGRATION_ID) + bridge = hass.data[hue.DOMAIN][discovery_info['host']] + cur_lights = {} + cur_groups = {} - bridge_id = discovery_info['bridge_id'] - bridge = hass.data[hue.DOMAIN][bridge_id] - unthrottled_update_lights(hass, bridge, add_devices) + api_version = tuple( + int(v) for v in bridge.api.config.apiversion.split('.')) + + allow_groups = bridge.allow_groups + if allow_groups and api_version < GROUP_MIN_API_VERSION: + _LOGGER.warning('Please update your Hue bridge to support groups') + allow_groups = False + + # Hue updates all lights via a single API call. + # + # If we call a service to update 2 lights, we only want the API to be + # called once. + # + # The throttle decorator will return right away if a call is currently + # in progress. This means that if we are updating 2 lights, the first one + # is in the update method, the second one will skip it and assume the + # update went through and updates it's data, not good! + # + # The current mechanism will make sure that all lights will wait till + # the update call is done before writing their data to the state machine. + # + # An alternative approach would be to disable automatic polling by Home + # Assistant and take control ourselves. This works great for polling as now + # we trigger from 1 time update an update to all entities. However it gets + # tricky from inside async_turn_on and async_turn_off. + # + # If automatic polling is enabled, Home Assistant will call the entity + # update method after it is done calling all the services. This means that + # when we update, we know all commands have been processed. If we trigger + # the update from inside async_turn_on, the update will not capture the + # changes to the second entity until the next polling update because the + # throttle decorator will prevent the call. + + progress = None + light_progress = set() + group_progress = set() + + async def request_update(is_group, object_id): + """Request an update. + + We will only make 1 request to the server for updating at a time. If a + request is in progress, we will join the request that is in progress. + + This approach is possible because should_poll=True. That means that + Home Assistant will ask lights for updates during a polling cycle or + after it has called a service. + + We keep track of the lights that are waiting for the request to finish. + When new data comes in, we'll trigger an update for all non-waiting + lights. This covers the case where a service is called to enable 2 + lights but in the meanwhile some other light has changed too. + """ + nonlocal progress + + progress_set = group_progress if is_group else light_progress + progress_set.add(object_id) + + if progress is not None: + return await progress + + progress = asyncio.ensure_future(update_bridge()) + result = await progress + progress = None + light_progress.clear() + group_progress.clear() + return result + + async def update_bridge(): + """Update the values of the bridge. + + Will update lights and, if enabled, groups from the bridge. + """ + tasks = [] + tasks.append(async_update_items( + hass, bridge, async_add_devices, request_update, + False, cur_lights, light_progress + )) + + if allow_groups: + tasks.append(async_update_items( + hass, bridge, async_add_devices, request_update, + True, cur_groups, group_progress + )) + + await asyncio.wait(tasks) + + await update_bridge() -@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) -def update_lights(hass, bridge, add_devices): - """Update the Hue light objects with latest info from the bridge.""" - return unthrottled_update_lights(hass, bridge, add_devices) +async def async_update_items(hass, bridge, async_add_devices, + request_bridge_update, is_group, current, + progress_waiting): + """Update either groups or lights from the bridge.""" + import aiohue - -def unthrottled_update_lights(hass, bridge, add_devices): - """Update the lights (Internal version of update_lights).""" - import phue - - if not bridge.configured: - return + if is_group: + api = bridge.api.groups + else: + api = bridge.api.lights try: - api = bridge.get_api() - except phue.PhueRequestTimeout: - _LOGGER.warning("Timeout trying to reach the bridge") - bridge.available = False - return - except ConnectionRefusedError: - _LOGGER.error("The bridge refused the connection") - bridge.available = False - return - except socket.error: - # socket.error when we cannot reach Hue - _LOGGER.exception("Cannot reach the bridge") + with async_timeout.timeout(4): + await api.update() + except (asyncio.TimeoutError, aiohue.AiohueException): + if not bridge.available: + return + + _LOGGER.error('Unable to reach bridge %s', bridge.host) bridge.available = False + + for light_id, light in current.items(): + if light_id not in progress_waiting: + light.async_schedule_update_ha_state() + return - bridge.available = True + if not bridge.available: + _LOGGER.info('Reconnected to bridge %s', bridge.host) + bridge.available = True - new_lights = process_lights( - hass, api, bridge, - lambda **kw: update_lights(hass, bridge, add_devices, **kw)) - if bridge.allow_hue_groups: - new_lightgroups = process_groups( - hass, api, bridge, - lambda **kw: update_lights(hass, bridge, add_devices, **kw)) - new_lights.extend(new_lightgroups) + new_lights = [] + + for item_id in api: + if item_id not in current: + current[item_id] = HueLight( + api[item_id], request_bridge_update, bridge, is_group) + + new_lights.append(current[item_id]) + elif item_id not in progress_waiting: + current[item_id].async_schedule_update_ha_state() if new_lights: - add_devices(new_lights) - - -def process_lights(hass, api, bridge, update_lights_cb): - """Set up HueLight objects for all lights.""" - api_lights = api.get('lights') - - if not isinstance(api_lights, dict): - _LOGGER.error("Got unexpected result from Hue API") - return [] - - new_lights = [] - - for light_id, info in api_lights.items(): - if light_id not in bridge.lights: - bridge.lights[light_id] = HueLight( - int(light_id), info, bridge, - update_lights_cb, - bridge.allow_unreachable, - bridge.allow_in_emulated_hue) - new_lights.append(bridge.lights[light_id]) - else: - bridge.lights[light_id].info = info - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_CALLBACK.format( - bridge.bridge_id, - bridge.lights[light_id].light_id)) - - return new_lights - - -def process_groups(hass, api, bridge, update_lights_cb): - """Set up HueLight objects for all groups.""" - api_groups = api.get('groups') - - if not isinstance(api_groups, dict): - _LOGGER.error('Got unexpected result from Hue API') - return [] - - new_lights = [] - - for lightgroup_id, info in api_groups.items(): - if 'state' not in info: - _LOGGER.warning( - "Group info does not contain state. Please update your hub") - return [] - - if lightgroup_id not in bridge.lightgroups: - bridge.lightgroups[lightgroup_id] = HueLight( - int(lightgroup_id), info, bridge, - update_lights_cb, - bridge.allow_unreachable, - bridge.allow_in_emulated_hue, True) - new_lights.append(bridge.lightgroups[lightgroup_id]) - else: - bridge.lightgroups[lightgroup_id].info = info - hass.helpers.dispatcher.dispatcher_send( - SIGNAL_CALLBACK.format( - bridge.bridge_id, - bridge.lightgroups[lightgroup_id].light_id)) - - return new_lights + async_add_devices(new_lights) class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, light_id, info, bridge, update_lights_cb, - allow_unreachable, allow_in_emulated_hue, is_group=False): + def __init__(self, light, request_bridge_update, bridge, is_group=False): """Initialize the light.""" - self.light_id = light_id - self.info = info + self.light = light + self.async_request_bridge_update = request_bridge_update self.bridge = bridge - self.update_lights = update_lights_cb - self.allow_unreachable = allow_unreachable self.is_group = is_group - self.allow_in_emulated_hue = allow_in_emulated_hue if is_group: - self._command_func = self.bridge.set_group + self.is_osram = False + self.is_philips = False else: - self._command_func = self.bridge.set_light + self.is_osram = light.manufacturername == 'OSRAM' + self.is_philips = light.manufacturername == 'Philips' @property def unique_id(self): """Return the ID of this Hue light.""" - return self.info.get('uniqueid') + return self.light.uniqueid @property def name(self): """Return the name of the Hue light.""" - return self.info.get('name', DEVICE_DEFAULT_NAME) + return self.light.name @property def brightness(self): """Return the brightness of this light between 0..255.""" if self.is_group: - return self.info['action'].get('bri') - return self.info['state'].get('bri') + return self.light.action.get('bri') + return self.light.state.get('bri') @property def xy_color(self): """Return the XY color value.""" if self.is_group: - return self.info['action'].get('xy') - return self.info['state'].get('xy') + return self.light.action.get('xy') + return self.light.state.get('xy') @property def color_temp(self): """Return the CT color value.""" if self.is_group: - return self.info['action'].get('ct') - return self.info['state'].get('ct') + return self.light.action.get('ct') + return self.light.state.get('ct') @property def is_on(self): """Return true if device is on.""" if self.is_group: - return self.info['state']['any_on'] - return self.info['state']['on'] + return self.light.state['any_on'] + return self.light.state['on'] @property def available(self): """Return if light is available.""" return self.bridge.available and (self.is_group or - self.allow_unreachable or - self.info['state']['reachable']) + self.bridge.allow_unreachable or + self.light.state['reachable']) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_HUE.get(self.info.get('type'), SUPPORT_HUE_EXTENDED) + return SUPPORT_HUE.get(self.light.type, SUPPORT_HUE_EXTENDED) @property def effect_list(self): """Return the list of supported effects.""" return [EFFECT_COLORLOOP, EFFECT_RANDOM] - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the specified or all lights on.""" command = {'on': True} @@ -293,7 +273,7 @@ class HueLight(Light): command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_XY_COLOR in kwargs: - if self.info.get('manufacturername') == 'OSRAM': + if self.is_osram: color_hue, sat = color_util.color_xy_to_hs( *kwargs[ATTR_XY_COLOR]) command['hue'] = color_hue / 360 * 65535 @@ -301,7 +281,7 @@ class HueLight(Light): else: command['xy'] = kwargs[ATTR_XY_COLOR] elif ATTR_RGB_COLOR in kwargs: - if self.info.get('manufacturername') == 'OSRAM': + if self.is_osram: hsv = color_util.color_RGB_to_hsv( *(int(val) for val in kwargs[ATTR_RGB_COLOR])) command['hue'] = hsv[0] / 360 * 65535 @@ -336,12 +316,15 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - elif self.info.get('manufacturername') == 'Philips': + elif self.is_philips: command['effect'] = 'none' - self._command_func(self.light_id, command) + if self.is_group: + await self.light.set_action(**command) + else: + await self.light.set_state(**command) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the specified or all lights off.""" command = {'on': False} @@ -359,27 +342,19 @@ class HueLight(Light): else: command['alert'] = 'none' - self._command_func(self.light_id, command) + if self.is_group: + await self.light.set_action(**command) + else: + await self.light.set_state(**command) - def update(self): + async def async_update(self): """Synchronize state with bridge.""" - self.update_lights(no_throttle=True) + await self.async_request_bridge_update(self.is_group, self.light.id) @property def device_state_attributes(self): """Return the device state attributes.""" attributes = {} - if not self.allow_in_emulated_hue: - attributes[ATTR_EMULATED_HUE_HIDDEN] = \ - not self.allow_in_emulated_hue if self.is_group: attributes[ATTR_IS_HUE_GROUP] = self.is_group return attributes - - @asyncio.coroutine - def async_added_to_hass(self): - """Register update callback.""" - dev_id = self.bridge.bridge_id, self.light_id - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CALLBACK.format(*dev_id), - self.async_schedule_update_ha_state) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index b6f6a1c5a92..d0164706626 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,7 +4,6 @@ Support for MQTT discovery. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/#discovery """ -import asyncio import json import logging import re @@ -35,19 +34,16 @@ ALLOWED_PLATFORMS = { ALREADY_DISCOVERED = 'mqtt_discovered_components' -@asyncio.coroutine -def async_start(hass, discovery_topic, hass_config): +async def async_start(hass, discovery_topic, hass_config): """Initialize of MQTT Discovery.""" - # pylint: disable=unused-variable - @asyncio.coroutine - def async_device_message_received(topic, payload, qos): + async def async_device_message_received(topic, payload, qos): """Process the received message.""" match = TOPIC_MATCHER.match(topic) if not match: return - prefix_topic, component, node_id, object_id = match.groups() + _prefix_topic, component, node_id, object_id = match.groups() try: payload = json.loads(payload) @@ -88,10 +84,10 @@ def async_start(hass, discovery_topic, hass_config): _LOGGER.info("Found new component: %s %s", component, discovery_id) - yield from async_load_platform( + await async_load_platform( hass, component, platform, payload, hass_config) - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( hass, discovery_topic + '/#', async_device_message_received, 0) return True diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index ad4ae66df17..43aa996c799 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -203,8 +203,8 @@ def get_config_value(node, value_index, tries=5): return None -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Z-Wave platform (generic part).""" if discovery_info is None or DATA_NETWORK not in hass.data: return False @@ -504,8 +504,7 @@ def setup(hass, config): "target node:%s, instance=%s", node_id, group, target_node_id, instance) - @asyncio.coroutine - def async_refresh_entity(service): + async def async_refresh_entity(service): """Refresh values that specific entity depends on.""" entity_id = service.data.get(ATTR_ENTITY_ID) async_dispatcher_send( @@ -559,8 +558,7 @@ def setup(hass, config): network.start() hass.bus.fire(const.EVENT_NETWORK_START) - @asyncio.coroutine - def _check_awaked(): + async def _check_awaked(): """Wait for Z-wave awaked state (or timeout) and finalize start.""" _LOGGER.debug( "network state: %d %s", network.state, @@ -585,7 +583,7 @@ def setup(hass, config): network.state_str) break else: - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) hass.async_add_job(_finalize_start) @@ -798,11 +796,10 @@ class ZWaveDeviceEntityValues(): dict_id = id(self) - @asyncio.coroutine - def discover_device(component, device, dict_id): + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device - yield from discovery.async_load_platform( + await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) self._hass.add_job(discover_device, component, device, dict_id) @@ -844,8 +841,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.update_properties() self.maybe_schedule_update() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Add device to dict.""" async_dispatcher_connect( self.hass, diff --git a/homeassistant/core.py b/homeassistant/core.py index b49b94f853d..65db82a1fbe 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -79,7 +79,7 @@ def callback(func: Callable[..., None]) -> Callable[..., None]: def is_callback(func: Callable[..., Any]) -> bool: """Check if function is safe to be called in the event loop.""" - return '_hass_callback' in func.__dict__ + return '_hass_callback' in getattr(func, '__dict__', {}) @callback diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 82322fec1e5..cb587c432c1 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,8 +5,6 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -import asyncio - from homeassistant import setup, core from homeassistant.loader import bind_hass from homeassistant.const import ( @@ -58,17 +56,16 @@ def discover(hass, service, discovered=None, component=None, hass_config=None): async_discover(hass, service, discovered, component, hass_config)) -@asyncio.coroutine @bind_hass -def async_discover(hass, service, discovered=None, component=None, - hass_config=None): +async def async_discover(hass, service, discovered=None, component=None, + hass_config=None): """Fire discovery event. Can ensure a component is loaded.""" if component in DEPENDENCY_BLACKLIST: raise HomeAssistantError( 'Cannot discover the {} component.'.format(component)) if component is not None and component not in hass.config.components: - yield from setup.async_setup_component( + await setup.async_setup_component( hass, component, hass_config) data = { @@ -134,10 +131,9 @@ def load_platform(hass, component, platform, discovered=None, hass_config)) -@asyncio.coroutine @bind_hass -def async_load_platform(hass, component, platform, discovered=None, - hass_config=None): +async def async_load_platform(hass, component, platform, discovered=None, + hass_config=None): """Load a component and platform dynamically. Target components will be loaded and an EVENT_PLATFORM_DISCOVERED will be @@ -148,7 +144,7 @@ def async_load_platform(hass, component, platform, discovered=None, Use `listen_platform` to register a callback for these events. - Warning: Do not yield from this inside a setup method to avoid a dead lock. + Warning: Do not await this inside a setup method to avoid a dead lock. Use `hass.async_add_job(async_load_platform(..))` instead. This method is a coroutine. @@ -160,7 +156,7 @@ def async_load_platform(hass, component, platform, discovered=None, setup_success = True if component not in hass.config.components: - setup_success = yield from setup.async_setup_component( + setup_success = await setup.async_setup_component( hass, component, hass_config) # No need to fire event if we could not setup component diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index a869251dc3c..82ba6a734f8 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -261,6 +261,16 @@ class Throttle(object): def __call__(self, method): """Caller for the throttle.""" + # Make sure we return a coroutine if the method is async. + if asyncio.iscoroutinefunction(method): + async def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + else: + def throttled_value(): + """Stand-in function for when real func is being throttled.""" + return None + if self.limit_no_throttle is not None: method = Throttle(self.limit_no_throttle)(method) @@ -277,16 +287,6 @@ class Throttle(object): is_func = (not hasattr(method, '__self__') and '.' not in method.__qualname__.split('..')[-1]) - # Make sure we return a coroutine if the method is async. - if asyncio.iscoroutinefunction(method): - async def throttled_value(): - """Stand-in function for when real func is being throttled.""" - return None - else: - def throttled_value(): - """Stand-in function for when real func is being throttled.""" - return None - @wraps(method) def wrapper(*args, **kwargs): """Wrap that allows wrapped to be called only once per min_time. diff --git a/requirements_all.txt b/requirements_all.txt index f25200ba49e..839987611bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ aiodns==1.1.1 aiohttp_cors==0.6.0 # homeassistant.components.hue -aiohue==0.3.0 +aiohue==1.2.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 @@ -568,9 +568,6 @@ pdunehd==1.3 # homeassistant.components.media_player.pandora pexpect==4.0.1 -# homeassistant.components.hue -phue==1.0 - # homeassistant.components.rpi_pfio pifacecommon==4.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8b3cc8d207a..d41f9589de2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.6.0 # homeassistant.components.hue -aiohue==0.3.0 +aiohue==1.2.0 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/hue/__init__.py b/tests/components/hue/__init__.py new file mode 100644 index 00000000000..8cff8700aaf --- /dev/null +++ b/tests/components/hue/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hue component.""" diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py new file mode 100644 index 00000000000..7ccc202b31b --- /dev/null +++ b/tests/components/hue/conftest.py @@ -0,0 +1,17 @@ +"""Fixtures for Hue tests.""" +from unittest.mock import patch + +import pytest + +from tests.common import mock_coro_func + + +@pytest.fixture +def mock_bridge(): + """Mock the HueBridge from initializing.""" + with patch('homeassistant.components.hue._find_username_from_config', + return_value=None), \ + patch('homeassistant.components.hue.HueBridge') as mock_bridge: + mock_bridge().async_setup = mock_coro_func() + mock_bridge.reset_mock() + yield mock_bridge diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py new file mode 100644 index 00000000000..88a7223d91e --- /dev/null +++ b/tests/components/hue/test_bridge.py @@ -0,0 +1,98 @@ +"""Test Hue bridge.""" +import asyncio +from unittest.mock import Mock, patch + +import aiohue +import pytest + +from homeassistant.components import hue + +from tests.common import mock_coro + + +class MockBridge(hue.HueBridge): + """Class that sets default for constructor.""" + + def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf', + username=None, **kwargs): + """Initialize a mock bridge.""" + super().__init__(host, hass, filename, username, **kwargs) + + +@pytest.fixture +def mock_request(): + """Mock configurator.async_request_config.""" + with patch('homeassistant.components.configurator.' + 'async_request_config') as mock_request: + yield mock_request + + +async def test_setup_request_config_button_not_pressed(hass, mock_request): + """Test we request config if link button has not been pressed.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + +async def test_setup_request_config_invalid_username(hass, mock_request): + """Test we request config if username is no longer whitelisted.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.Unauthorized): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + +async def test_setup_timeout(hass, mock_request): + """Test we give up when there is a timeout.""" + with patch('aiohue.Bridge.create_user', + side_effect=asyncio.TimeoutError): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 0 + + +async def test_only_create_no_username(hass): + """.""" + with patch('aiohue.Bridge.create_user') as mock_create, \ + patch('aiohue.Bridge.initialize') as mock_init: + await MockBridge(hass, username='bla').async_setup() + + assert len(mock_create.mock_calls) == 0 + assert len(mock_init.mock_calls) == 1 + + +async def test_configurator_callback(hass, mock_request): + """.""" + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + await MockBridge(hass).async_setup() + + assert len(mock_request.mock_calls) == 1 + + callback = mock_request.mock_calls[0][1][2] + + mock_init = Mock(return_value=mock_coro()) + mock_create = Mock(return_value=mock_coro()) + + with patch('aiohue.Bridge') as mock_bridge, \ + patch('homeassistant.helpers.discovery.async_load_platform', + return_value=mock_coro()) as mock_load_platform, \ + patch('homeassistant.components.hue.save_json') as mock_save: + inst = mock_bridge() + inst.username = 'mock-user' + inst.create_user = mock_create + inst.initialize = mock_init + await callback(None) + + assert len(mock_create.mock_calls) == 1 + assert len(mock_init.mock_calls) == 1 + assert len(mock_save.mock_calls) == 1 + assert mock_save.mock_calls[0][1][1] == { + '1.2.3.4': { + 'username': 'mock-user' + } + } + assert len(mock_load_platform.mock_calls) == 1 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py new file mode 100644 index 00000000000..959e3c6241b --- /dev/null +++ b/tests/components/hue/test_config_flow.py @@ -0,0 +1,184 @@ +"""Tests for Philips Hue config flow.""" +import asyncio +from unittest.mock import patch + +import aiohue +import pytest +import voluptuous as vol + +from homeassistant.components import hue + +from tests.common import MockConfigEntry, mock_coro + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow .""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + + flow = hue.HueFlowHandler() + flow.hass = hass + await flow.async_step_init() + + with patch('aiohue.Bridge') as mock_bridge: + def mock_constructor(host, websession): + mock_bridge.host = host + return mock_bridge + + mock_bridge.side_effect = mock_constructor + mock_bridge.username = 'username-abc' + mock_bridge.config.name = 'Mock Bridge' + mock_bridge.config.bridgeid = 'bridge-id-1234' + mock_bridge.create_user.return_value = mock_coro() + mock_bridge.initialize.return_value = mock_coro() + + result = await flow.async_step_link(user_input={}) + + assert mock_bridge.host == '1.2.3.4' + assert len(mock_bridge.create_user.mock_calls) == 1 + assert len(mock_bridge.initialize.mock_calls) == 1 + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '1.2.3.4', + 'bridge_id': 'bridge-id-1234', + 'username': 'username-abc' + } + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): + """Test config flow discovers only already configured bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'} + ]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'}, + {'internalipaddress': '5.6.7.8', 'id': 'beer'} + ]) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + {'internalipaddress': '1.2.3.4', 'id': 'bla'}, + {'internalipaddress': '5.6.7.8', 'id': 'beer'} + ]) + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = hue.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert flow.host == '5.6.7.8' + + +async def test_flow_timeout_discovery(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.discovery.discover_nupnp', + side_effect=asyncio.TimeoutError): + result = await flow.async_step_init() + + assert result['type'] == 'abort' + + +async def test_flow_link_timeout(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=asyncio.TimeoutError): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } + + +async def test_flow_link_button_not_pressed(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.LinkButtonNotPressed): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } + + +async def test_flow_link_unknown_host(hass): + """Test config flow .""" + flow = hue.HueFlowHandler() + flow.hass = hass + + with patch('aiohue.Bridge.create_user', + side_effect=aiohue.RequestError): + result = await flow.async_step_link({}) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == { + 'base': 'register_failed' + } diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py new file mode 100644 index 00000000000..690419fcb7a --- /dev/null +++ b/tests/components/hue/test_setup.py @@ -0,0 +1,74 @@ +"""Test Hue setup process.""" +from homeassistant.setup import async_setup_component +from homeassistant.components import hue +from homeassistant.components.discovery import SERVICE_HUE + + +async def test_setup_with_multiple_hosts(hass, mock_bridge): + """Multiple hosts specified in the config file.""" + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: [ + {hue.CONF_HOST: '127.0.0.1'}, + {hue.CONF_HOST: '192.168.1.10'}, + ] + } + }) + + assert len(mock_bridge.mock_calls) == 2 + hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) + assert hosts == ['127.0.0.1', '192.168.1.10'] + assert len(hass.data[hue.DOMAIN]) == 2 + + +async def test_bridge_discovered(hass, mock_bridge): + """Bridge discovery.""" + assert await async_setup_component(hass, hue.DOMAIN, {}) + + await hass.helpers.discovery.async_discover(SERVICE_HUE, { + 'host': '192.168.1.10', + 'serial': '1234567', + }) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_calls) == 1 + assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' + assert len(hass.data[hue.DOMAIN]) == 1 + + +async def test_bridge_configure_and_discovered(hass, mock_bridge): + """Bridge is in the config file, then we discover it.""" + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '192.168.1.10' + } + } + }) + + assert len(mock_bridge.mock_calls) == 1 + assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' + assert len(hass.data[hue.DOMAIN]) == 1 + + mock_bridge.reset_mock() + + await hass.helpers.discovery.async_discover(SERVICE_HUE, { + 'host': '192.168.1.10', + 'serial': '1234567', + }) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_calls) == 0 + assert len(hass.data[hue.DOMAIN]) == 1 + + +async def test_setup_no_host(hass, aioclient_mock): + """Check we call discovery if domain specified but no bridges.""" + aioclient_mock.get(hue.API_NUPNP, json=[]) + + result = await async_setup_component( + hass, hue.DOMAIN, {hue.DOMAIN: {}}) + assert result + + assert len(aioclient_mock.mock_calls) == 1 + assert len(hass.data[hue.DOMAIN]) == 0 diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 559467d5e9a..8abf51fdf0c 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -1,545 +1,590 @@ """Philips Hue lights platform tests.""" - +import asyncio +from collections import deque import logging -import unittest -import unittest.mock as mock -from unittest.mock import call, MagicMock, patch +from unittest.mock import Mock + +import aiohue +from aiohue.lights import Lights +from aiohue.groups import Groups +import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light -from tests.common import get_test_home_assistant, MockDependency - _LOGGER = logging.getLogger(__name__) HUE_LIGHT_NS = 'homeassistant.components.light.hue.' - - -class TestSetup(unittest.TestCase): - """Test the Hue light platform.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - def setup_mocks_for_update_lights(self): - """Set up all mocks for update_lights tests.""" - self.mock_bridge = MagicMock() - self.mock_bridge.bridge_id = 'bridge-id' - self.mock_bridge.allow_hue_groups = False - self.mock_api = MagicMock() - self.mock_bridge.get_api.return_value = self.mock_api - self.mock_add_devices = MagicMock() - - def setup_mocks_for_process_lights(self): - """Set up all mocks for process_lights tests.""" - self.mock_bridge = self.create_mock_bridge('host') - self.mock_api = MagicMock() - self.mock_api.get.return_value = {} - self.mock_bridge.get_api.return_value = self.mock_api - - def setup_mocks_for_process_groups(self): - """Set up all mocks for process_groups tests.""" - self.mock_bridge = self.create_mock_bridge('host') - self.mock_bridge.get_group.return_value = { - 'name': 'Group 0', 'state': {'any_on': True}} - - self.mock_api = MagicMock() - self.mock_api.get.return_value = {} - self.mock_bridge.get_api.return_value = self.mock_api - - def create_mock_bridge(self, host, allow_hue_groups=True): - """Return a mock HueBridge with reasonable defaults.""" - mock_bridge = MagicMock() - mock_bridge.bridge_id = 'bridge-id' - mock_bridge.host = host - mock_bridge.allow_hue_groups = allow_hue_groups - mock_bridge.lights = {} - mock_bridge.lightgroups = {} - return mock_bridge - - def create_mock_lights(self, lights): - """Return a dict suitable for mocking api.get('lights').""" - mock_bridge_lights = lights - - for info in mock_bridge_lights.values(): - if 'state' not in info: - info['state'] = {'on': False} - - return mock_bridge_lights - - def build_mock_light(self, bridge, light_id, name): - """Return a mock HueLight.""" - light = MagicMock() - light.bridge = bridge - light.light_id = light_id - light.name = name - return light - - def test_setup_platform_no_discovery_info(self): - """Test setup_platform without discovery info.""" - self.hass.data[hue.DOMAIN] = {} - mock_add_devices = MagicMock() - - hue_light.setup_platform(self.hass, {}, mock_add_devices) - - mock_add_devices.assert_not_called() - - def test_setup_platform_no_bridge_id(self): - """Test setup_platform without a bridge.""" - self.hass.data[hue.DOMAIN] = {} - mock_add_devices = MagicMock() - - hue_light.setup_platform(self.hass, {}, mock_add_devices, {}) - - mock_add_devices.assert_not_called() - - def test_setup_platform_one_bridge(self): - """Test setup_platform with one bridge.""" - mock_bridge = MagicMock() - self.hass.data[hue.DOMAIN] = {'10.0.0.1': mock_bridge} - mock_add_devices = MagicMock() - - with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ - as mock_update_lights: - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '10.0.0.1'}) - mock_update_lights.assert_called_once_with( - self.hass, mock_bridge, mock_add_devices) - - def test_setup_platform_multiple_bridges(self): - """Test setup_platform wuth multiple bridges.""" - mock_bridge = MagicMock() - mock_bridge2 = MagicMock() - self.hass.data[hue.DOMAIN] = { - '10.0.0.1': mock_bridge, - '192.168.0.10': mock_bridge2, +GROUP_RESPONSE = { + "1": { + "name": "Group 1", + "lights": [ + "1", + "2" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 254, + "hue": 10000, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, } - mock_add_devices = MagicMock() - - with patch(HUE_LIGHT_NS + 'unthrottled_update_lights') \ - as mock_update_lights: - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '10.0.0.1'}) - hue_light.setup_platform( - self.hass, {}, mock_add_devices, - {'bridge_id': '192.168.0.10'}) - - mock_update_lights.assert_has_calls([ - call(self.hass, mock_bridge, mock_add_devices), - call(self.hass, mock_bridge2, mock_add_devices), - ]) - - @MockDependency('phue') - def test_update_lights_with_no_lights(self, mock_phue): - """Test the update_lights function when no lights are found.""" - self.setup_mocks_for_update_lights() - - with patch(HUE_LIGHT_NS + 'process_lights', return_value=[]) \ - as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_not_called() - self.mock_add_devices.assert_not_called() - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_some_lights(self, mock_phue): - """Test the update_lights function with some lights.""" - self.setup_mocks_for_update_lights() - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_not_called() - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_no_groups(self, mock_phue): - """Test the update_lights function when no groups are found.""" - self.setup_mocks_for_update_lights() - self.mock_bridge.allow_hue_groups = True - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', return_value=[]) \ - as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_lights_and_groups(self, mock_phue): - """Test the update_lights function with both lights and groups.""" - self.setup_mocks_for_update_lights() - self.mock_bridge.allow_hue_groups = True - mock_lights = [ - self.build_mock_light(self.mock_bridge, 42, 'some'), - self.build_mock_light(self.mock_bridge, 84, 'light'), - ] - mock_groups = [ - self.build_mock_light(self.mock_bridge, 15, 'and'), - self.build_mock_light(self.mock_bridge, 72, 'groups'), - ] - - with patch(HUE_LIGHT_NS + 'process_lights', - return_value=mock_lights) as mock_process_lights: - with patch(HUE_LIGHT_NS + 'process_groups', - return_value=mock_groups) as mock_process_groups: - with patch.object(self.hass.helpers.dispatcher, - 'dispatcher_send') as dispatcher_send: - hue_light.unthrottled_update_lights( - self.hass, self.mock_bridge, self.mock_add_devices) - - mock_process_lights.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - mock_process_groups.assert_called_once_with( - self.hass, self.mock_api, self.mock_bridge, mock.ANY) - # note that mock_lights has been modified in place and - # now contains both lights and groups - self.mock_add_devices.assert_called_once_with( - mock_lights) - dispatcher_send.assert_not_called() - - @MockDependency('phue') - def test_update_lights_with_two_bridges(self, mock_phue): - """Test the update_lights function with two bridges.""" - self.setup_mocks_for_update_lights() - - mock_bridge_one = self.create_mock_bridge('one', False) - mock_bridge_one_lights = self.create_mock_lights( - {1: {'name': 'b1l1'}, 2: {'name': 'b1l2'}}) - - mock_bridge_two = self.create_mock_bridge('two', False) - mock_bridge_two_lights = self.create_mock_lights( - {1: {'name': 'b2l1'}, 3: {'name': 'b2l3'}}) - - with patch('homeassistant.components.light.hue.HueLight.' - 'schedule_update_ha_state'): - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_one_lights - with patch.object(mock_bridge_one, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_one, self.mock_add_devices) - - mock_api = MagicMock() - mock_api.get.return_value = mock_bridge_two_lights - with patch.object(mock_bridge_two, 'get_api', - return_value=mock_api): - hue_light.unthrottled_update_lights( - self.hass, mock_bridge_two, self.mock_add_devices) - - self.assertEqual(sorted(mock_bridge_one.lights.keys()), [1, 2]) - self.assertEqual(sorted(mock_bridge_two.lights.keys()), [1, 3]) - - self.assertEqual(len(self.mock_add_devices.mock_calls), 2) - - # first call - name, args, kwargs = self.mock_add_devices.mock_calls[0] - self.assertEqual(len(args), 1) - self.assertEqual(len(kwargs), 0) - - # second call works the same - name, args, kwargs = self.mock_add_devices.mock_calls[1] - self.assertEqual(len(args), 1) - self.assertEqual(len(kwargs), 0) - - def test_process_lights_api_error(self): - """Test the process_lights function when the bridge errors out.""" - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = None - - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - self.assertEqual(self.mock_bridge.lights, {}) - - def test_process_lights_no_lights(self): - """Test the process_lights function when bridge returns no lights.""" - self.setup_mocks_for_process_lights() - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - mock_dispatcher_send.assert_not_called() - self.assertEqual(self.mock_bridge.lights, {}) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_lights_some_lights(self, mock_hue_light): - """Test the process_lights function with multiple groups.""" - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - mock_dispatcher_send.assert_not_called() - self.assertEqual(len(self.mock_bridge.lights), 2) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_lights_new_light(self, mock_hue_light): - """ - Test the process_lights function with new groups. - - Test what happens when we already have a light and a new one shows up. - """ - self.setup_mocks_for_process_lights() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lights = { - 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_lights( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue), - ]) - mock_dispatcher_send.assert_called_once_with( - 'hue_light_callback_bridge-id_1') - self.assertEqual(len(self.mock_bridge.lights), 2) - - def test_process_groups_api_error(self): - """Test the process_groups function when the bridge errors out.""" - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = None - - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - self.assertEqual(self.mock_bridge.lightgroups, {}) - - def test_process_groups_no_state(self): - """Test the process_groups function when bridge returns no status.""" - self.setup_mocks_for_process_groups() - self.mock_bridge.get_group.return_value = {'name': 'Group 0'} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual([], ret) - mock_dispatcher_send.assert_not_called() - self.assertEqual(self.mock_bridge.lightgroups, {}) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_groups_some_groups(self, mock_hue_light): - """Test the process_groups function with multiple groups.""" - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 2) - mock_hue_light.assert_has_calls([ - call( - 1, {'state': 'on'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - mock_dispatcher_send.assert_not_called() - self.assertEqual(len(self.mock_bridge.lightgroups), 2) - - @patch(HUE_LIGHT_NS + 'HueLight') - def test_process_groups_new_group(self, mock_hue_light): - """ - Test the process_groups function with new groups. - - Test what happens when we already have a light and a new one shows up. - """ - self.setup_mocks_for_process_groups() - self.mock_api.get.return_value = { - 1: {'state': 'on'}, 2: {'state': 'off'}} - self.mock_bridge.lightgroups = { - 1: self.build_mock_light(self.mock_bridge, 1, 'foo')} - - with patch.object(self.hass.helpers.dispatcher, 'dispatcher_send') \ - as mock_dispatcher_send: - ret = hue_light.process_groups( - self.hass, self.mock_api, self.mock_bridge, None) - - self.assertEqual(len(ret), 1) - mock_hue_light.assert_has_calls([ - call( - 2, {'state': 'off'}, self.mock_bridge, mock.ANY, - self.mock_bridge.allow_unreachable, - self.mock_bridge.allow_in_emulated_hue, True), - ]) - mock_dispatcher_send.assert_called_once_with( - 'hue_light_callback_bridge-id_1') - self.assertEqual(len(self.mock_bridge.lightgroups), 2) + }, + "2": { + "name": "Group 2", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 153, + "hue": 4345, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, + } + } +} +LIGHT_1_ON = { + "state": { + "on": True, + "bri": 144, + "hue": 13088, + "sat": 212, + "xy": [0.5128, 0.4147], + "ct": 467, + "alert": "none", + "effect": "none", + "colormode": "xy", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 1", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "456", +} +LIGHT_1_OFF = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "xy", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 1", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "456", +} +LIGHT_2_OFF = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", +} +LIGHT_2_ON = { + "state": { + "on": True, + "bri": 100, + "hue": 13088, + "sat": 210, + "xy": [.5, .4], + "ct": 420, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2 new", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", +} +LIGHT_RESPONSE = { + "1": LIGHT_1_ON, + "2": LIGHT_2_OFF, +} -class TestHueLight(unittest.TestCase): - """Test the HueLight class.""" +@pytest.fixture +def mock_bridge(hass): + """Mock a Hue bridge.""" + bridge = Mock(available=True, allow_groups=False, host='1.1.1.1') + bridge.mock_requests = [] + # We're using a deque so we can schedule multiple responses + # and also means that `popleft()` will blow up if we get more updates + # than expected. + bridge.mock_light_responses = deque() + bridge.mock_group_responses = deque() - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False + async def mock_request(method, path, **kwargs): + kwargs['method'] = method + kwargs['path'] = path + bridge.mock_requests.append(kwargs) - self.light_id = 42 - self.mock_info = MagicMock() - self.mock_bridge = MagicMock() - self.mock_update_lights = MagicMock() - self.mock_allow_unreachable = MagicMock() - self.mock_is_group = MagicMock() - self.mock_allow_in_emulated_hue = MagicMock() - self.mock_is_group = False + if path == 'lights': + return bridge.mock_light_responses.popleft() + elif path == 'groups': + return bridge.mock_group_responses.popleft() + return None - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() + bridge.api.config.apiversion = '9.9.9' + bridge.api.lights = Lights({}, mock_request) + bridge.api.groups = Groups({}, mock_request) - def buildLight( - self, light_id=None, info=None, update_lights=None, is_group=None): - """Helper to build a HueLight object with minimal fuss.""" - if 'state' not in info: - on_key = 'any_on' if is_group is not None else 'on' - info['state'] = {on_key: False} + return bridge - return hue_light.HueLight( - light_id if light_id is not None else self.light_id, - info if info is not None else self.mock_info, - self.mock_bridge, - (update_lights - if update_lights is not None - else self.mock_update_lights), - self.mock_allow_unreachable, self.mock_allow_in_emulated_hue, - is_group if is_group is not None else self.mock_is_group) - def test_unique_id_for_light(self): - """Test the unique_id method with lights.""" - light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEqual('foobar', light.unique_id) +async def setup_bridge(hass, mock_bridge): + """Load the Hue light platform with the provided bridge.""" + hass.config.components.add(hue.DOMAIN) + hass.data[hue.DOMAIN] = {'mock-host': mock_bridge} + await hass.helpers.discovery.async_load_platform('light', 'hue', { + 'host': 'mock-host' + }) + await hass.async_block_till_done() - light = self.buildLight(info={}) - self.assertIsNone(light.unique_id) - def test_unique_id_for_group(self): - """Test the unique_id method with groups.""" - light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEqual('foobar', light.unique_id) +async def test_not_load_groups_if_old_bridge(hass, mock_bridge): + """Test that we don't try to load gorups if bridge runs old software.""" + mock_bridge.api.config.apiversion = '1.12.0' + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 0 - light = self.buildLight(info={}, is_group=True) - self.assertIsNone(light.unique_id) + +async def test_no_lights_or_groups(hass, mock_bridge): + """Test the update_lights function when no lights are found.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append({}) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 0 + + +async def test_lights(hass, mock_bridge): + """Test the update_lights function with some lights.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + # 1 All Lights group, 2 lights + assert len(hass.states.async_all()) == 3 + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['color_temp'] == 467 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.state == 'off' + + +async def test_groups(hass, mock_bridge): + """Test the update_lights function with some lights.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + # 1 all lights group, 2 hue group lights + assert len(hass.states.async_all()) == 3 + + lamp_1 = hass.states.get('light.group_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 254 + assert lamp_1.attributes['color_temp'] == 250 + + lamp_2 = hass.states.get('light.group_2') + assert lamp_2 is not None + assert lamp_2.state == 'on' + + +async def test_new_group_discovered(hass, mock_bridge): + """Test if 2nd update has a new group.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + new_group_response = dict(GROUP_RESPONSE) + new_group_response['3'] = { + "name": "Group 3", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": True, + "bri": 153, + "hue": 4345, + "sat": 254, + "effect": "none", + "xy": [ + 0.5, + 0.5 + ], + "ct": 250, + "alert": "select", + "colormode": "ct" + }, + "state": { + "any_on": True, + "all_on": False, + } + } + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(new_group_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.group_1' + }, blocking=True) + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 4 + + new_group = hass.states.get('light.group_3') + assert new_group is not None + assert new_group.state == 'on' + assert new_group.attributes['brightness'] == 153 + assert new_group.attributes['color_temp'] == 250 + + +async def test_new_light_discovered(hass, mock_bridge): + """Test if 2nd update has a new light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + new_light_response = dict(LIGHT_RESPONSE) + new_light_response['3'] = { + "state": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "xy": [0, 0], + "ct": 0, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 3", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "789", + } + + mock_bridge.mock_light_responses.append(new_light_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 + + light = hass.states.get('light.hue_lamp_3') + assert light is not None + assert light.state == 'off' + + +async def test_other_group_update(hass, mock_bridge): + """Test changing one group that will impact the state of other light.""" + mock_bridge.allow_groups = True + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 3 + + group_2 = hass.states.get('light.group_2') + assert group_2 is not None + assert group_2.name == 'Group 2' + assert group_2.state == 'on' + assert group_2.attributes['brightness'] == 153 + assert group_2.attributes['color_temp'] == 250 + + updated_group_response = dict(GROUP_RESPONSE) + updated_group_response['2'] = { + "name": "Group 2 new", + "lights": [ + "3", + "4", + "5" + ], + "type": "LightGroup", + "action": { + "on": False, + "bri": 0, + "hue": 0, + "sat": 0, + "effect": "none", + "xy": [ + 0, + 0 + ], + "ct": 0, + "alert": "none", + "colormode": "ct" + }, + "state": { + "any_on": False, + "all_on": False, + } + } + + mock_bridge.mock_light_responses.append({}) + mock_bridge.mock_group_responses.append(updated_group_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.group_1' + }, blocking=True) + # 2x group update, 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 3 + + group_2 = hass.states.get('light.group_2') + assert group_2 is not None + assert group_2.name == 'Group 2 new' + assert group_2.state == 'off' + + +async def test_other_light_update(hass, mock_bridge): + """Test changing one light that will impact state of other light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 3 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.name == 'Hue Lamp 2' + assert lamp_2.state == 'off' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['2'] = { + "state": { + "on": True, + "bri": 100, + "hue": 13088, + "sat": 210, + "xy": [.5, .4], + "ct": 420, + "alert": "none", + "effect": "none", + "colormode": "hs", + "reachable": True + }, + "type": "Extended color light", + "name": "Hue Lamp 2 new", + "modelid": "LCT001", + "swversion": "66009461", + "manufacturername": "Philips", + "uniqueid": "123", + } + + mock_bridge.mock_light_responses.append(updated_light_response) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_1' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 3 + + lamp_2 = hass.states.get('light.hue_lamp_2') + assert lamp_2 is not None + assert lamp_2.name == 'Hue Lamp 2 new' + assert lamp_2.state == 'on' + assert lamp_2.attributes['brightness'] == 100 + + +async def test_update_timeout(hass, mock_bridge): + """Test bridge marked as not available if timeout error during update.""" + mock_bridge.api.lights.update = Mock(side_effect=asyncio.TimeoutError) + mock_bridge.api.groups.update = Mock(side_effect=asyncio.TimeoutError) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 0 + assert len(hass.states.async_all()) == 0 + assert mock_bridge.available is False + + +async def test_update_unauthorized(hass, mock_bridge): + """Test bridge marked as not available if unauthorized during update.""" + mock_bridge.api.lights.update = Mock(side_effect=aiohue.Unauthorized) + mock_bridge.api.groups.update = Mock(side_effect=aiohue.Unauthorized) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 0 + assert len(hass.states.async_all()) == 0 + assert mock_bridge.available is False + + +async def test_light_turn_on_service(hass, mock_bridge): + """Test calling the turn on service on a light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + light = hass.states.get('light.hue_lamp_2') + assert light is not None + assert light.state == 'off' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['2'] = LIGHT_2_ON + + mock_bridge.mock_light_responses.append(updated_light_response) + + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2', + 'brightness': 100, + 'color_temp': 300, + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + assert mock_bridge.mock_requests[1]['json'] == { + 'bri': 100, + 'on': True, + 'ct': 300, + 'effect': 'none', + 'alert': 'none', + } + + assert len(hass.states.async_all()) == 3 + + light = hass.states.get('light.hue_lamp_2') + assert light is not None + assert light.state == 'on' + + +async def test_light_turn_off_service(hass, mock_bridge): + """Test calling the turn on service on a light.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + light = hass.states.get('light.hue_lamp_1') + assert light is not None + assert light.state == 'on' + + updated_light_response = dict(LIGHT_RESPONSE) + updated_light_response['1'] = LIGHT_1_OFF + + mock_bridge.mock_light_responses.append(updated_light_response) + + await hass.services.async_call('light', 'turn_off', { + 'entity_id': 'light.hue_lamp_1', + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + assert mock_bridge.mock_requests[1]['json'] == { + 'on': False, + 'alert': 'none', + } + + assert len(hass.states.async_all()) == 3 + + light = hass.states.get('light.hue_lamp_1') + assert light is not None + assert light.state == 'off' def test_available(): """Test available property.""" light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=False, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=False), is_group=False, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is False light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=True, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=True), is_group=False, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is True light = hue_light.HueLight( - info={'state': {'reachable': False}}, - allow_unreachable=False, + light=Mock(state={'reachable': False}), + request_bridge_update=None, + bridge=Mock(allow_unreachable=False), is_group=True, - - light_id=None, - bridge=mock.Mock(), - update_lights_cb=None, - allow_in_emulated_hue=False, ) assert light.available is True diff --git a/tests/components/test_hue.py b/tests/components/test_hue.py deleted file mode 100644 index 78f8b573666..00000000000 --- a/tests/components/test_hue.py +++ /dev/null @@ -1,588 +0,0 @@ -"""Generic Philips Hue component tests.""" -import asyncio -import logging -import unittest -from unittest.mock import call, MagicMock, patch - -import aiohue -import pytest -import voluptuous as vol - -from homeassistant.components import configurator, hue -from homeassistant.const import CONF_FILENAME, CONF_HOST -from homeassistant.setup import setup_component, async_setup_component - -from tests.common import ( - assert_setup_component, get_test_home_assistant, get_test_config_dir, - MockDependency, MockConfigEntry, mock_coro -) - -_LOGGER = logging.getLogger(__name__) - - -class TestSetup(unittest.TestCase): - """Test the Hue component.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - @MockDependency('phue') - def test_setup_no_domain(self, mock_phue): - """If it's not in the config we won't even try.""" - with assert_setup_component(0): - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, {})) - mock_phue.Bridge.assert_not_called() - self.assertEqual({}, self.hass.data[hue.DOMAIN]) - - @MockDependency('phue') - def test_setup_with_host(self, mock_phue): - """Host specified in the config file.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: 'localhost'}]}})) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_setup_with_phue_conf(self, mock_phue): - """No host in the config file, but one is cached in phue.conf.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch( - 'homeassistant.components.hue._find_host_from_config', - return_value='localhost'): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_FILENAME: 'phue.conf'}]}})) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_setup_with_multiple_hosts(self, mock_phue): - """Multiple hosts specified in the config file.""" - mock_bridge = mock_phue.Bridge - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: 'localhost'}, - {CONF_HOST: '192.168.0.1'}]}})) - - mock_bridge.assert_has_calls([ - call( - 'localhost', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)), - call( - '192.168.0.1', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE))]) - mock_load.mock_bridge.assert_not_called() - mock_load.assert_has_calls([ - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '127.0.0.1'}), - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.0.1'}), - ], any_order=True) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(2, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_bridge_discovered(self, mock_phue): - """Bridge discovery.""" - mock_bridge = mock_phue.Bridge - mock_service = MagicMock() - discovery_info = {'host': '192.168.0.10', 'serial': 'foobar'} - - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, {})) - hue.bridge_discovered(self.hass, mock_service, discovery_info) - - mock_bridge.assert_called_once_with( - '192.168.0.10', - config_file_path=get_test_config_dir('phue-foobar.conf')) - mock_load.assert_called_once_with( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.0.10'}) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - @MockDependency('phue') - def test_bridge_configure_and_discovered(self, mock_phue): - """Bridge is in the config file, then we discover it.""" - mock_bridge = mock_phue.Bridge - mock_service = MagicMock() - discovery_info = {'host': '192.168.1.10', 'serial': 'foobar'} - - with assert_setup_component(1): - with patch('homeassistant.helpers.discovery.load_platform') \ - as mock_load: - # First we set up the component from config - self.assertTrue(setup_component( - self.hass, hue.DOMAIN, - {hue.DOMAIN: {hue.CONF_BRIDGES: [ - {CONF_HOST: '192.168.1.10'}]}})) - - mock_bridge.assert_called_once_with( - '192.168.1.10', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - calls_to_mock_load = [ - call( - self.hass, 'light', hue.DOMAIN, - {'bridge_id': '192.168.1.10'}), - ] - mock_load.assert_has_calls(calls_to_mock_load) - - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - # Then we discover the same bridge - hue.bridge_discovered(self.hass, mock_service, discovery_info) - - # No additional calls - mock_bridge.assert_called_once_with( - '192.168.1.10', - config_file_path=get_test_config_dir( - hue.PHUE_CONFIG_FILE)) - mock_load.assert_has_calls(calls_to_mock_load) - - # Still only one - self.assertTrue(hue.DOMAIN in self.hass.data) - self.assertEqual(1, len(self.hass.data[hue.DOMAIN])) - - -class TestHueBridge(unittest.TestCase): - """Test the HueBridge class.""" - - def setUp(self): # pylint: disable=invalid-name - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.data[hue.DOMAIN] = {} - self.skip_teardown_stop = False - - def tearDown(self): - """Stop everything that was started.""" - if not self.skip_teardown_stop: - self.hass.stop() - - @MockDependency('phue') - def test_setup_bridge_connection_refused(self, mock_phue): - """Test a registration failed with a connection refused exception.""" - mock_bridge = mock_phue.Bridge - mock_bridge.side_effect = ConnectionRefusedError() - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertTrue(bridge.config_request_id is None) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - - @MockDependency('phue') - def test_setup_bridge_registration_exception(self, mock_phue): - """Test a registration failed with an exception.""" - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = mock_phue.PhueRegistrationException(1, 2) - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - self.assertTrue(isinstance(bridge.config_request_id, str)) - - mock_bridge.assert_called_once_with( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - - @MockDependency('phue') - def test_setup_bridge_registration_succeeds(self, mock_phue): - """Test a registration success sequence.""" - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, registration is done - None, - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertTrue(bridge.configured) - self.assertTrue(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # Make sure the request is done - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configured', self.hass.states.all()[0].state) - - @MockDependency('phue') - def test_setup_bridge_registration_fails(self, mock_phue): - """ - Test a registration failure sequence. - - This may happen when we start the registration process, the user - responds to the request but the bridge has become unreachable. - """ - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, the bridge has gone away - ConnectionRefusedError(), - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # The request should still be pending - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configure', self.hass.states.all()[0].state) - - @MockDependency('phue') - def test_setup_bridge_registration_retry(self, mock_phue): - """ - Test a registration retry sequence. - - This may happen when we start the registration process, the user - responds to the request but we fail to confirm it with the bridge. - """ - mock_bridge = mock_phue.Bridge - mock_phue.PhueRegistrationException = Exception - mock_bridge.side_effect = [ - # First call, raise because not registered - mock_phue.PhueRegistrationException(1, 2), - # Second call, for whatever reason authentication fails - mock_phue.PhueRegistrationException(1, 2), - ] - - bridge = hue.HueBridge( - 'localhost', self.hass, hue.PHUE_CONFIG_FILE, None) - bridge.setup() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # Simulate the user confirming the registration - self.hass.services.call( - configurator.DOMAIN, configurator.SERVICE_CONFIGURE, - {configurator.ATTR_CONFIGURE_ID: bridge.config_request_id}) - - self.hass.block_till_done() - self.assertFalse(bridge.configured) - self.assertFalse(bridge.config_request_id is None) - - # We should see a total of two identical calls - args = call( - 'localhost', - config_file_path=get_test_config_dir(hue.PHUE_CONFIG_FILE)) - mock_bridge.assert_has_calls([args, args]) - - # Make sure the request is done - self.assertEqual(1, len(self.hass.states.all())) - self.assertEqual('configure', self.hass.states.all()[0].state) - self.assertEqual( - 'Failed to register, please try again.', - self.hass.states.all()[0].attributes.get(configurator.ATTR_ERRORS)) - - @MockDependency('phue') - def test_hue_activate_scene(self, mock_phue): - """Test the hue_activate_scene service.""" - with patch('homeassistant.helpers.discovery.load_platform'): - bridge = hue.HueBridge('localhost', self.hass, - hue.PHUE_CONFIG_FILE, None) - bridge.setup() - - # No args - self.hass.services.call(hue.DOMAIN, hue.SERVICE_HUE_SCENE, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - # Only one arg - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_GROUP_NAME: 'group'}, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_SCENE_NAME: 'scene'}, - blocking=True) - bridge.bridge.run_scene.assert_not_called() - - # Both required args - self.hass.services.call( - hue.DOMAIN, hue.SERVICE_HUE_SCENE, - {hue.ATTR_GROUP_NAME: 'group', hue.ATTR_SCENE_NAME: 'scene'}, - blocking=True) - bridge.bridge.run_scene.assert_called_once_with('group', 'scene') - - -async def test_setup_no_host(hass, requests_mock): - """No host specified in any way.""" - requests_mock.get(hue.API_NUPNP, json=[]) - with MockDependency('phue') as mock_phue: - result = await async_setup_component( - hass, hue.DOMAIN, {hue.DOMAIN: {}}) - assert result - - mock_phue.Bridge.assert_not_called() - - assert hass.data[hue.DOMAIN] == {} - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow .""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - - flow = hue.HueFlowHandler() - flow.hass = hass - await flow.async_step_init() - - with patch('aiohue.Bridge') as mock_bridge: - def mock_constructor(host, websession): - mock_bridge.host = host - return mock_bridge - - mock_bridge.side_effect = mock_constructor - mock_bridge.username = 'username-abc' - mock_bridge.config.name = 'Mock Bridge' - mock_bridge.config.bridgeid = 'bridge-id-1234' - mock_bridge.create_user.return_value = mock_coro() - mock_bridge.initialize.return_value = mock_coro() - - result = await flow.async_step_link(user_input={}) - - assert mock_bridge.host == '1.2.3.4' - assert len(mock_bridge.create_user.mock_calls) == 1 - assert len(mock_bridge.initialize.mock_calls) == 1 - - assert result['type'] == 'create_entry' - assert result['title'] == 'Mock Bridge' - assert result['data'] == { - 'host': '1.2.3.4', - 'bridge_id': 'bridge-id-1234', - 'username': 'username-abc' - } - - -async def test_flow_no_discovered_bridges(hass, aioclient_mock): - """Test config flow discovers no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): - """Test config flow discovers only already configured bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - MockConfigEntry(domain='hue', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_one_bridge_discovered(hass, aioclient_mock): - """Test config flow discovers one bridge.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'} - ]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - - -async def test_flow_two_bridges_discovered(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'}, - {'internalipaddress': '5.6.7.8', 'id': 'beer'} - ]) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'init' - - with pytest.raises(vol.Invalid): - assert result['data_schema']({'host': '0.0.0.0'}) - - result['data_schema']({'host': '1.2.3.4'}) - result['data_schema']({'host': '5.6.7.8'}) - - -async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ - {'internalipaddress': '1.2.3.4', 'id': 'bla'}, - {'internalipaddress': '5.6.7.8', 'id': 'beer'} - ]) - MockConfigEntry(domain='hue', data={ - 'host': '1.2.3.4' - }).add_to_hass(hass) - flow = hue.HueFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert flow.host == '5.6.7.8' - - -async def test_flow_timeout_discovery(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.discovery.discover_nupnp', - side_effect=asyncio.TimeoutError): - result = await flow.async_step_init() - - assert result['type'] == 'abort' - - -async def test_flow_link_timeout(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=asyncio.TimeoutError): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'register_failed' - } - - -async def test_flow_link_button_not_pressed(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'register_failed' - } - - -async def test_flow_link_unknown_host(hass): - """Test config flow .""" - flow = hue.HueFlowHandler() - flow.hass = hass - - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.RequestError): - result = await flow.async_step_link({}) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == { - 'base': 'register_failed' - } diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index cdbf91d09e5..30c9d3ba489 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -17,7 +17,7 @@ from homeassistant.setup import setup_component import pytest from tests.common import ( - get_test_home_assistant, async_fire_time_changed) + get_test_home_assistant, async_fire_time_changed, mock_coro) from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues @@ -468,6 +468,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_discovery(self, discovery, get_platform): """Test the creation of a new entity.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -500,8 +501,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): key=lambda a: id(a))) assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[0] == self.hass assert args[1] == 'mock_component' @@ -532,6 +532,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_existing_values(self, discovery, get_platform): """Test the loading of already discovered values.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -563,8 +564,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): key=lambda a: id(a))) assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[0] == self.hass assert args[1] == 'mock_component' @@ -599,6 +599,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): """Test ignore workaround.""" + discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform mock_device = MagicMock() @@ -629,8 +630,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): self.hass.block_till_done() assert discovery.async_load_platform.called - # Second call is to async yield from - assert len(discovery.async_load_platform.mock_calls) == 2 + assert len(discovery.async_load_platform.mock_calls) == 1 args = discovery.async_load_platform.mock_calls[0][1] assert args[1] == 'binary_sensor' diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 2087dc2adb5..b345400ba17 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -1,5 +1,4 @@ """Test discovery helpers.""" -import asyncio from unittest.mock import patch import pytest @@ -24,7 +23,8 @@ class TestHelpersDiscovery: """Stop everything that was started.""" self.hass.stop() - @patch('homeassistant.setup.async_setup_component') + @patch('homeassistant.setup.async_setup_component', + return_value=mock_coro()) def test_listen(self, mock_setup_component): """Test discovery listen/discover combo.""" helpers = self.hass.helpers @@ -199,15 +199,13 @@ class TestHelpersDiscovery: assert len(component_calls) == 1 -@asyncio.coroutine -def test_load_platform_forbids_config(): +async def test_load_platform_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - yield from discovery.async_load_platform(None, 'config', 'zwave') + await discovery.async_load_platform(None, 'config', 'zwave') -@asyncio.coroutine -def test_discover_forbids_config(): +async def test_discover_forbids_config(): """Test you cannot setup config component with load_platform.""" with pytest.raises(HomeAssistantError): - yield from discovery.async_discover(None, None, None, 'config') + await discovery.async_discover(None, None, None, 'config') diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 5493843c246..60b0e68ca59 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -291,3 +291,11 @@ async def test_throttle_async(): assert (await test_method()) is True assert (await test_method()) is None + + @util.Throttle(timedelta(seconds=2), timedelta(seconds=0.1)) + async def test_method2(): + """Only first call should return a value.""" + return True + + assert (await test_method2()) is True + assert (await test_method2()) is None From 66c6f9cdd66517cb49cd0e30c38c46d09f7d2d35 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 17 Mar 2018 11:51:40 +0100 Subject: [PATCH 060/924] Unused xiaomi miio sensor method removed (#13281) * Unused method removed. * remove unused import --- homeassistant/components/sensor/xiaomi_miio.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index af7534d9112..cb172735ac4 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -4,7 +4,6 @@ Support for Xiaomi Mi Air Quality Monitor (PM2.5). For more details about this platform, please refer to the documentation https://home-assistant.io/components/sensor.xiaomi_miio/ """ -from functools import partial import logging import voluptuous as vol @@ -131,21 +130,6 @@ class XiaomiAirQualityMonitor(Entity): """Return the state attributes of the device.""" return self._state_attrs - async def _try_command(self, mask_error, func, *args, **kwargs): - """Call a device command handling error messages.""" - from miio import DeviceException - try: - result = await self.hass.async_add_job( - partial(func, *args, **kwargs)) - - _LOGGER.debug("Response received from miio device: %s", result) - - return result == SUCCESS - except DeviceException as exc: - _LOGGER.error(mask_error, exc) - self._available = False - return False - async def async_update(self): """Fetch state from the miio device.""" from miio import DeviceException From 05676ba18b02746d0b49befd20ef1088180e77de Mon Sep 17 00:00:00 2001 From: thrawnarn Date: Sat, 17 Mar 2018 12:14:01 +0100 Subject: [PATCH 061/924] Changed to async/await (#13246) * Changed to async/await * Hound fixes * Lint fixes * Changed sleep --- .../components/media_player/bluesound.py | 132 +++++++----------- 1 file changed, 52 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index a07e577c969..1b6310d4cab 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Bluesound platforms.""" if DATA_BLUESOUND not in hass.data: hass.data[DATA_BLUESOUND] = [] @@ -149,8 +149,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): hass, async_add_devices, host.get(CONF_HOST), host.get(CONF_PORT), host.get(CONF_NAME)) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to method of Bluesound devices.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -166,7 +165,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): target_players = hass.data[DATA_BLUESOUND] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service]['schema'] @@ -211,13 +210,12 @@ class BluesoundPlayer(MediaPlayerDevice): except ValueError: return -1 - @asyncio.coroutine - def force_update_sync_status( + async def force_update_sync_status( self, on_updated_cb=None, raise_timeout=False): """Update the internal status.""" resp = None try: - resp = yield from self.send_bluesound_command( + resp = await self.send_bluesound_command( 'SyncStatus', raise_timeout, raise_timeout) except Exception: raise @@ -254,16 +252,15 @@ class BluesoundPlayer(MediaPlayerDevice): on_updated_cb() return True - @asyncio.coroutine - def _start_poll_command(self): + async def _start_poll_command(self): """Loop which polls the status of the player.""" try: while True: - yield from self.async_update_status() + await self.async_update_status() except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self._name) - yield from asyncio.sleep( + await asyncio.sleep( NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop) self.start_polling() @@ -282,15 +279,14 @@ class BluesoundPlayer(MediaPlayerDevice): """Stop the polling task.""" self._polling_task.cancel() - @asyncio.coroutine - def async_init(self): + async def async_init(self, triggered=None): """Initialize the player async.""" try: if self._retry_remove is not None: self._retry_remove() self._retry_remove = None - yield from self.force_update_sync_status( + await self.force_update_sync_status( self._init_callback, True) except (asyncio.TimeoutError, ClientError): _LOGGER.info("Node %s is offline, retrying later", self.host) @@ -301,20 +297,18 @@ class BluesoundPlayer(MediaPlayerDevice): self.host) raise - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update internal status of the entity.""" if not self._is_online: return - yield from self.async_update_sync_status() - yield from self.async_update_presets() - yield from self.async_update_captures() - yield from self.async_update_services() + await self.async_update_sync_status() + await self.async_update_presets() + await self.async_update_captures() + await self.async_update_services() - @asyncio.coroutine - def send_bluesound_command(self, method, raise_timeout=False, - allow_offline=False): + async def send_bluesound_command(self, method, raise_timeout=False, + allow_offline=False): """Send command to the player.""" import xmltodict @@ -330,10 +324,10 @@ class BluesoundPlayer(MediaPlayerDevice): try: websession = async_get_clientsession(self._hass) with async_timeout.timeout(10, loop=self._hass.loop): - response = yield from websession.get(url) + response = await websession.get(url) if response.status == 200: - result = yield from response.text() + result = await response.text() if len(result) < 1: data = None else: @@ -352,8 +346,7 @@ class BluesoundPlayer(MediaPlayerDevice): return data - @asyncio.coroutine - def async_update_status(self): + async def async_update_status(self): """Use the poll session to always get the status of the player.""" import xmltodict response = None @@ -372,7 +365,7 @@ class BluesoundPlayer(MediaPlayerDevice): try: with async_timeout.timeout(125, loop=self._hass.loop): - response = yield from self._polling_session.get( + response = await self._polling_session.get( url, headers={CONNECTION: KEEP_ALIVE}) @@ -380,7 +373,7 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.error("Error %s on %s. Trying one more time.", response.status, url) else: - result = yield from response.text() + result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() self._status = xmltodict.parse(result)['status'].copy() @@ -392,8 +385,8 @@ class BluesoundPlayer(MediaPlayerDevice): self._group_name = group_name # the sleep is needed to make sure that the # devices is synced - yield from asyncio.sleep(1, loop=self._hass.loop) - yield from self.async_trigger_sync_on_all() + await asyncio.sleep(1, loop=self._hass.loop) + await self.async_trigger_sync_on_all() elif self.is_grouped: # when player is grouped we need to fetch volume from # sync_status. We will force an update if the player is @@ -402,7 +395,7 @@ class BluesoundPlayer(MediaPlayerDevice): # the device is playing. This would solve alot of # problems. This change will be done when the # communication is moved to a separate library - yield from self.force_update_sync_status() + await self.force_update_sync_status() self.async_schedule_update_ha_state() @@ -415,13 +408,12 @@ class BluesoundPlayer(MediaPlayerDevice): self._name) raise - @asyncio.coroutine - def async_trigger_sync_on_all(self): + async def async_trigger_sync_on_all(self): """Trigger sync status update on all devices.""" _LOGGER.debug("Trigger sync status on all devices") for player in self._hass.data[DATA_BLUESOUND]: - yield from player.force_update_sync_status() + await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) async def async_update_sync_status(self, on_updated_cb=None, @@ -788,8 +780,7 @@ class BluesoundPlayer(MediaPlayerDevice): """Return true if shuffle is active.""" return True if self._status.get('shuffle', '0') == '1' else False - @asyncio.coroutine - def async_join(self, master): + async def async_join(self, master): """Join the player to a group.""" master_device = [device for device in self.hass.data[DATA_BLUESOUND] if device.entity_id == master] @@ -798,37 +789,33 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.debug("Trying to join player: %s to master: %s", self.host, master_device[0].host) - yield from master_device[0].async_add_slave(self) + await master_device[0].async_add_slave(self) else: _LOGGER.error("Master not found %s", master_device) - @asyncio.coroutine - def async_unjoin(self): + async def async_unjoin(self): """Unjoin the player from a group.""" if self._master is None: return _LOGGER.debug("Trying to unjoin player: %s", self.host) - yield from self._master.async_remove_slave(self) + await self._master.async_remove_slave(self) - @asyncio.coroutine - def async_add_slave(self, slave_device): + async def async_add_slave(self, slave_device): """Add slave to master.""" return self.send_bluesound_command('/AddSlave?slave={}&port={}' .format(slave_device.host, slave_device.port)) - @asyncio.coroutine - def async_remove_slave(self, slave_device): + async def async_remove_slave(self, slave_device): """Remove slave to master.""" return self.send_bluesound_command('/RemoveSlave?slave={}&port={}' .format(slave_device.host, slave_device.port)) - @asyncio.coroutine - def async_increase_timer(self): + async def async_increase_timer(self): """Increase sleep time on player.""" - sleep_time = yield from self.send_bluesound_command('/Sleep') + sleep_time = await self.send_bluesound_command('/Sleep') if sleep_time is None: _LOGGER.error('Error while increasing sleep time on player: %s', self.host) @@ -836,21 +823,18 @@ class BluesoundPlayer(MediaPlayerDevice): return int(sleep_time.get('sleep', '0')) - @asyncio.coroutine - def async_clear_timer(self): + async def async_clear_timer(self): """Clear sleep timer on player.""" sleep = 1 while sleep > 0: - sleep = yield from self.async_increase_timer() + sleep = await self.async_increase_timer() - @asyncio.coroutine - def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" return self.send_bluesound_command('/Shuffle?state={}' .format('1' if shuffle else '0')) - @asyncio.coroutine - def async_select_source(self, source): + async def async_select_source(self, source): """Select input source.""" if self.is_grouped and not self.is_master: return @@ -874,16 +858,14 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(url) - @asyncio.coroutine - def async_clear_playlist(self): + async def async_clear_playlist(self): """Clear players playlist.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Clear') - @asyncio.coroutine - def async_media_next_track(self): + async def async_media_next_track(self): """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return @@ -897,8 +879,7 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(cmd) - @asyncio.coroutine - def async_media_previous_track(self): + async def async_media_previous_track(self): """Send media_previous command to media player.""" if self.is_grouped and not self.is_master: return @@ -912,40 +893,35 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(cmd) - @asyncio.coroutine - def async_media_play(self): + async def async_media_play(self): """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Play') - @asyncio.coroutine - def async_media_pause(self): + async def async_media_pause(self): """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Pause') - @asyncio.coroutine - def async_media_stop(self): + async def async_media_stop(self): """Send stop command.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Pause') - @asyncio.coroutine - def async_media_seek(self, position): + async def async_media_seek(self, position): """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return return self.send_bluesound_command('Play?seek=' + str(float(position))) - @asyncio.coroutine - def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """ Send the play_media command to the media player. @@ -961,24 +937,21 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command(url) - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Volume up the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return return self.async_set_volume_level(((current_vol*100)+1)/100) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Volume down the media player.""" current_vol = self.volume_level if not current_vol or current_vol < 0: return return self.async_set_volume_level(((current_vol*100)-1)/100) - @asyncio.coroutine - def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Send volume_up command to media player.""" if volume < 0: volume = 0 @@ -987,8 +960,7 @@ class BluesoundPlayer(MediaPlayerDevice): return self.send_bluesound_command( 'Volume?level=' + str(float(volume) * 100)) - @asyncio.coroutine - def async_mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command to media player.""" if mute: volume = self.volume_level From f5093b474a010a8059e148417f5a2484e1cff90b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 17 Mar 2018 12:27:21 +0100 Subject: [PATCH 062/924] Python 3.5 async with (#13283) --- homeassistant/helpers/entity_platform.py | 2 +- homeassistant/helpers/restore_state.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 712121bbdb5..501ab5057a3 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -311,7 +311,7 @@ class EntityPlatform(object): self.scan_interval) return - with (await self._process_updates): + async with self._process_updates: tasks = [] for entity in self.entities.values(): if not entity.should_poll: diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index aac00b07d7a..eb88a3db369 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -75,7 +75,7 @@ async def async_get_last_state(hass, entity_id: str): if _LOCK not in hass.data: hass.data[_LOCK] = asyncio.Lock(loop=hass.loop) - with (await hass.data[_LOCK]): + async with hass.data[_LOCK]: if DATA_RESTORE_CACHE not in hass.data: await hass.async_add_job( _load_restore_cache, hass) From 3442b6741d969def6b97b99d8bc2c978252efed0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 17 Mar 2018 13:14:53 +0100 Subject: [PATCH 063/924] Fix WUnderground duplicate entity ids (#13285) * Fix WUnderground duplicate entity ids * Entity Namespace --- .../components/sensor/wunderground.py | 25 ++++++++++++++++--- homeassistant/helpers/entity.py | 4 +-- homeassistant/util/__init__.py | 4 +-- tests/components/sensor/test_wunderground.py | 20 +++++++++++++++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 0375bb1344c..7938b17e4d6 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -14,11 +14,12 @@ import async_timeout import voluptuous as vol from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.components import sensor from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) + LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -617,6 +618,8 @@ LANG_CODES = [ 'CY', 'SN', 'JI', 'YI', ] +DEFAULT_ENTITY_NAMESPACE = 'pws' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, @@ -627,22 +630,31 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.longitude, vol.Required(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_ENTITY_NAMESPACE, + default=DEFAULT_ENTITY_NAMESPACE): cv.string, }) +# Stores a list of entity ids we added in order to support multiple stations +# at once. +ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' + @asyncio.coroutine def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" + hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) + latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + namespace = config.get(CONF_ENTITY_NAMESPACE) rest = WUndergroundData( hass, config.get(CONF_API_KEY), config.get(CONF_PWS_ID), config.get(CONF_LANG), latitude, longitude) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable)) + sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) yield from rest.async_update() if not rest.data: @@ -654,7 +666,8 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, class WUndergroundSensor(Entity): """Implementing the WUnderground sensor.""" - def __init__(self, hass: HomeAssistantType, rest, condition): + def __init__(self, hass: HomeAssistantType, rest, condition, + namespace: str): """Initialize the sensor.""" self.rest = rest self._condition = condition @@ -666,8 +679,12 @@ class WUndergroundSensor(Entity): self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") self.rest.request_feature(SENSOR_TYPES[condition].feature) + current_ids = set(hass.states.async_entity_ids(sensor.DOMAIN)) + current_ids |= hass.data[ADDED_ENTITY_IDS_KEY] self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "pws_" + condition, hass=hass) + ENTITY_ID_FORMAT, "{} {}".format(namespace, condition), + current_ids=current_ids) + hass.data[ADDED_ENTITY_IDS_KEY].add(self.entity_id) def _cfg_expand(self, what, default=None): """Parse and return sensor data.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 4efe8d2f6c3..efaefc26184 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,7 +4,7 @@ import logging import functools as ft from timeit import default_timer as timer -from typing import Optional, List +from typing import Optional, List, Iterable from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, @@ -42,7 +42,7 @@ def generate_entity_id(entity_id_format: str, name: Optional[str], @callback def async_generate_entity_id(entity_id_format: str, name: Optional[str], - current_ids: Optional[List[str]] = None, + current_ids: Optional[Iterable[str]] = None, hass: Optional[HomeAssistant] = None) -> str: """Generate a unique entity ID based on given entity IDs or used IDs.""" if current_ids is None: diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index 82ba6a734f8..a8a84c6c880 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -13,7 +13,7 @@ from functools import wraps from types import MappingProxyType from unicodedata import normalize -from typing import Any, Optional, TypeVar, Callable, Sequence, KeysView, Union +from typing import Any, Optional, TypeVar, Callable, KeysView, Union, Iterable from .dt import as_local, utcnow @@ -72,7 +72,7 @@ def convert(value: T, to_type: Callable[[T], U], def ensure_unique_string(preferred_string: str, current_strings: - Union[Sequence[str], KeysView[str]]) -> str: + Union[Iterable[str], KeysView[str]]) -> str: """Return a string that is not present in current_strings. If preferred string exists will append _2, _3, .. diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 27047ba0ad0..65526e2d938 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -143,3 +143,23 @@ def test_invalid_data(hass, aioclient_mock): for condition in VALID_CONFIG['monitored_conditions']: state = hass.states.get('sensor.pws_' + condition) assert state.state == STATE_UNKNOWN + + +async def test_entity_id_with_multiple_stations(hass, aioclient_mock): + """Test not generating duplicate entity ids with multiple stations.""" + aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + + config = [ + VALID_CONFIG, + {**VALID_CONFIG, 'entity_namespace': 'hi'} + ] + await async_setup_component(hass, 'sensor', {'sensor': config}) + await hass.async_block_till_done() + + state = hass.states.get('sensor.pws_weather') + assert state is not None + assert state.state == 'Clear' + + state = hass.states.get('sensor.hi_weather') + assert state is not None + assert state.state == 'Clear' From 8fed405da7ede24a57dee08e189bf90ccf05657e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:37:09 +0100 Subject: [PATCH 064/924] Upgrade aiohttp to 3.0.9 (#13288) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c91d7c84aa9..814a4679e1d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.7 +aiohttp==3.0.9 async_timeout==2.0.0 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index 839987611bc..c53084d4da2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.7 +aiohttp==3.0.9 async_timeout==2.0.0 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index 2e44258c619..816458459f2 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.0.7', + 'aiohttp==3.0.9', 'async_timeout==2.0.0', 'astral==1.6', 'certifi>=2017.4.17', From e01a0f91d6a8704469912c07569d70aab4194051 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:37:53 +0100 Subject: [PATCH 065/924] Upgrade aiohttp_cors to 0.7.0 (#13289) --- homeassistant/components/http/__init__.py | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4d313b5132e..17906157a6e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -4,7 +4,6 @@ This module provides WSGI application to serve the Home Assistant API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/http/ """ - from ipaddress import ip_network import logging import os @@ -32,7 +31,7 @@ from .static import ( from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa from .view import HomeAssistantView # noqa -REQUIREMENTS = ['aiohttp_cors==0.6.0'] +REQUIREMENTS = ['aiohttp_cors==0.7.0'] DOMAIN = 'http' diff --git a/requirements_all.txt b/requirements_all.txt index c53084d4da2..ed3f9fea94e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -71,7 +71,7 @@ aiodns==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.6.0 +aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d41f9589de2..6dee6b37c7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -32,7 +32,7 @@ aioautomatic==0.6.5 # homeassistant.components.emulated_hue # homeassistant.components.http -aiohttp_cors==0.6.0 +aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.2.0 From aec61b7c86f182975a60750359546e68139cb434 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:39:24 +0100 Subject: [PATCH 066/924] Upgrade sqlalchemy to 1.2.5 (#13292) --- homeassistant/components/recorder/__init__.py | 13 +++++-------- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 23c073ff80a..f10e0fc75d7 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.2'] +REQUIREMENTS = ['sqlalchemy==1.2.5'] _LOGGER = logging.getLogger(__name__) @@ -64,16 +64,13 @@ CONNECT_RETRY_WAIT = 3 FILTER_SCHEMA = vol.Schema({ vol.Optional(CONF_EXCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_DOMAINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_DOMAINS): - vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EVENT_TYPES): - vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string]), }), vol.Optional(CONF_INCLUDE, default={}): vol.Schema({ + vol.Optional(CONF_DOMAINS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_ENTITIES): cv.entity_ids, - vol.Optional(CONF_DOMAINS): - vol.All(cv.ensure_list, [cv.string]) }) }) @@ -255,7 +252,7 @@ class Recorder(threading.Thread): self.hass.add_job(register) result = hass_started.result() - # If shutdown happened before HASS finished starting + # If shutdown happened before Home Assistant finished starting if result is shutdown_task: return diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 5d5d61ff822..af9fa233d40 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.2'] +REQUIREMENTS = ['sqlalchemy==1.2.5'] CONF_QUERIES = 'queries' CONF_QUERY = 'query' diff --git a/requirements_all.txt b/requirements_all.txt index ed3f9fea94e..66009b91c0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1165,7 +1165,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.2 +sqlalchemy==1.2.5 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6dee6b37c7d..4f2eafdac56 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -176,7 +176,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.2 +sqlalchemy==1.2.5 # homeassistant.components.statsd statsd==3.2.1 From d35077271db8cbf23f4b2d57b46aad846eb2e968 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:40:03 +0100 Subject: [PATCH 067/924] Upgrade TwitterAPI to 2.5.0 (#13287) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index db7de8e40a0..9489e05cfa5 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.4.10'] +REQUIREMENTS = ['TwitterAPI==2.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 66009b91c0e..5fecb0ce5df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -52,7 +52,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.4.10 +TwitterAPI==2.5.0 # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 82f59ba98488bd6abb6f0003095094106f25405a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:40:31 +0100 Subject: [PATCH 068/924] Upgrade numpy to 1.14.2 (#13291) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 031e0aa42e5..9b4598f3c42 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.0'] +REQUIREMENTS = ['numpy==1.14.2'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index df58e2e9dc4..18e74966a59 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.0'] +REQUIREMENTS = ['numpy==1.14.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5fecb0ce5df..b4078136525 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -528,7 +528,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.0 +numpy==1.14.2 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f2eafdac56..6e58464efe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -99,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.0 +numpy==1.14.2 # homeassistant.components.mqtt # homeassistant.components.shiftr From dbc59ad1a7029be2c243a3375edb326b5ddd5a87 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 17 Mar 2018 17:41:10 +0100 Subject: [PATCH 069/924] Upgrade python-telegram-bot to 10.0.1 (#13294) --- homeassistant/components/telegram_bot/__init__.py | 8 ++++---- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 9e5d4cd9665..3041e7b41e0 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==9.0.0'] +REQUIREMENTS = ['python-telegram-bot==10.0.1'] _LOGGER = logging.getLogger(__name__) @@ -181,7 +181,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, while retry_num < num_retries: req = requests.get(url, **params) if not req.ok: - _LOGGER.warning("Status code %s (retry #%s) loading %s.", + _LOGGER.warning("Status code %s (retry #%s) loading %s", req.status_code, retry_num + 1, url) else: data = io.BytesIO(req.content) @@ -189,10 +189,10 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, data.seek(0) data.name = url return data - _LOGGER.warning("Empty data (retry #%s) in %s).", + _LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) retry_num += 1 - _LOGGER.warning("Can't load photo in %s after %s retries.", + _LOGGER.warning("Can't load photo in %s after %s retries", url, retry_num) elif filepath is not None: if hass.config.is_allowed_path(filepath): diff --git a/requirements_all.txt b/requirements_all.txt index b4078136525..33fb1789b46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -979,7 +979,7 @@ python-synology==0.1.0 python-tado==0.2.2 # homeassistant.components.telegram_bot -python-telegram-bot==9.0.0 +python-telegram-bot==10.0.1 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 181eca4b455f870324d9420ad1db775c6f4f19c4 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 17 Mar 2018 17:43:07 +0100 Subject: [PATCH 070/924] Upgrade python-forecastio to 1.4.0 (#13282) * Upgrade python-forecastio to 1.4.0 * Upgrade python-forecastio to 1.4.0 for sensor as well. --- homeassistant/components/sensor/darksky.py | 2 +- homeassistant/components/weather/darksky.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index e224feb7db7..3049415c754 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-forecastio==1.3.5'] +REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 139f8abfce6..52aa8c46046 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -19,7 +19,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['python-forecastio==1.3.5'] +REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 33fb1789b46..47b38a876f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -918,7 +918,7 @@ python-etherscan-api==0.0.3 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky -python-forecastio==1.3.5 +python-forecastio==1.4.0 # homeassistant.components.gc100 python-gc100==1.0.3a diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e58464efe5..0a7c3b493d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -144,7 +144,7 @@ pynx584==0.4 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky -python-forecastio==1.3.5 +python-forecastio==1.4.0 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From 4d3743f3f79a1127208ce885ae501991ec3e704e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 17 Mar 2018 19:08:52 +0100 Subject: [PATCH 071/924] Delete .gitmodules (#13295) --- .gitmodules | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29bb2d..00000000000 From d042b3d7d109a2a94b45a51d8e0a16e421e23515 Mon Sep 17 00:00:00 2001 From: cburgess Date: Sat, 17 Mar 2018 17:35:16 -0700 Subject: [PATCH 072/924] Update to latest python-nest (#12590) Due to an upstream bug some devices will be assigned a where_id that is not visible in the nest API. As a result we can't get a friendly name for the where_id. A workaround has been released for python-nest in version 3.7.0. Update the home assistant requirements to python-nest==3.7.0 to work around this issue. References: https://nestdevelopers.io/t/missing-where-name-from-some-devices/1202 https://github.com/jkoelker/python-nest/issues/127 https://github.com/jkoelker/python-nest/pull/128 Fixes #12589 Fixes #12950 Fixes #13074 --- homeassistant/components/nest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 37028decf71..e7d2ba90438 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['python-nest==3.1.0'] +REQUIREMENTS = ['python-nest==3.7.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 47b38a876f7..797c251be76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -952,7 +952,7 @@ python-mpd2==0.5.5 python-mystrom==0.3.8 # homeassistant.components.nest -python-nest==3.1.0 +python-nest==3.7.0 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From 8ed302402603c60ff71d5c0baebec269b3769b46 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 18 Mar 2018 01:37:31 +0100 Subject: [PATCH 073/924] Upgrade async_timeout to 2.0.1 (#13290) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 814a4679e1d..e43e1f3dafe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 aiohttp==3.0.9 -async_timeout==2.0.0 +async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 797c251be76..2da1c3a6990 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,7 +7,7 @@ jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 aiohttp==3.0.9 -async_timeout==2.0.0 +async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 attrs==17.4.0 diff --git a/setup.py b/setup.py index 816458459f2..a317aeb18f1 100755 --- a/setup.py +++ b/setup.py @@ -50,7 +50,7 @@ REQUIRES = [ 'voluptuous==0.11.1', 'typing>=3,<4', 'aiohttp==3.0.9', - 'async_timeout==2.0.0', + 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', 'attrs==17.4.0', From b45dad507a0275c91f5142c10ce420545324e965 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sun, 18 Mar 2018 16:57:53 +0100 Subject: [PATCH 074/924] Add initial support fo HomematicIP components (#12761) * Add initial support fo HomematicIP components * Fix module import * Update reqirments file as well * Added HomematicIP files * Update to homematicip * Code cleanup based on highligted issues * Update of reqiremnets file as well * Fix dispatcher usage * Rename homematicip to homematicip_cloud --- .coveragerc | 3 + homeassistant/components/homematicip_cloud.py | 170 ++++++++++++ .../components/sensor/homematicip_cloud.py | 258 ++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 434 insertions(+) create mode 100644 homeassistant/components/homematicip_cloud.py create mode 100644 homeassistant/components/sensor/homematicip_cloud.py diff --git a/.coveragerc b/.coveragerc index 4da5343bf4f..d98048636c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -109,6 +109,9 @@ omit = homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py + homeassistant/components/homematicip_cloud.py + homeassistant/components/*/homematicip_cloud.py + homeassistant/components/ihc/* homeassistant/components/*/ihc.py diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py new file mode 100644 index 00000000000..a89678624eb --- /dev/null +++ b/homeassistant/components/homematicip_cloud.py @@ -0,0 +1,170 @@ +""" +Support for HomematicIP components. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip/ +""" + +import logging +from socket import timeout + +import voluptuous as vol +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import (dispatcher_send, + async_dispatcher_connect) +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['homematicip==0.8'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'homematicip_cloud' + +CONF_NAME = 'name' +CONF_ACCESSPOINT = 'accesspoint' +CONF_AUTHTOKEN = 'authtoken' + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): [vol.Schema({ + vol.Optional(CONF_NAME, default=''): cv.string, + vol.Required(CONF_ACCESSPOINT): cv.string, + vol.Required(CONF_AUTHTOKEN): cv.string, + })], +}, extra=vol.ALLOW_EXTRA) + +EVENT_HOME_CHANGED = 'homematicip_home_changed' +EVENT_DEVICE_CHANGED = 'homematicip_device_changed' +EVENT_GROUP_CHANGED = 'homematicip_group_changed' +EVENT_SECURITY_CHANGED = 'homematicip_security_changed' +EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' + +ATTR_HOME_ID = 'home_id' +ATTR_HOME_LABEL = 'home_label' +ATTR_DEVICE_ID = 'device_id' +ATTR_DEVICE_LABEL = 'device_label' +ATTR_STATUS_UPDATE = 'status_update' +ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_LOW_BATTERY = 'low_battery' +ATTR_SABOTAGE = 'sabotage' +ATTR_RSSI = 'rssi' + + +def setup(hass, config): + """Set up the HomematicIP component.""" + # pylint: disable=import-error, no-name-in-module + from homematicip.home import Home + hass.data.setdefault(DOMAIN, {}) + homes = hass.data[DOMAIN] + accesspoints = config.get(DOMAIN, []) + + def _update_event(events): + """Handle incoming HomeMaticIP events.""" + for event in events: + etype = event['eventType'] + edata = event['data'] + if etype == 'DEVICE_CHANGED': + dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) + elif etype == 'GROUP_CHANGED': + dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) + elif etype == 'HOME_CHANGED': + dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) + elif etype == 'JOURNAL_CHANGED': + dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) + return True + + for device in accesspoints: + name = device.get(CONF_NAME) + accesspoint = device.get(CONF_ACCESSPOINT) + authtoken = device.get(CONF_AUTHTOKEN) + + home = Home() + if name.lower() == 'none': + name = '' + home.label = name + try: + home.set_auth_token(authtoken) + home.init(accesspoint) + if home.get_current_state(): + _LOGGER.info("Connection to HMIP established") + else: + _LOGGER.warning("Connection to HMIP could not be established") + return False + except timeout: + _LOGGER.warning("Connection to HMIP could not be established") + return False + homes[home.id] = home + home.onEvent += _update_event + home.enable_events() + _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) + + for component in ['sensor']: + load_platform(hass, component, DOMAIN, + {'homeid': home.id}, config) + return True + + +class HomematicipGenericDevice(Entity): + """Representation of an HomematicIP generic device.""" + + def __init__(self, hass, home, device, signal=None): + """Initialize the generic device.""" + self.hass = hass + self._home = home + self._device = device + async_dispatcher_connect( + self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + + @callback + def _device_changed(self, deviceid): + """Handle device state changes.""" + if deviceid is None or deviceid == self._device.id: + _LOGGER.debug('Event device %s', self._device.label) + self.async_schedule_update_ha_state() + + def _name(self, addon=''): + """Return the name of the device.""" + name = '' + if self._home.label != '': + name += self._home.label + ' ' + name += self._device.label + if addon != '': + name += ' ' + addon + return name + + @property + def name(self): + """Return the name of the generic device.""" + return self._name() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Device available.""" + return not self._device.unreach + + def _generic_state_attributes(self): + """Return the state attributes of the generic device.""" + laststatus = '' + if self._device.lastStatusUpdate is not None: + laststatus = self._device.lastStatusUpdate.isoformat() + return { + ATTR_HOME_LABEL: self._home.label, + ATTR_DEVICE_LABEL: self._device.label, + ATTR_HOME_ID: self._device.homeId, + ATTR_DEVICE_ID: self._device.id.lower(), + ATTR_STATUS_UPDATE: laststatus, + ATTR_FIRMWARE_STATE: self._device.updateState.lower(), + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } + + @property + def device_state_attributes(self): + """Return the state attributes of the generic device.""" + return self._generic_state_attributes() diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py new file mode 100644 index 00000000000..8f298bbb3f6 --- /dev/null +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -0,0 +1,258 @@ +""" +Support for HomematicIP sensors. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homematicip/ +""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, + ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) +from homeassistant.const import TEMP_CELSIUS, STATE_OK + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematicip_cloud'] + +ATTR_VALVE_STATE = 'valve_state' +ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE_OFFSET = 'temperature_offset' + +HMIP_UPTODATE = 'up_to_date' +HMIP_VALVE_DONE = 'adaption_done' +HMIP_SABOTAGE = 'sabotage' + +STATE_LOW_BATTERY = 'low_battery' +STATE_SABOTAGE = 'sabotage' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the HomematicIP sensors devices.""" + # pylint: disable=import-error, no-name-in-module + from homematicip.device import ( + HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, + TemperatureHumiditySensorDisplay) + + _LOGGER.info('Setting up HomeMaticIP accespoint & generic devices') + homeid = discovery_info['homeid'] + home = hass.data[DOMAIN][homeid] + devices = [HomematicipAccesspoint(hass, home)] + if home.devices is None: + return + for device in home.devices: + devices.append(HomematicipDeviceStatus(hass, home, device)) + if isinstance(device, HeatingThermostat): + devices.append(HomematicipHeatingThermostat(hass, home, device)) + if isinstance(device, TemperatureHumiditySensorWithoutDisplay): + devices.append(HomematicipSensorThermometer(hass, home, device)) + devices.append(HomematicipSensorHumidity(hass, home, device)) + if isinstance(device, TemperatureHumiditySensorDisplay): + devices.append(HomematicipSensorThermometer(hass, home, device)) + devices.append(HomematicipSensorHumidity(hass, home, device)) + add_devices(devices) + + +class HomematicipAccesspoint(Entity): + """Representation of an HomeMaticIP access point.""" + + def __init__(self, hass, home): + """Initialize the access point sensor.""" + self.hass = hass + self._home = home + dispatcher_connect( + self.hass, EVENT_HOME_CHANGED, self._home_changed) + _LOGGER.debug('Setting up access point %s', home.label) + + @callback + def _home_changed(self, deviceid): + """Handle device state changes.""" + if deviceid is None or deviceid == self._home.id: + _LOGGER.debug('Event access point %s', self._home.label) + self.async_schedule_update_ha_state() + + @property + def name(self): + """Return the name of the access point device.""" + if self._home.label == '': + return 'Access Point Status' + return '{} Access Point Status'.format(self._home.label) + + @property + def icon(self): + """Return the icon of the access point device.""" + return 'mdi:access-point-network' + + @property + def state(self): + """Return the state of the access point.""" + return self._home.dutyCycle + + @property + def available(self): + """Device available.""" + return self._home.connected + + @property + def device_state_attributes(self): + """Return the state attributes of the access point.""" + return { + ATTR_HOME_LABEL: self._home.label, + ATTR_HOME_ID: self._home.id, + } + + +class HomematicipDeviceStatus(HomematicipGenericDevice): + """Representation of an HomematicIP device status.""" + + def __init__(self, hass, home, device, signal=None): + """Initialize the device.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up sensor device status: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Status') + + @property + def icon(self): + """Return the icon of the status device.""" + if (hasattr(self._device, 'sabotage') and + self._device.sabotage == HMIP_SABOTAGE): + return 'mdi:alert' + elif self._device.lowBat: + return 'mdi:battery-outline' + elif self._device.updateState.lower() != HMIP_UPTODATE: + return 'mdi:refresh' + return 'mdi:check' + + @property + def state(self): + """Return the state of the generic device.""" + if (hasattr(self._device, 'sabotage') and + self._device.sabotage == HMIP_SABOTAGE): + return STATE_SABOTAGE + elif self._device.lowBat: + return STATE_LOW_BATTERY + elif self._device.updateState.lower() != HMIP_UPTODATE: + return self._device.updateState.lower() + return STATE_OK + + +class HomematicipHeatingThermostat(HomematicipGenericDevice): + """MomematicIP heating thermostat representation.""" + + def __init__(self, hass, home, device): + """"Initialize heating thermostat.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + + @property + def icon(self): + """Return the icon.""" + if self._device.valveState.lower() != HMIP_VALVE_DONE: + return 'mdi:alert' + return 'mdi:radiator' + + @property + def state(self): + """Return the state of the radiator valve.""" + if self._device.valveState.lower() != HMIP_VALVE_DONE: + return self._device.valveState.lower() + return round(self._device.valvePosition*100) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_VALVE_STATE: self._device.valveState.lower(), + ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue + } + + +class HomematicipSensorHumidity(HomematicipGenericDevice): + """MomematicIP thermometer device.""" + + def __init__(self, hass, home, device): + """"Initialize the thermometer device.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up humidity device: %s', + device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Humidity') + + @property + def icon(self): + """Return the icon.""" + return 'mdi:water' + + @property + def state(self): + """Return the state.""" + return self._device.humidity + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return '%' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } + + +class HomematicipSensorThermometer(HomematicipGenericDevice): + """MomematicIP thermometer device.""" + + def __init__(self, hass, home, device): + """"Initialize the thermometer device.""" + super().__init__(hass, home, device) + _LOGGER.debug('Setting up thermometer device: %s', device.label) + + @property + def name(self): + """Return the name of the device.""" + return self._name('Temperature') + + @property + def icon(self): + """Return the icon.""" + return 'mdi:thermometer' + + @property + def state(self): + """Return the state.""" + return self._device.actualTemperature + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_RSSI: self._device.rssiDeviceValue, + } diff --git a/requirements_all.txt b/requirements_all.txt index 2da1c3a6990..fbddbe9c448 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,6 +358,9 @@ holidays==0.9.4 # homeassistant.components.frontend home-assistant-frontend==20180316.0 +# homeassistant.components.homematicip_cloud +homematicip==0.8 + # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a From 1e17b2fd63c9bcc3355015afb7127a59b1692c7b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 18 Mar 2018 15:58:52 +0000 Subject: [PATCH 075/924] Added Time based SMA to Filter Sensor (#13104) * Added Time based SMA * move "now" to _filter_state() * Addressed comments * fix long line * type and name * # pylint: disable=redefined-builtin * added test --- homeassistant/components/sensor/filter.py | 66 ++++++++++++++++++++++- tests/components/sensor/test_filter.py | 18 ++++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index cde50699b29..aad7fec26a0 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -20,12 +20,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) FILTER_NAME_LOWPASS = 'lowpass' FILTER_NAME_OUTLIER = 'outlier' FILTER_NAME_THROTTLE = 'throttle' +FILTER_NAME_TIME_SMA = 'time_simple_moving_average' FILTERS = Registry() CONF_FILTERS = 'filters' @@ -34,6 +36,9 @@ CONF_FILTER_WINDOW_SIZE = 'window_size' CONF_FILTER_PRECISION = 'precision' CONF_FILTER_RADIUS = 'radius' CONF_FILTER_TIME_CONSTANT = 'time_constant' +CONF_TIME_SMA_TYPE = 'type' + +TIME_SMA_LAST = 'last' DEFAULT_WINDOW_SIZE = 1 DEFAULT_PRECISION = 2 @@ -44,24 +49,37 @@ NAME_TEMPLATE = "{} filter" ICON = 'mdi:chart-line-variant' FILTER_SCHEMA = vol.Schema({ - vol.Optional(CONF_FILTER_WINDOW_SIZE, - default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), }) +# pylint: disable=redefined-builtin FILTER_OUTLIER_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_OUTLIER, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_RADIUS, default=DEFAULT_FILTER_RADIUS): vol.Coerce(float), }) FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_LOWPASS, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), vol.Optional(CONF_FILTER_TIME_CONSTANT, default=DEFAULT_FILTER_TIME_CONSTANT): vol.Coerce(int), }) +FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ + vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, + vol.Optional(CONF_TIME_SMA_TYPE, + default=TIME_SMA_LAST): vol.In( + [None, TIME_SMA_LAST]), + + vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, + cv.positive_timedelta) +}) + FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, }) @@ -72,6 +90,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FILTERS): vol.All(cv.ensure_list, [vol.Any(FILTER_OUTLIER_SCHEMA, FILTER_LOWPASS_SCHEMA, + FILTER_TIME_SMA_SCHEMA, FILTER_THROTTLE_SCHEMA)]) }) @@ -277,6 +296,49 @@ class LowPassFilter(Filter): return filtered +@FILTERS.register(FILTER_NAME_TIME_SMA) +class TimeSMAFilter(Filter): + """Simple Moving Average (SMA) Filter. + + The window_size is determined by time, and SMA is time weighted. + + Args: + variant (enum): type of argorithm used to connect discrete values + """ + + def __init__(self, window_size, precision, entity, type): + """Initialize Filter.""" + super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) + self._time_window = int(window_size.total_seconds()) + self.last_leak = None + self.queue = deque() + + def _leak(self, now): + """Remove timeouted elements.""" + while self.queue: + timestamp, _ = self.queue[0] + if timestamp + self._time_window <= now: + self.last_leak = self.queue.popleft() + else: + return + + def _filter_state(self, new_state): + now = int(dt_util.utcnow().timestamp()) + + self._leak(now) + self.queue.append((now, float(new_state))) + moving_sum = 0 + start = now - self._time_window + _, prev_val = self.last_leak or (0, float(new_state)) + + for timestamp, val in self.queue: + moving_sum += (timestamp-start)*prev_val + start, prev_val = timestamp, val + moving_sum += (now-start)*prev_val + + return moving_sum/self._time_window + + @FILTERS.register(FILTER_NAME_THROTTLE) class ThrottleFilter(Filter): """Throttle Filter. diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index dd1112d65f8..0d4082731ab 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -1,8 +1,11 @@ """The test for the data filter sensor platform.""" +from datetime import timedelta import unittest +from unittest.mock import patch from homeassistant.components.sensor.filter import ( - LowPassFilter, OutlierFilter, ThrottleFilter) + LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) +import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, assert_setup_component @@ -90,3 +93,16 @@ class TestFilterSensor(unittest.TestCase): if not filt.skip_processing: filtered.append(new_state) self.assertEqual([20, 21], filtered) + + def test_time_sma(self): + """Test if time_sma filter works.""" + filt = TimeSMAFilter(window_size=timedelta(minutes=2), + precision=2, + entity=None, + type='last') + past = dt_util.utcnow() - timedelta(minutes=5) + for state in self.values: + with patch('homeassistant.util.dt.utcnow', return_value=past): + filtered = filt.filter_state(state) + past += timedelta(minutes=1) + self.assertEqual(21.5, filtered) From 022d8fb816bde0676f0cb9cbda6cd8efe817064b Mon Sep 17 00:00:00 2001 From: maxclaey Date: Sun, 18 Mar 2018 17:00:08 +0100 Subject: [PATCH 076/924] Support for security systems controlled by IFTTT (#12975) * Add IFTTT alarm control panel * Update .coveragerc * Add support for code * Bugfix * Fix logging problem * Pin requirements * Update requirements_all.txt * Fix lint errors * Use ifttt component as a dependency instead of interacting with ifttt manually Take into account review comments * No default value for code * Take into account review comments * Provide a "push_alarm_state" service to change the state from IFTTT * Add service description * Fix @balloob review comments. Thanks! * Fix service description name --- .coveragerc | 1 + .../components/alarm_control_panel/ifttt.py | 146 ++++++++++++++++++ .../alarm_control_panel/services.yaml | 10 ++ 3 files changed, 157 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/ifttt.py diff --git a/.coveragerc b/.coveragerc index d98048636c3..5e1bbe67144 100644 --- a/.coveragerc +++ b/.coveragerc @@ -312,6 +312,7 @@ omit = homeassistant/components/alarm_control_panel/canary.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/ialarm.py + homeassistant/components/alarm_control_panel/ifttt.py homeassistant/components/alarm_control_panel/manual_mqtt.py homeassistant/components/alarm_control_panel/nx584.py homeassistant/components/alarm_control_panel/simplisafe.py diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py new file mode 100644 index 00000000000..eb1a8f8ed7d --- /dev/null +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -0,0 +1,146 @@ +""" +Interfaces with alarm control panels that have to be controlled through IFTTT. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/alarm_control_panel.ifttt/ +""" +import logging + +import voluptuous as vol + +import homeassistant.components.alarm_control_panel as alarm +from homeassistant.components.alarm_control_panel import ( + DOMAIN, PLATFORM_SCHEMA) +from homeassistant.components.ifttt import ( + ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['ifttt'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_STATES = [ + STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME] + +DATA_IFTTT_ALARM = 'ifttt_alarm' +DEFAULT_NAME = "Home" + +EVENT_ALARM_ARM_AWAY = "alarm_arm_away" +EVENT_ALARM_ARM_HOME = "alarm_arm_home" +EVENT_ALARM_ARM_NIGHT = "alarm_arm_night" +EVENT_ALARM_DISARM = "alarm_disarm" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_CODE): cv.string, +}) + +SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" + +PUSH_ALARM_STATE_SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_STATE): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a control panel managed through IFTTT.""" + if DATA_IFTTT_ALARM not in hass.data: + hass.data[DATA_IFTTT_ALARM] = [] + + name = config.get(CONF_NAME) + code = config.get(CONF_CODE) + + alarmpanel = IFTTTAlarmPanel(name, code) + hass.data[DATA_IFTTT_ALARM].append(alarmpanel) + add_devices([alarmpanel]) + + async def push_state_update(service): + """Set the service state as device state attribute.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + state = service.data.get(ATTR_STATE) + devices = hass.data[DATA_IFTTT_ALARM] + if entity_ids: + devices = [d for d in devices if d.entity_id in entity_ids] + + for device in devices: + device.push_alarm_state(state) + device.async_schedule_update_ha_state() + + hass.services.register(DOMAIN, SERVICE_PUSH_ALARM_STATE, push_state_update, + schema=PUSH_ALARM_STATE_SERVICE_SCHEMA) + + +class IFTTTAlarmPanel(alarm.AlarmControlPanel): + """Representation of an alarm control panel controlled throught IFTTT.""" + + def __init__(self, name, code): + """Initialize the alarm control panel.""" + self._name = name + self._code = code + self._state = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def assumed_state(self): + """Notify that this platform return an assumed state.""" + return True + + @property + def code_format(self): + """Return one or more characters.""" + return None if self._code is None else '.+' + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_DISARM) + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_ARM_AWAY) + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_ARM_HOME) + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + if not self._check_code(code): + return + self.set_alarm_state(EVENT_ALARM_ARM_NIGHT) + + def set_alarm_state(self, event): + """Call the IFTTT trigger service to change the alarm state.""" + data = {ATTR_EVENT: event} + + self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) + _LOGGER.debug("Called IFTTT component to trigger event %s", event) + + def push_alarm_state(self, value): + """Push the alarm state to the given value.""" + if value in ALLOWED_STATES: + _LOGGER.debug("Pushed the alarm state to %s", value) + self._state = value + + def _check_code(self, code): + return self._code is None or self._code == code diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 72784c8178c..391de2033c7 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -69,3 +69,13 @@ alarmdecoder_alarm_toggle_chime: code: description: A required code to toggle the alarm control panel chime with. example: 1234 + +ifttt_push_alarm_state: + description: Update the alarm state to the specified value. + fields: + entity_id: + description: Name of the alarm control panel which state has to be updated. + example: 'alarm_control_panel.downstairs' + state: + description: The state to which the alarm control panel has to be set. + example: 'armed_night' From 1dcc51cbdfc4e262841b0fe734ca69ea6b4f960d Mon Sep 17 00:00:00 2001 From: uchagani Date: Sun, 18 Mar 2018 12:02:07 -0400 Subject: [PATCH 077/924] Add ecobee fan mode (#12732) * add ability to set fan on * add tests and change "not on" status to "auto" * hound fix * more hounds * I don't understand new lines * fix linting errors * more linting fixes * change method signature * lint fixes * hopefully last lint fix * correct temp ranges according to ecobee API docs * update dependency to latest version * update tests with values from new temp logic * fix linting issue * more linting fixes * add SUPPORT_FAN_MODE to capabilities * add fan_list to attributes. restore current fan state to OFF if fan is not running. change target high/low temps from null to target temp when not in auto mode. change target temp from null to high/low temp when in auto mode change mode attribute to climate_mode for consistency with other lists. * remove unused import * simplify logic * lint fixes * revert change for target temps --- homeassistant/components/climate/ecobee.py | 80 +++++++++++++++------- homeassistant/components/ecobee.py | 2 +- requirements_all.txt | 2 +- tests/components/climate/test_ecobee.py | 48 ++++++++----- 4 files changed, 88 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/climate/ecobee.py b/homeassistant/components/climate/ecobee.py index 6a4253ceca7..e64c2d5000e 100644 --- a/homeassistant/components/climate/ecobee.py +++ b/homeassistant/components/climate/ecobee.py @@ -14,10 +14,10 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AWAY_MODE, SUPPORT_HOLD_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_TARGET_HUMIDITY_HIGH, - SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_FAN_MODE, + SUPPORT_TARGET_TEMPERATURE_LOW, STATE_OFF) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_OFF, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} @@ -50,7 +50,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE | SUPPORT_HOLD_MODE | SUPPORT_OPERATION_MODE | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_HIGH | - SUPPORT_TARGET_TEMPERATURE_LOW) + SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_FAN_MODE) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -122,6 +122,7 @@ class Thermostat(ClimateDevice): self._climate_list = self.climate_list self._operation_list = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off'] + self._fan_list = ['auto', 'on'] self.update_without_throttle = False def update(self): @@ -180,24 +181,29 @@ class Thermostat(ClimateDevice): return self.thermostat['runtime']['desiredCool'] / 10.0 return None - @property - def desired_fan_mode(self): - """Return the desired fan mode of operation.""" - return self.thermostat['runtime']['desiredFanMode'] - @property def fan(self): - """Return the current fan state.""" + """Return the current fan status.""" if 'fan' in self.thermostat['equipmentStatus']: return STATE_ON return STATE_OFF + @property + def current_fan_mode(self): + """Return the fan setting.""" + return self.thermostat['runtime']['desiredFanMode'] + @property def current_hold_mode(self): """Return current hold mode.""" mode = self._current_hold_mode return None if mode == AWAY_MODE else mode + @property + def fan_list(self): + """Return the available fan modes.""" + return self._fan_list + @property def _current_hold_mode(self): events = self.thermostat['events'] @@ -206,7 +212,7 @@ class Thermostat(ClimateDevice): if event['type'] == 'hold': if event['holdClimateRef'] == 'away': if int(event['endDate'][0:4]) - \ - int(event['startDate'][0:4]) <= 1: + int(event['startDate'][0:4]) <= 1: # A temporary hold from away climate is a hold return 'away' # A permanent hold from away climate @@ -228,7 +234,7 @@ class Thermostat(ClimateDevice): def current_operation(self): """Return current operation.""" if self.operation_mode == 'auxHeatOnly' or \ - self.operation_mode == 'heatPump': + self.operation_mode == 'heatPump': return STATE_HEAT return self.operation_mode @@ -271,10 +277,11 @@ class Thermostat(ClimateDevice): operation = STATE_HEAT else: operation = status + return { "actual_humidity": self.thermostat['runtime']['actualHumidity'], "fan": self.fan, - "mode": self.mode, + "climate_mode": self.mode, "operation": operation, "climate_list": self.climate_list, "fan_min_on_time": self.fan_min_on_time @@ -342,25 +349,46 @@ class Thermostat(ClimateDevice): cool_temp_setpoint, heat_temp_setpoint, self.hold_preference()) _LOGGER.debug("Setting ecobee hold_temp to: heat=%s, is=%s, " - "cool=%s, is=%s", heat_temp, isinstance( - heat_temp, (int, float)), cool_temp, + "cool=%s, is=%s", heat_temp, + isinstance(heat_temp, (int, float)), cool_temp, isinstance(cool_temp, (int, float))) self.update_without_throttle = True + def set_fan_mode(self, fan_mode): + """Set the fan mode. Valid values are "on" or "auto".""" + if (fan_mode.lower() != STATE_ON) and (fan_mode.lower() != STATE_AUTO): + error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" + _LOGGER.error(error) + return + + cool_temp = self.thermostat['runtime']['desiredCool'] / 10.0 + heat_temp = self.thermostat['runtime']['desiredHeat'] / 10.0 + self.data.ecobee.set_fan_mode(self.thermostat_index, fan_mode, + cool_temp, heat_temp, + self.hold_preference()) + + _LOGGER.info("Setting fan mode to: %s", fan_mode) + def set_temp_hold(self, temp): - """Set temperature hold in modes other than auto.""" - # Set arbitrary range when not in auto mode - if self.current_operation == STATE_HEAT: + """Set temperature hold in modes other than auto. + + Ecobee API: It is good practice to set the heat and cool hold + temperatures to be the same, if the thermostat is in either heat, cool, + auxHeatOnly, or off mode. If the thermostat is in auto mode, an + additional rule is required. The cool hold temperature must be greater + than the heat hold temperature by at least the amount in the + heatCoolMinDelta property. + https://www.ecobee.com/home/developer/api/examples/ex5.shtml + """ + if self.current_operation == STATE_HEAT or self.current_operation == \ + STATE_COOL: heat_temp = temp - cool_temp = temp + 20 - elif self.current_operation == STATE_COOL: - heat_temp = temp - 20 cool_temp = temp else: - # In auto mode set temperature between - heat_temp = temp - 10 - cool_temp = temp + 10 + delta = self.thermostat['settings']['heatCoolMinDelta'] / 10 + heat_temp = temp - delta + cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) def set_temperature(self, **kwargs): @@ -369,8 +397,8 @@ class Thermostat(ClimateDevice): high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) temp = kwargs.get(ATTR_TEMPERATURE) - if self.current_operation == STATE_AUTO and (low_temp is not None or - high_temp is not None): + if self.current_operation == STATE_AUTO and \ + (low_temp is not None or high_temp is not None): self.set_auto_temp_hold(low_temp, high_temp) elif temp is not None: self.set_temp_hold(temp) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index 132e230c137..d1503dc74dc 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.util import Throttle from homeassistant.util.json import save_json -REQUIREMENTS = ['python-ecobee-api==0.0.15'] +REQUIREMENTS = ['python-ecobee-api==0.0.17'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fbddbe9c448..37c7150c146 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -911,7 +911,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.15 +python-ecobee-api==0.0.17 # homeassistant.components.climate.eq3btsmart # python-eq3bt==0.1.9 diff --git a/tests/components/climate/test_ecobee.py b/tests/components/climate/test_ecobee.py index 4732376fceb..eb843d8eb34 100644 --- a/tests/components/climate/test_ecobee.py +++ b/tests/components/climate/test_ecobee.py @@ -3,6 +3,7 @@ import unittest from unittest import mock import homeassistant.const as const import homeassistant.components.climate.ecobee as ecobee +from homeassistant.components.climate import STATE_OFF class TestEcobee(unittest.TestCase): @@ -23,6 +24,7 @@ class TestEcobee(unittest.TestCase): 'desiredFanMode': 'on'}, 'settings': {'hvacMode': 'auto', 'fanMinOnTime': 10, + 'heatCoolMinDelta': 50, 'holdAction': 'nextTransition'}, 'equipmentStatus': 'fan', 'events': [{'name': 'Event1', @@ -81,17 +83,17 @@ class TestEcobee(unittest.TestCase): def test_desired_fan_mode(self): """Test desired fan mode property.""" - self.assertEqual('on', self.thermostat.desired_fan_mode) + self.assertEqual('on', self.thermostat.current_fan_mode) self.ecobee['runtime']['desiredFanMode'] = 'auto' - self.assertEqual('auto', self.thermostat.desired_fan_mode) + self.assertEqual('auto', self.thermostat.current_fan_mode) def test_fan(self): """Test fan property.""" self.assertEqual(const.STATE_ON, self.thermostat.fan) self.ecobee['equipmentStatus'] = '' - self.assertEqual(const.STATE_OFF, self.thermostat.fan) + self.assertEqual(STATE_OFF, self.thermostat.fan) self.ecobee['equipmentStatus'] = 'heatPump, heatPump2' - self.assertEqual(const.STATE_OFF, self.thermostat.fan) + self.assertEqual(STATE_OFF, self.thermostat.fan) def test_current_hold_mode_away_temporary(self): """Test current hold mode when away.""" @@ -180,7 +182,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'heat'}, self.thermostat.device_state_attributes) @@ -189,7 +191,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'heat'}, self.thermostat.device_state_attributes) self.ecobee['equipmentStatus'] = 'compCool1' @@ -197,7 +199,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'cool'}, self.thermostat.device_state_attributes) self.ecobee['equipmentStatus'] = '' @@ -205,7 +207,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'idle'}, self.thermostat.device_state_attributes) @@ -214,7 +216,7 @@ class TestEcobee(unittest.TestCase): 'climate_list': ['Climate1', 'Climate2'], 'fan': 'off', 'fan_min_on_time': 10, - 'mode': 'Climate1', + 'climate_mode': 'Climate1', 'operation': 'Unknown'}, self.thermostat.device_state_attributes) @@ -321,7 +323,7 @@ class TestEcobee(unittest.TestCase): self.assertFalse(self.data.ecobee.delete_vacation.called) self.assertFalse(self.data.ecobee.resume_program.called) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40.0, 20.0, 'nextTransition')]) + [mock.call(1, 35.0, 25.0, 'nextTransition')]) self.assertFalse(self.data.ecobee.set_climate_hold.called) def test_set_auto_temp_hold(self): @@ -337,21 +339,21 @@ class TestEcobee(unittest.TestCase): self.data.reset_mock() self.thermostat.set_temp_hold(30.0) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40.0, 20.0, 'nextTransition')]) + [mock.call(1, 35.0, 25.0, 'nextTransition')]) # Heat mode self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'heat' self.thermostat.set_temp_hold(30) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 50, 30, 'nextTransition')]) + [mock.call(1, 30, 30, 'nextTransition')]) # Cool mode self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'cool' self.thermostat.set_temp_hold(30) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 10, 'nextTransition')]) + [mock.call(1, 30, 30, 'nextTransition')]) def test_set_temperature(self): """Test set temperature.""" @@ -366,21 +368,21 @@ class TestEcobee(unittest.TestCase): self.data.reset_mock() self.thermostat.set_temperature(temperature=20) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 30, 10, 'nextTransition')]) + [mock.call(1, 25, 15, 'nextTransition')]) # Cool -> Hold self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'cool' self.thermostat.set_temperature(temperature=20.5) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20.5, 0.5, 'nextTransition')]) + [mock.call(1, 20.5, 20.5, 'nextTransition')]) # Heat -> Hold self.data.reset_mock() self.ecobee['settings']['hvacMode'] = 'heat' self.thermostat.set_temperature(temperature=20) self.data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 40, 20, 'nextTransition')]) + [mock.call(1, 20, 20, 'nextTransition')]) # Heat -> Auto self.data.reset_mock() @@ -450,3 +452,17 @@ class TestEcobee(unittest.TestCase): """Test climate list property.""" self.assertEqual(['Climate1', 'Climate2'], self.thermostat.climate_list) + + def test_set_fan_mode_on(self): + """Test set fan mode to on.""" + self.data.reset_mock() + self.thermostat.set_fan_mode('on') + self.data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, 'on', 20, 40, 'nextTransition')]) + + def test_set_fan_mode_auto(self): + """Test set fan mode to auto.""" + self.data.reset_mock() + self.thermostat.set_fan_mode('auto') + self.data.ecobee.set_fan_mode.assert_has_calls( + [mock.call(1, 'auto', 20, 40, 'nextTransition')]) From 9cb3c9034f5deeb333e6e68394a331f6b9896c4e Mon Sep 17 00:00:00 2001 From: Igor Bernstein Date: Sun, 18 Mar 2018 12:17:56 -0400 Subject: [PATCH 078/924] Zigbee fan (#12289) * wip: initial control * fix initial state * cosmetic cleanup * doc typo * lint * fixes * fix unknown bug * Lint --- homeassistant/components/fan/zha.py | 114 ++++++++++++++++++++++++++ homeassistant/components/zha/const.py | 1 + 2 files changed, 115 insertions(+) create mode 100644 homeassistant/components/fan/zha.py diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py new file mode 100644 index 00000000000..3288a788e1f --- /dev/null +++ b/homeassistant/components/fan/zha.py @@ -0,0 +1,114 @@ +""" +Fans on Zigbee Home Automation networks. + +For more details on this platform, please refer to the documentation +at https://home-assistant.io/components/fan.zha/ +""" +import asyncio +import logging +from homeassistant.components import zha +from homeassistant.components.fan import ( + DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + SUPPORT_SET_SPEED) +from homeassistant.const import STATE_UNKNOWN + +DEPENDENCIES = ['zha'] + +_LOGGER = logging.getLogger(__name__) + +# Additional speeds in zigbee's ZCL +# Spec is unclear as to what this value means. On King Of Fans HBUniversal +# receiver, this means Very High. +SPEED_ON = 'on' +# The fan speed is self-regulated +SPEED_AUTO = 'auto' +# When the heated/cooled space is occupied, the fan is always on +SPEED_SMART = 'smart' + +SPEED_LIST = [ + SPEED_OFF, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_HIGH, + SPEED_ON, + SPEED_AUTO, + SPEED_SMART +] + +VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} +SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Zigbee Home Automation fans.""" + discovery_info = zha.get_discovery_info(hass, discovery_info) + if discovery_info is None: + return + + async_add_devices([ZhaFan(**discovery_info)], update_before_add=True) + + +class ZhaFan(zha.Entity, FanEntity): + """Representation of a ZHA fan.""" + + _domain = DOMAIN + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return SPEED_LIST + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._state + + @property + def is_on(self) -> bool: + """Return true if entity is on.""" + if self._state == STATE_UNKNOWN: + return False + return self._state != SPEED_OFF + + @asyncio.coroutine + def async_turn_on(self, speed: str = None, **kwargs) -> None: + """Turn the entity on.""" + if speed is None: + speed = SPEED_MEDIUM + + yield from self.async_set_speed(speed) + + @asyncio.coroutine + def async_turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + yield from self.async_set_speed(SPEED_OFF) + + @asyncio.coroutine + def async_set_speed(self: FanEntity, speed: str) -> None: + """Set the speed of the fan.""" + yield from self._endpoint.fan.write_attributes({ + 'fan_mode': SPEED_TO_VALUE[speed]}) + + self._state = speed + self.async_schedule_update_ha_state() + + @asyncio.coroutine + def async_update(self): + """Retrieve latest state.""" + result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) + new_value = result.get('fan_mode', None) + self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index deaa1257396..4fe3581d5b2 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -36,6 +36,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', + zcl.clusters.hvac.Fan: 'fan', }) # A map of hass components to all Zigbee clusters it could use From 437ffc8337d778a1815424f57e6e8dc69e9bab76 Mon Sep 17 00:00:00 2001 From: Kevin Raddatz Date: Sun, 18 Mar 2018 17:25:25 +0100 Subject: [PATCH 079/924] Update plex.py (#12157) * Update plex.py show information about media depending if it is a movie or an episode set time_between_scans to 10 s to match with plex media_player component * Update plex.py lint * Update plex.py linting * Update plex.py linting * Update plex.py linting * Update plex.py added catch for tracks and everything else if no release year is given, instead of () it now show nothing * Update plex.py Remove the album year to match with the Plex UI * Update README.rst * Update README.rst * Update plex.py reformat code to make it more readable recorded tv shows might not have episode numbers assigned -> check before adding to title * Update plex.py cleanup excessive whitespace * Update plex.py cleanup excessive whitespace --- homeassistant/components/sensor/plex.py | 40 ++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 87af51d2bbd..505983cb3a7 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -115,9 +115,41 @@ class PlexSensor(Entity): sessions = self._server.sessions() now_playing = [] for sess in sessions: - user = sess.usernames[0] if sess.usernames is not None else "" - title = sess.title if sess.title is not None else "" - year = sess.year if sess.year is not None else "" - now_playing.append((user, "{0} ({1})".format(title, year))) + user = sess.usernames[0] + device = sess.players[0].title + now_playing_user = "{0} - {1}".format(user, device) + now_playing_title = "" + + if sess.TYPE == 'episode': + # example: + # "Supernatural (2005) - S01 · E13 - Route 666" + season_title = sess.grandparentTitle + if sess.show().year is not None: + season_title += " ({0})".format(sess.show().year) + season_episode = "S{0}".format(sess.parentIndex) + if sess.index is not None: + season_episode += " · E{1}".format(sess.index) + episode_title = sess.title + now_playing_title = "{0} - {1} - {2}".format(season_title, + season_episode, + episode_title) + elif sess.TYPE == 'track': + # example: + # "Billy Talent - Afraid of Heights - Afraid of Heights" + track_artist = sess.grandparentTitle + track_album = sess.parentTitle + track_title = sess.title + now_playing_title = "{0} - {1} - {2}".format(track_artist, + track_album, + track_title) + else: + # example: + # "picture_of_last_summer_camp (2015)" + # "The Incredible Hulk (2008)" + now_playing_title = sess.title + if sess.year is not None: + now_playing_title += " ({0})".format(sess.year) + + now_playing.append((now_playing_user, now_playing_title)) self._state = len(sessions) self._now_playing = now_playing From 1cbf9792d768255fb7a5e136de9c5b23bf2048d2 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Sun, 18 Mar 2018 17:26:07 +0100 Subject: [PATCH 080/924] Support MQTT Lock discovery (#13303) --- homeassistant/components/lock/mqtt.py | 3 +++ homeassistant/components/mqtt/discovery.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index e73e35a9900..d8af22cd5c3 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -44,6 +44,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT lock.""" + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) + value_template = config.get(CONF_VALUE_TEMPLATE) if value_template is not None: value_template.hass = hass diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d0164706626..3263521f3f1 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -20,13 +20,14 @@ TOPIC_MATCHER = re.compile( r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[a-zA-Z0-9_-]+)/config') SUPPORTED_COMPONENTS = [ - 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch'] + 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], 'cover': ['mqtt'], 'fan': ['mqtt'], 'light': ['mqtt', 'mqtt_json', 'mqtt_template'], + 'lock': ['mqtt'], 'sensor': ['mqtt'], 'switch': ['mqtt'], } From 6b059489a62ac4c2008e7f79b1e65f1746c1f69d Mon Sep 17 00:00:00 2001 From: nielstron Date: Sun, 18 Mar 2018 17:26:33 +0100 Subject: [PATCH 081/924] Adding a discoverable Samsung Syncthru Printer sensor platform (#13134) * Added a simple component to support the BLNET Adds a component based on pyblnet, that hooks up the blnet to home assistant * Adds support for custimzation of blnet sensor devices * Setting up blnet as a platfrom * Updated use of state_attributes Now the friendly_name (and for digital values the mode) is set in the state_attributes whereas the name is defined as "blnet_(analog|digital)_{sensornumber}" so you can reliably add them to groups. * Added support for the SyncThru printer web service * Added pysyncthru to the requirements * Changed to Dependencis, import inside setup_platform * Switch back to REQUIREMENTS Looks like DEPENDENCIES is not meant for python packages but for other HA components * Fixed access to _attributes * Final fix * Several Bugfixes When the printer goes offline, the last state will be kept. Also now checks if the printer is reachable upon setup * Register syncthru as discoverable * Included possible conditions to monitor * Split the printer sensor in several seperate sensor entities * Fixed bug at sensor creation, pep8 conform * Bugfix * Bugfix * Removed Blnet components * Fixed unused import * Renamed discoverable to samsung_printer * Removed unused Attribute _friendly_name * Inserted missing space * Pinned requirements and added to coveragerc * Reduced redundancy by condensing into multiple sub-classes * Fixed indentation * Fixed super constructor calls * Fixed super constructor calls * Fixed format * Resolving style issues and using name instead of friendly_name * Pinned pysyncthru in requirements_all, having trouble with friendly_name * Iterating over dictionary instead of dict.keys() * ran gen_reqirements_all.py * Fixed flake 8 issues * Added a simple component to support the BLNET Adds a component based on pyblnet, that hooks up the blnet to home assistant * Implemented requested changes * raised dependecies to pysyncthru version that has timeouts * Raised required version for full timeout support * Adds support for custimzation of blnet sensor devices * Setting up blnet as a platfrom * Updated use of state_attributes Now the friendly_name (and for digital values the mode) is set in the state_attributes whereas the name is defined as "blnet_(analog|digital)_{sensornumber}" so you can reliably add them to groups. * Added support for the SyncThru printer web service * Added pysyncthru to the requirements * Removed Blnet components * Pinned requirements and added to coveragerc * Fixed indentation * Fixed format * Pinned pysyncthru in requirements_all, having trouble with friendly_name * ran gen_reqirements_all.py * Updated requirements_all * Renamed sensor objects, removed passing of hass entity * Removed merge artifacts * Reset syncthru to newest state * Updated requirements_all * switched to using the newest version of pysyncthru * Sorted coveragerc --- .coveragerc | 1 + homeassistant/components/discovery.py | 2 + homeassistant/components/sensor/syncthru.py | 233 ++++++++++++++++++++ requirements_all.txt | 3 + 4 files changed, 239 insertions(+) create mode 100644 homeassistant/components/sensor/syncthru.py diff --git a/.coveragerc b/.coveragerc index 5e1bbe67144..40fccd5e921 100644 --- a/.coveragerc +++ b/.coveragerc @@ -655,6 +655,7 @@ omit = homeassistant/components/sensor/supervisord.py homeassistant/components/sensor/swiss_hydrological_data.py homeassistant/components/sensor/swiss_public_transport.py + homeassistant/components/sensor/syncthru.py homeassistant/components/sensor/synologydsm.py homeassistant/components/sensor/systemmonitor.py homeassistant/components/sensor/sytadin.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 6ab7f42558b..eb53782d698 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -38,6 +38,7 @@ SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), @@ -53,6 +54,7 @@ SERVICE_HANDLERS = { SERVICE_HUE: ('hue', None), SERVICE_DECONZ: ('deconz', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), diff --git a/homeassistant/components/sensor/syncthru.py b/homeassistant/components/sensor/syncthru.py new file mode 100644 index 00000000000..a24482bda01 --- /dev/null +++ b/homeassistant/components/sensor/syncthru.py @@ -0,0 +1,233 @@ +""" +Support for Samsung Printers with SyncThru web interface. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/sensor.syncthru/ +""" + +import logging +import voluptuous as vol + +from homeassistant.const import ( + CONF_RESOURCE, CONF_HOST, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA + +REQUIREMENTS = ['pysyncthru==0.3.1'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Samsung Printer' +DEFAULT_MONITORED_CONDITIONS = [ + 'toner_black', + 'toner_cyan', + 'toner_magenta', + 'toner_yellow', + 'drum_black', + 'drum_cyan', + 'drum_magenta', + 'drum_yellow', + 'tray_1', + 'tray_2', + 'tray_3', + 'tray_4', + 'tray_5', + 'output_tray_0', + 'output_tray_1', + 'output_tray_2', + 'output_tray_3', + 'output_tray_4', + 'output_tray_5', +] +COLORS = [ + 'black', + 'cyan', + 'magenta', + 'yellow' +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RESOURCE): cv.url, + vol.Optional( + CONF_NAME, + default=DEFAULT_NAME + ): cv.string, + vol.Optional( + CONF_MONITORED_CONDITIONS, + default=DEFAULT_MONITORED_CONDITIONS + ): vol.All(cv.ensure_list, [vol.In(DEFAULT_MONITORED_CONDITIONS)]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the SyncThru component.""" + from pysyncthru import SyncThru, test_syncthru + + if discovery_info is not None: + host = discovery_info.get(CONF_HOST) + name = discovery_info.get(CONF_NAME, DEFAULT_NAME) + _LOGGER.debug("Discovered a new Samsung Printer: %s", discovery_info) + # Test if the discovered device actually is a syncthru printer + if not test_syncthru(host): + _LOGGER.error("No SyncThru Printer found at %s", host) + return + monitored = DEFAULT_MONITORED_CONDITIONS + else: + host = config.get(CONF_RESOURCE) + name = config.get(CONF_NAME) + monitored = config.get(CONF_MONITORED_CONDITIONS) + + # Main device, always added + try: + printer = SyncThru(host) + except TypeError: + # if an exception is thrown, printer cannot be set up + return + + printer.update() + devices = [SyncThruMainSensor(printer, name)] + + for key in printer.toner_status(filter_supported=True): + if 'toner_{}'.format(key) in monitored: + devices.append(SyncThruTonerSensor(printer, name, key)) + for key in printer.drum_status(filter_supported=True): + if 'drum_{}'.format(key) in monitored: + devices.append(SyncThruDrumSensor(printer, name, key)) + for key in printer.input_tray_status(filter_supported=True): + if 'tray_{}'.format(key) in monitored: + devices.append(SyncThruInputTraySensor(printer, name, key)) + for key in printer.output_tray_status(): + if 'output_tray_{}'.format(key) in monitored: + devices.append(SyncThruOutputTraySensor(printer, name, key)) + + add_devices(devices, True) + + +class SyncThruSensor(Entity): + """Implementation of an abstract Samsung Printer sensor platform.""" + + def __init__(self, syncthru, name): + """Initialize the sensor.""" + self.syncthru = syncthru + self._attributes = {} + self._state = None + self._name = name + self._icon = 'mdi:printer' + self._unit_of_measurement = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def icon(self): + """Return the icon of the device.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit of measuremnt.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._attributes + + +class SyncThruMainSensor(SyncThruSensor): + """Implementation of the main sensor, monitoring the general state.""" + + def update(self): + """Get the latest data from SyncThru and update the state.""" + self.syncthru.update() + self._state = self.syncthru.device_status() + + +class SyncThruTonerSensor(SyncThruSensor): + """Implementation of a Samsung Printer toner sensor platform.""" + + def __init__(self, syncthru, name, color): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Toner {}".format(name, color) + self._color = color + self._unit_of_measurement = '%' + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.toner_status( + ).get(self._color, {}) + self._state = self._attributes.get('remaining') + + +class SyncThruDrumSensor(SyncThruSensor): + """Implementation of a Samsung Printer toner sensor platform.""" + + def __init__(self, syncthru, name, color): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Drum {}".format(name, color) + self._color = color + self._unit_of_measurement = '%' + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.drum_status( + ).get(self._color, {}) + self._state = self._attributes.get('remaining') + + +class SyncThruInputTraySensor(SyncThruSensor): + """Implementation of a Samsung Printer input tray sensor platform.""" + + def __init__(self, syncthru, name, number): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Tray {}".format(name, number) + self._number = number + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.input_tray_status( + ).get(self._number, {}) + self._state = self._attributes.get('newError') + if self._state == '': + self._state = 'Ready' + + +class SyncThruOutputTraySensor(SyncThruSensor): + """Implementation of a Samsung Printer input tray sensor platform.""" + + def __init__(self, syncthru, name, number): + """Initialize the sensor.""" + super().__init__(syncthru, name) + self._name = "{} Output Tray {}".format(name, number) + self._number = number + + def update(self): + """Get the latest data from SyncThru and update the state.""" + # Data fetching is taken care of through the Main sensor + + if self.syncthru.is_online(): + self._attributes = self.syncthru.output_tray_status( + ).get(self._number, {}) + self._state = self._attributes.get('status') + if self._state == '': + self._state = 'Ready' diff --git a/requirements_all.txt b/requirements_all.txt index 37c7150c146..91173a1825c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,6 +894,9 @@ pysnmp==4.4.4 # homeassistant.components.notify.stride pystride==0.1.7 +# homeassistant.components.sensor.syncthru +pysyncthru==0.3.1 + # homeassistant.components.media_player.liveboxplaytv pyteleloisirs==3.3 From 89c7c80e42c769857389a3edda2a874b63f974f5 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 18 Mar 2018 18:00:29 -0400 Subject: [PATCH 082/924] Use hue/sat as internal light color interface (#11288) * Accept and report both xy and RGB color for lights * Fix demo light supported_features * Add new XY color util functions * Always make color changes available as xy and RGB * Always expose color as RGB and XY * Consolidate color supported_features * Test fixes * Additional test fix * Use hue/sat as the hass core color interface * Tests updates * Assume MQTT RGB devices need full RGB brightness * Convert new platforms * More migration * Use float for HS API * Fix backwards conversion for KNX lights * Adjust limitless min saturation for new scale --- homeassistant/components/alexa/smart_home.py | 21 +---- .../components/google_assistant/trait.py | 15 ++- .../components/homekit/type_lights.py | 28 +++--- homeassistant/components/light/__init__.py | 54 ++++++----- homeassistant/components/light/abode.py | 19 ++-- .../components/light/blinksticklight.py | 44 ++++++--- homeassistant/components/light/blinkt.py | 27 +++--- homeassistant/components/light/deconz.py | 23 ++--- homeassistant/components/light/demo.py | 40 +++----- homeassistant/components/light/flux_led.py | 14 +-- homeassistant/components/light/group.py | 45 +++------ homeassistant/components/light/hive.py | 29 +++--- homeassistant/components/light/hue.py | 50 ++++------ homeassistant/components/light/hyperion.py | 17 ++-- homeassistant/components/light/iglo.py | 16 ++-- homeassistant/components/light/knx.py | 23 ++--- homeassistant/components/light/lifx.py | 54 +++++------ homeassistant/components/light/lifx_legacy.py | 47 ++-------- .../components/light/limitlessled.py | 29 +++--- homeassistant/components/light/mqtt.py | 91 +++++++++---------- homeassistant/components/light/mqtt_json.py | 85 ++++++++--------- .../components/light/mqtt_template.py | 43 +++++---- homeassistant/components/light/mysensors.py | 23 ++--- homeassistant/components/light/mystrom.py | 19 ++-- .../components/light/osramlightify.py | 61 ++++--------- homeassistant/components/light/piglow.py | 27 +++--- .../components/light/rpi_gpio_pwm.py | 18 ++-- homeassistant/components/light/sensehat.py | 27 +++--- homeassistant/components/light/skybell.py | 16 ++-- homeassistant/components/light/tikteck.py | 22 +++-- homeassistant/components/light/tplink.py | 45 +++------ homeassistant/components/light/tradfri.py | 29 +++--- homeassistant/components/light/vera.py | 17 ++-- homeassistant/components/light/wemo.py | 32 +++---- homeassistant/components/light/wink.py | 28 +++--- .../components/light/xiaomi_aqara.py | 24 ++--- homeassistant/components/light/yeelight.py | 71 ++++----------- .../components/light/yeelightsunflower.py | 21 +++-- homeassistant/components/light/zengge.py | 42 ++++++--- homeassistant/components/light/zha.py | 35 +++---- homeassistant/components/light/zwave.py | 73 ++++++++------- homeassistant/components/switch/flux.py | 4 +- homeassistant/util/color.py | 41 ++++++++- tests/components/alexa/test_smart_home.py | 36 -------- .../google_assistant/test_smart_home.py | 10 +- .../components/google_assistant/test_trait.py | 10 +- tests/components/homekit/test_type_lights.py | 14 +-- tests/components/light/test_demo.py | 11 ++- tests/components/light/test_group.py | 48 ++-------- tests/components/light/test_init.py | 51 ++++++----- tests/components/light/test_mqtt.py | 28 +++--- tests/components/light/test_mqtt_json.py | 17 ++-- tests/components/light/test_mqtt_template.py | 13 +-- tests/components/light/test_zwave.py | 62 ++++++++----- tests/testing_config/.remember_the_milk.conf | 1 + .../custom_components/light/test.py | 2 +- tests/util/test_color.py | 71 +++++++++++++-- 57 files changed, 898 insertions(+), 965 deletions(-) create mode 100644 tests/testing_config/.remember_the_milk.conf diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 0d325534266..5e5155b3db8 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -438,9 +438,7 @@ class _LightCapabilities(_AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & light.SUPPORT_BRIGHTNESS: yield _AlexaBrightnessController(self.entity) - if supported & light.SUPPORT_RGB_COLOR: - yield _AlexaColorController(self.entity) - if supported & light.SUPPORT_XY_COLOR: + if supported & light.SUPPORT_COLOR: yield _AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield _AlexaColorTemperatureController(self.entity) @@ -842,25 +840,16 @@ def async_api_adjust_brightness(hass, config, request, entity): @asyncio.coroutine def async_api_set_color(hass, config, request, entity): """Process a set color request.""" - supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES) rgb = color_util.color_hsb_to_RGB( float(request[API_PAYLOAD]['color']['hue']), float(request[API_PAYLOAD]['color']['saturation']), float(request[API_PAYLOAD]['color']['brightness']) ) - if supported & light.SUPPORT_RGB_COLOR > 0: - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_RGB_COLOR: rgb, - }, blocking=False) - else: - xyz = color_util.color_RGB_to_xy(*rgb) - yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: entity.entity_id, - light.ATTR_XY_COLOR: (xyz[0], xyz[1]), - light.ATTR_BRIGHTNESS: xyz[2], - }, blocking=False) + yield from hass.services.async_call(entity.domain, SERVICE_TURN_ON, { + ATTR_ENTITY_ID: entity.entity_id, + light.ATTR_RGB_COLOR: rgb, + }, blocking=False) return api_message(request) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index c78d70e21e6..2f60f226042 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -243,7 +243,7 @@ class ColorSpectrumTrait(_Trait): if domain != light.DOMAIN: return False - return features & (light.SUPPORT_RGB_COLOR | light.SUPPORT_XY_COLOR) + return features & light.SUPPORT_COLOR def sync_attributes(self): """Return color spectrum attributes for a sync request.""" @@ -254,13 +254,11 @@ class ColorSpectrumTrait(_Trait): """Return color spectrum query attributes.""" response = {} - # No need to handle XY color because light component will always - # convert XY to RGB if possible (which is when brightness is available) - color_rgb = self.state.attributes.get(light.ATTR_RGB_COLOR) - if color_rgb is not None: + color_hs = self.state.attributes.get(light.ATTR_HS_COLOR) + if color_hs is not None: response['color'] = { 'spectrumRGB': int(color_util.color_rgb_to_hex( - color_rgb[0], color_rgb[1], color_rgb[2]), 16), + *color_util.color_hs_to_RGB(*color_hs)), 16), } return response @@ -274,11 +272,12 @@ class ColorSpectrumTrait(_Trait): """Execute a color spectrum command.""" # Convert integer to hex format and left pad with 0's till length 6 hex_value = "{0:06x}".format(params['color']['spectrumRGB']) - color = color_util.rgb_hex_to_rgb_list(hex_value) + color = color_util.color_RGB_to_hs( + *color_util.rgb_hex_to_rgb_list(hex_value)) await hass.services.async_call(light.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: self.state.entity_id, - light.ATTR_RGB_COLOR: color + light.ATTR_HS_COLOR: color }, blocking=True) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index db7172bef17..6cd60698110 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -2,10 +2,8 @@ import logging from homeassistant.components.light import ( - ATTR_RGB_COLOR, ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) + ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF -from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB from . import TYPES from .accessories import HomeAccessory, add_preload_service @@ -40,7 +38,7 @@ class Light(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_RGB_COLOR: + if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) self._hue = None @@ -102,15 +100,15 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle RGB Color - if self._features & SUPPORT_RGB_COLOR and self._flag[CHAR_HUE] and \ + # Handle Color + if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: - color = color_hsv_to_RGB(self._hue, self._saturation, 100) - _LOGGER.debug('%s: Set rgb_color to %s', self._entity_id, color) + color = (self._hue, self._saturation) + _LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) self._hass.components.light.turn_on( - self._entity_id, rgb_color=color) + self._entity_id, hs_color=color) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update light after state change.""" @@ -134,15 +132,11 @@ class Light(HomeAccessory): should_callback=False) self._flag[CHAR_BRIGHTNESS] = False - # Handle RGB Color + # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - rgb_color = new_state.attributes.get(ATTR_RGB_COLOR) - current_color = color_hsv_to_RGB(self._hue, self._saturation, 100)\ - if self._hue and self._saturation else [None] * 3 - if not self._flag[RGB_COLOR] and \ - isinstance(rgb_color, (list, tuple)) and \ - tuple(rgb_color) != current_color: - hue, saturation, _ = color_RGB_to_hsv(*rgb_color) + hue, saturation = new_state.attributes.get(ATTR_HS_COLOR) + if not self._flag[RGB_COLOR] and ( + hue != self._hue or saturation != self._saturation): self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index a3a962a7e34..f03521947b7 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -40,9 +40,8 @@ SUPPORT_BRIGHTNESS = 1 SUPPORT_COLOR_TEMP = 2 SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 -SUPPORT_RGB_COLOR = 16 +SUPPORT_COLOR = 16 SUPPORT_TRANSITION = 32 -SUPPORT_XY_COLOR = 64 SUPPORT_WHITE_VALUE = 128 # Integer that represents transition time in seconds to make change. @@ -51,6 +50,7 @@ ATTR_TRANSITION = "transition" # Lists holding color values ATTR_RGB_COLOR = "rgb_color" ATTR_XY_COLOR = "xy_color" +ATTR_HS_COLOR = "hs_color" ATTR_COLOR_TEMP = "color_temp" ATTR_KELVIN = "kelvin" ATTR_MIN_MIREDS = "min_mireds" @@ -86,8 +86,9 @@ LIGHT_PROFILES_FILE = "light_profiles.csv" PROP_TO_ATTR = { 'brightness': ATTR_BRIGHTNESS, 'color_temp': ATTR_COLOR_TEMP, - 'rgb_color': ATTR_RGB_COLOR, - 'xy_color': ATTR_XY_COLOR, + 'min_mireds': ATTR_MIN_MIREDS, + 'max_mireds': ATTR_MAX_MIREDS, + 'hs_color': ATTR_HS_COLOR, 'white_value': ATTR_WHITE_VALUE, 'effect_list': ATTR_EFFECT_LIST, 'effect': ATTR_EFFECT, @@ -111,6 +112,11 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): @@ -149,13 +155,13 @@ def is_on(hass, entity_id=None): @bind_hass def turn_on(hass, entity_id=None, transition=None, brightness=None, - brightness_pct=None, rgb_color=None, xy_color=None, + brightness_pct=None, rgb_color=None, xy_color=None, hs_color=None, color_temp=None, kelvin=None, white_value=None, profile=None, flash=None, effect=None, color_name=None): """Turn all or specified light on.""" hass.add_job( async_turn_on, hass, entity_id, transition, brightness, brightness_pct, - rgb_color, xy_color, color_temp, kelvin, white_value, + rgb_color, xy_color, hs_color, color_temp, kelvin, white_value, profile, flash, effect, color_name) @@ -163,8 +169,9 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, @bind_hass def async_turn_on(hass, entity_id=None, transition=None, brightness=None, brightness_pct=None, rgb_color=None, xy_color=None, - color_temp=None, kelvin=None, white_value=None, - profile=None, flash=None, effect=None, color_name=None): + hs_color=None, color_temp=None, kelvin=None, + white_value=None, profile=None, flash=None, effect=None, + color_name=None): """Turn all or specified light on.""" data = { key: value for key, value in [ @@ -175,6 +182,7 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, (ATTR_BRIGHTNESS_PCT, brightness_pct), (ATTR_RGB_COLOR, rgb_color), (ATTR_XY_COLOR, xy_color), + (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), (ATTR_WHITE_VALUE, white_value), @@ -254,6 +262,14 @@ def preprocess_turn_on_alternatives(params): if brightness_pct is not None: params[ATTR_BRIGHTNESS] = int(255 * brightness_pct/100) + xy_color = params.pop(ATTR_XY_COLOR, None) + if xy_color is not None: + params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + + rgb_color = params.pop(ATTR_RGB_COLOR, None) + if rgb_color is not None: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + class SetIntentHandler(intent.IntentHandler): """Handle set color intents.""" @@ -281,7 +297,7 @@ class SetIntentHandler(intent.IntentHandler): if 'color' in slots: intent.async_test_feature( - state, SUPPORT_RGB_COLOR, 'changing colors') + state, SUPPORT_COLOR, 'changing colors') service_data[ATTR_RGB_COLOR] = slots['color']['value'] # Use original passed in value of the color because we don't have # human readable names for that internally. @@ -428,13 +444,8 @@ class Light(ToggleEntity): return None @property - def xy_color(self): - """Return the XY color value [float, float].""" - return None - - @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" + def hs_color(self): + """Return the hue and saturation color value [float, float].""" return None @property @@ -484,11 +495,12 @@ class Light(ToggleEntity): if value is not None: data[attr] = value - if ATTR_RGB_COLOR not in data and ATTR_XY_COLOR in data and \ - ATTR_BRIGHTNESS in data: - data[ATTR_RGB_COLOR] = color_util.color_xy_brightness_to_RGB( - data[ATTR_XY_COLOR][0], data[ATTR_XY_COLOR][1], - data[ATTR_BRIGHTNESS]) + # Expose current color also as RGB and XY + if ATTR_HS_COLOR in data: + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB( + *data[ATTR_HS_COLOR]) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy( + *data[ATTR_HS_COLOR]) return data diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py index d3e79b38647..bfea19fc3fa 100644 --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/light/abode.py @@ -8,8 +8,9 @@ import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util DEPENDENCIES = ['abode'] @@ -44,10 +45,12 @@ class AbodeLight(AbodeDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if (ATTR_RGB_COLOR in kwargs and + if (ATTR_HS_COLOR in kwargs and self._device.is_dimmable and self._device.has_color): - self._device.set_color(kwargs[ATTR_RGB_COLOR]) - elif ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: + self._device.set_color(color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR])) + + if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: self._device.set_level(kwargs[ATTR_BRIGHTNESS]) else: self._device.switch_on() @@ -68,16 +71,16 @@ class AbodeLight(AbodeDevice, Light): return self._device.brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" if self._device.is_dimmable and self._device.has_color: - return self._device.color + return color_util.color_RGB_to_hs(*self._device.color) @property def supported_features(self): """Flag supported features.""" if self._device.is_dimmable and self._device.has_color: - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR elif self._device.is_dimmable: return SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/light/blinksticklight.py b/homeassistant/components/light/blinksticklight.py index d6a6ef465a8..18a6b4ae266 100644 --- a/homeassistant/components/light/blinksticklight.py +++ b/homeassistant/components/light/blinksticklight.py @@ -9,9 +9,11 @@ import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, + PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['blinkstick==1.1.8'] @@ -21,7 +23,7 @@ CONF_SERIAL = 'serial' DEFAULT_NAME = 'Blinkstick' -SUPPORT_BLINKSTICK = SUPPORT_RGB_COLOR +SUPPORT_BLINKSTICK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_SERIAL): cv.string, @@ -39,7 +41,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): stick = blinkstick.find_by_serial(serial) - add_devices([BlinkStickLight(stick, name)]) + add_devices([BlinkStickLight(stick, name)], True) class BlinkStickLight(Light): @@ -50,7 +52,8 @@ class BlinkStickLight(Light): self._stick = stick self._name = name self._serial = stick.get_serial() - self._rgb_color = stick.get_color() + self._hs_color = None + self._brightness = None @property def should_poll(self): @@ -63,14 +66,19 @@ class BlinkStickLight(Light): return self._name @property - def rgb_color(self): + def brightness(self): + """Read back the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): """Read back the color of the light.""" - return self._rgb_color + return self._hs_color @property def is_on(self): - """Check whether any of the LEDs colors are non-zero.""" - return sum(self._rgb_color) > 0 + """Return True if entity is on.""" + return self._brightness > 0 @property def supported_features(self): @@ -79,18 +87,24 @@ class BlinkStickLight(Light): def update(self): """Read back the device state.""" - self._rgb_color = self._stick.get_color() + rgb_color = self._stick.get_color() + hsv = color_util.color_RGB_to_hsv(*rgb_color) + self._hs_color = hsv[:2] + self._brightness = hsv[2] def turn_on(self, **kwargs): """Turn the device on.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs[ATTR_BRIGHTNESS] else: - self._rgb_color = [255, 255, 255] + self._brightness = 255 - self._stick.set_color(red=self._rgb_color[0], - green=self._rgb_color[1], - blue=self._rgb_color[2]) + rgb_color = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._stick.set_color( + red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) def turn_off(self, **kwargs): """Turn the device off.""" diff --git a/homeassistant/components/light/blinkt.py b/homeassistant/components/light/blinkt.py index db3171cf4cf..97edd7c54d2 100644 --- a/homeassistant/components/light/blinkt.py +++ b/homeassistant/components/light/blinkt.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['blinkt==0.1.0'] _LOGGER = logging.getLogger(__name__) -SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_BLINKT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'blinkt' @@ -55,7 +56,7 @@ class BlinktLight(Light): self._index = index self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -71,12 +72,9 @@ class BlinktLight(Light): return self._brightness @property - def rgb_color(self): - """Read back the color of the light. - - Returns [r, g, b] list with values in range of 0-255. - """ - return self._rgb_color + def hs_color(self): + """Read back the color of the light.""" + return self._hs_color @property def supported_features(self): @@ -100,16 +98,17 @@ class BlinktLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on and set correct brightness & color.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] percent_bright = (self._brightness / 255) + rgb_color = color_util.color_hs_to_RGB(*self._hs_color) self._blinkt.set_pixel(self._index, - self._rgb_color[0], - self._rgb_color[1], - self._rgb_color[2], + rgb_color[0], + rgb_color[1], + rgb_color[2], percent_bright) self._blinkt.show() diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index a3e54434109..020f43d9935 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -7,12 +7,12 @@ https://home-assistant.io/components/light.deconz/ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback -from homeassistant.util.color import color_RGB_to_xy +import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] @@ -51,8 +51,7 @@ class DeconzLight(Light): self._features |= SUPPORT_COLOR_TEMP if self._light.xy is not None: - self._features |= SUPPORT_RGB_COLOR - self._features |= SUPPORT_XY_COLOR + self._features |= SUPPORT_COLOR if self._light.effect is not None: self._features |= SUPPORT_EFFECT @@ -124,14 +123,8 @@ class DeconzLight(Light): if ATTR_COLOR_TEMP in kwargs: data['ct'] = kwargs[ATTR_COLOR_TEMP] - if ATTR_RGB_COLOR in kwargs: - xyb = color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - data['xy'] = xyb[0], xyb[1] - data['bri'] = xyb[2] - - if ATTR_XY_COLOR in kwargs: - data['xy'] = kwargs[ATTR_XY_COLOR] + if ATTR_HS_COLOR in kwargs: + data['xy'] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) if ATTR_BRIGHTNESS in kwargs: data['bri'] = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index acc70a57ff4..05aecd542e2 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -7,14 +7,13 @@ https://home-assistant.io/components/demo/ import random from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, - Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) LIGHT_COLORS = [ - [237, 224, 33], - [255, 63, 111], + (56, 86), + (345, 75), ] LIGHT_EFFECT_LIST = ['rainbow', 'none'] @@ -22,7 +21,7 @@ LIGHT_EFFECT_LIST = ['rainbow', 'none'] LIGHT_TEMPS = [240, 380] SUPPORT_DEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) + SUPPORT_COLOR | SUPPORT_WHITE_VALUE) def setup_platform(hass, config, add_devices_callback, discovery_info=None): @@ -40,17 +39,16 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): class DemoLight(Light): """Representation of a demo light.""" - def __init__(self, unique_id, name, state, available=False, rgb=None, - ct=None, brightness=180, xy_color=(.5, .5), white=200, - effect_list=None, effect=None): + def __init__(self, unique_id, name, state, available=False, hs_color=None, + ct=None, brightness=180, white=200, effect_list=None, + effect=None): """Initialize the light.""" self._unique_id = unique_id self._name = name self._state = state - self._rgb = rgb + self._hs_color = hs_color self._ct = ct or random.choice(LIGHT_TEMPS) self._brightness = brightness - self._xy_color = xy_color self._white = white self._effect_list = effect_list self._effect = effect @@ -83,14 +81,9 @@ class DemoLight(Light): return self._brightness @property - def xy_color(self) -> tuple: - """Return the XY color value [float, float].""" - return self._xy_color - - @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" - return self._rgb + def hs_color(self) -> tuple: + """Return the hs color value.""" + return self._hs_color @property def color_temp(self) -> int: @@ -126,8 +119,8 @@ class DemoLight(Light): """Turn the light on.""" self._state = True - if ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: self._ct = kwargs[ATTR_COLOR_TEMP] @@ -135,9 +128,6 @@ class DemoLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - if ATTR_XY_COLOR in kwargs: - self._xy_color = kwargs[ATTR_XY_COLOR] - if ATTR_WHITE_VALUE in kwargs: self._white = kwargs[ATTR_WHITE_VALUE] diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 2a239c9ae10..ed0836f1449 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -12,10 +12,11 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_RGB_COLOR, Light, PLATFORM_SCHEMA) + SUPPORT_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['flux_led==0.21'] @@ -27,7 +28,7 @@ ATTR_MODE = 'mode' DOMAIN = 'flux_led' SUPPORT_FLUX_LED = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR) + SUPPORT_COLOR) MODE_RGB = 'rgb' MODE_RGBW = 'rgbw' @@ -183,9 +184,9 @@ class FluxLight(Light): return self._bulb.brightness @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._bulb.getRgb() + return color_util.color_RGB_to_hs(*self._bulb.getRgb()) @property def supported_features(self): @@ -202,7 +203,8 @@ class FluxLight(Light): if not self.is_on: self._bulb.turnOn() - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + rgb = color_util.color_hs_to_RGB(*hs_color) brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py index b4a5e9dddfb..f9ffbb4e0bf 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/group.py @@ -19,12 +19,11 @@ from homeassistant.const import (STATE_ON, ATTR_ENTITY_ID, CONF_NAME, from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.light import ( - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_XY_COLOR, - SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_XY_COLOR, - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION) + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, + ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, + ATTR_FLASH, ATTR_TRANSITION) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -37,8 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT - | SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION - | SUPPORT_XY_COLOR | SUPPORT_WHITE_VALUE) + | SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION + | SUPPORT_WHITE_VALUE) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -58,8 +57,7 @@ class LightGroup(light.Light): self._is_on = False # type: bool self._available = False # type: bool self._brightness = None # type: Optional[int] - self._xy_color = None # type: Optional[Tuple[float, float]] - self._rgb_color = None # type: Optional[Tuple[int, int, int]] + self._hs_color = None # type: Optional[Tuple[float, float]] self._color_temp = None # type: Optional[int] self._min_mireds = 154 # type: Optional[int] self._max_mireds = 500 # type: Optional[int] @@ -108,14 +106,9 @@ class LightGroup(light.Light): return self._brightness @property - def xy_color(self) -> Optional[Tuple[float, float]]: - """Return the XY color value [float, float].""" - return self._xy_color - - @property - def rgb_color(self) -> Optional[Tuple[int, int, int]]: - """Return the RGB color value [int, int, int].""" - return self._rgb_color + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the HS color value [float, float].""" + return self._hs_color @property def color_temp(self) -> Optional[int]: @@ -164,11 +157,8 @@ class LightGroup(light.Light): if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] - if ATTR_XY_COLOR in kwargs: - data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] - - if ATTR_RGB_COLOR in kwargs: - data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + data[ATTR_HS_COLOR] = kwargs[ATTR_HS_COLOR] if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] @@ -210,13 +200,8 @@ class LightGroup(light.Light): self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) - self._xy_color = _reduce_attribute( - on_states, ATTR_XY_COLOR, reduce=_mean_tuple) - - self._rgb_color = _reduce_attribute( - on_states, ATTR_RGB_COLOR, reduce=_mean_tuple) - if self._rgb_color is not None: - self._rgb_color = tuple(map(int, self._rgb_color)) + self._hs_color = _reduce_attribute( + on_states, ATTR_HS_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index e57bdf2c046..c4ecc5a9d2c 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -4,13 +4,13 @@ Support for the Hive devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.hive/ """ -import colorsys from homeassistant.components.hive import DATA_HIVE from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_RGB_COLOR, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util DEPENDENCIES = ['hive'] @@ -75,10 +75,11 @@ class HiveDeviceLight(Light): return self.session.light.get_color_temp(self.node_id) @property - def rgb_color(self) -> tuple: - """Return the RBG color value.""" + def hs_color(self) -> tuple: + """Return the hs color value.""" if self.light_device_type == "colourtuneablelight": - return self.session.light.get_color(self.node_id) + rgb = self.session.light.get_color(self.node_id) + return color_util.color_RGB_to_hs(*rgb) @property def is_on(self): @@ -99,15 +100,11 @@ class HiveDeviceLight(Light): if ATTR_COLOR_TEMP in kwargs: tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) new_color_temp = round(1000000 / tmp_new_color_temp) - if ATTR_RGB_COLOR in kwargs: - get_new_color = kwargs.get(ATTR_RGB_COLOR) - tmp_new_color = colorsys.rgb_to_hsv(get_new_color[0], - get_new_color[1], - get_new_color[2]) - hue = int(round(tmp_new_color[0] * 360)) - saturation = int(round(tmp_new_color[1] * 100)) - value = int(round((tmp_new_color[2] / 255) * 100)) - new_color = (hue, saturation, value) + if ATTR_HS_COLOR in kwargs: + get_new_color = kwargs.get(ATTR_HS_COLOR) + hue = int(get_new_color[0]) + saturation = int(get_new_color[1]) + new_color = (hue, saturation, 100) self.session.light.turn_on(self.node_id, self.light_device_type, new_brightness, new_color_temp, @@ -132,7 +129,7 @@ class HiveDeviceLight(Light): supported_features = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP) elif self.light_device_type == "colourtuneablelight": supported_features = ( - SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR) + SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR) return supported_features diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index c45d9c5c44e..b1562aaba8f 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -13,12 +13,11 @@ import async_timeout import homeassistant.components.hue as hue from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, - FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) -import homeassistant.util.color as color_util + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, + ATTR_TRANSITION, ATTR_HS_COLOR, EFFECT_COLORLOOP, EFFECT_RANDOM, + FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, + Light) DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -28,8 +27,7 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_HUE_ON_OFF = (SUPPORT_FLASH | SUPPORT_TRANSITION) SUPPORT_HUE_DIMMABLE = (SUPPORT_HUE_ON_OFF | SUPPORT_BRIGHTNESS) SUPPORT_HUE_COLOR_TEMP = (SUPPORT_HUE_DIMMABLE | SUPPORT_COLOR_TEMP) -SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | - SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR) +SUPPORT_HUE_COLOR = (SUPPORT_HUE_DIMMABLE | SUPPORT_EFFECT | SUPPORT_COLOR) SUPPORT_HUE_EXTENDED = (SUPPORT_HUE_COLOR_TEMP | SUPPORT_HUE_COLOR) SUPPORT_HUE = { @@ -228,11 +226,17 @@ class HueLight(Light): return self.light.state.get('bri') @property - def xy_color(self): - """Return the XY color value.""" + def hs_color(self): + """Return the hs color value.""" if self.is_group: - return self.light.action.get('xy') - return self.light.state.get('xy') + return ( + self.light.action.get('hue') / 65535 * 360, + self.light.action.get('sat') / 255 * 100, + ) + return ( + self.light.state.get('hue') / 65535 * 360, + self.light.state.get('sat') / 255 * 100, + ) @property def color_temp(self): @@ -272,25 +276,9 @@ class HueLight(Light): if ATTR_TRANSITION in kwargs: command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) - if ATTR_XY_COLOR in kwargs: - if self.is_osram: - color_hue, sat = color_util.color_xy_to_hs( - *kwargs[ATTR_XY_COLOR]) - command['hue'] = color_hue / 360 * 65535 - command['sat'] = sat / 100 * 255 - else: - command['xy'] = kwargs[ATTR_XY_COLOR] - elif ATTR_RGB_COLOR in kwargs: - if self.is_osram: - hsv = color_util.color_RGB_to_hsv( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['hue'] = hsv[0] / 360 * 65535 - command['sat'] = hsv[1] / 100 * 255 - command['bri'] = hsv[2] / 100 * 255 - else: - xyb = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - command['xy'] = xyb[0], xyb[1] + if ATTR_HS_COLOR in kwargs: + command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) elif ATTR_COLOR_TEMP in kwargs: temp = kwargs[ATTR_COLOR_TEMP] command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index 2057192299e..e5a4bd18115 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -11,10 +11,11 @@ import socket import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_NAME) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,7 @@ DEFAULT_EFFECT_LIST = ['HDMI', 'Cinema brighten lights', 'Cinema dim lights', 'Color traces', 'UDP multicast listener', 'UDP listener', 'X-Mas'] -SUPPORT_HYPERION = (SUPPORT_RGB_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) +SUPPORT_HYPERION = (SUPPORT_COLOR | SUPPORT_BRIGHTNESS | SUPPORT_EFFECT) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -107,9 +108,9 @@ class Hyperion(Light): return self._brightness @property - def rgb_color(self): - """Return last RGB color value set.""" - return self._rgb_color + def hs_color(self): + """Return last color value set.""" + return color_util.color_RGB_to_hs(*self._rgb_color) @property def is_on(self): @@ -138,8 +139,8 @@ class Hyperion(Light): def turn_on(self, **kwargs): """Turn the lights on.""" - if ATTR_RGB_COLOR in kwargs: - rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) elif self._rgb_mem == [0, 0, 0]: rgb_color = self._default_color else: diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index c7de8d8bede..77e3972968c 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -10,8 +10,8 @@ import math import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, SUPPORT_EFFECT, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_EFFECT, PLATFORM_SCHEMA, Light) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -77,9 +77,9 @@ class IGloLamp(Light): self._lamp.min_kelvin)) @property - def rgb_color(self): - """Return the RGB value.""" - return self._lamp.state()['rgb'] + def hs_color(self): + """Return the hs value.""" + return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb']) @property def effect(self): @@ -95,7 +95,7 @@ class IGloLamp(Light): def supported_features(self): """Flag supported features.""" return (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_RGB_COLOR | SUPPORT_EFFECT) + SUPPORT_COLOR | SUPPORT_EFFECT) @property def is_on(self): @@ -111,8 +111,8 @@ class IGloLamp(Light): self._lamp.brightness(brightness) return - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._lamp.rgb(*rgb) return diff --git a/homeassistant/components/light/knx.py b/homeassistant/components/light/knx.py index 83083e34bad..18446951735 100644 --- a/homeassistant/components/light/knx.py +++ b/homeassistant/components/light/knx.py @@ -9,11 +9,12 @@ import voluptuous as vol from homeassistant.components.knx import ATTR_DISCOVER_DEVICES, DATA_KNX from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, Light) from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util CONF_ADDRESS = 'address' CONF_STATE_ADDRESS = 'state_address' @@ -114,15 +115,10 @@ class KNXLight(Light): None @property - def xy_color(self): - """Return the XY color value [float, float].""" - return None - - @property - def rgb_color(self): - """Return the RBG color value.""" + def hs_color(self): + """Return the HS color value.""" if self.device.supports_color: - return self.device.current_color + return color_util.color_RGB_to_hs(*self.device.current_color) return None @property @@ -157,7 +153,7 @@ class KNXLight(Light): if self.device.supports_brightness: flags |= SUPPORT_BRIGHTNESS if self.device.supports_color: - flags |= SUPPORT_RGB_COLOR + flags |= SUPPORT_COLOR return flags async def async_turn_on(self, **kwargs): @@ -165,9 +161,10 @@ class KNXLight(Light): if ATTR_BRIGHTNESS in kwargs: if self.device.supports_brightness: await self.device.set_brightness(int(kwargs[ATTR_BRIGHTNESS])) - elif ATTR_RGB_COLOR in kwargs: + elif ATTR_HS_COLOR in kwargs: if self.device.supports_color: - await self.device.set_color(kwargs[ATTR_RGB_COLOR]) + await self.device.set_color(color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR])) else: await self.device.set_on() diff --git a/homeassistant/components/light/lifx.py b/homeassistant/components/light/lifx.py index 0bb65a78c6e..dff5ccd42ac 100644 --- a/homeassistant/components/light/lifx.py +++ b/homeassistant/components/light/lifx.py @@ -16,10 +16,10 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, - ATTR_EFFECT, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_XY_COLOR, - DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, - SUPPORT_XY_COLOR, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_KELVIN, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_XY_COLOR, COLOR_GROUP, DOMAIN, LIGHT_TURN_ON_SCHEMA, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, + SUPPORT_TRANSITION, VALID_BRIGHTNESS, VALID_BRIGHTNESS_PCT, Light, preprocess_turn_on_alternatives) from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -87,11 +87,22 @@ LIFX_EFFECT_SCHEMA = vol.Schema({ LIFX_EFFECT_PULSE_SCHEMA = LIFX_EFFECT_SCHEMA.extend({ ATTR_BRIGHTNESS: VALID_BRIGHTNESS, ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_COLOR_NAME: cv.string, - ATTR_RGB_COLOR: vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), - vol.Coerce(tuple)), - ATTR_COLOR_TEMP: vol.All(vol.Coerce(int), vol.Range(min=1)), - ATTR_KELVIN: vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.byte, cv.byte, cv.byte)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence((cv.small_float, cv.small_float)), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): + vol.All(vol.ExactSequence( + (vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)))), + vol.Coerce(tuple)), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=1)), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): + vol.All(vol.Coerce(int), vol.Range(min=0)), ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), ATTR_MODE: vol.In(PULSE_MODES), @@ -168,16 +179,8 @@ def find_hsbk(**kwargs): preprocess_turn_on_alternatives(kwargs) - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - color_util.color_RGB_to_hsv(*kwargs[ATTR_RGB_COLOR]) - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - brightness = int(brightness / 100 * 65535) - kelvin = 3500 - - if ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] hue = int(hue / 360 * 65535) saturation = int(saturation / 100 * 65535) kelvin = 3500 @@ -585,7 +588,7 @@ class LIFXColor(LIFXLight): def supported_features(self): """Flag supported features.""" support = super().supported_features - support |= SUPPORT_RGB_COLOR | SUPPORT_XY_COLOR + support |= SUPPORT_COLOR return support @property @@ -598,15 +601,12 @@ class LIFXColor(LIFXLight): ] @property - def rgb_color(self): - """Return the RGB value.""" - hue, sat, bri, _ = self.device.color - + def hs_color(self): + """Return the hs value.""" + hue, sat, _, _ = self.device.color hue = hue / 65535 * 360 sat = sat / 65535 * 100 - bri = bri / 65535 * 100 - - return color_util.color_hsv_to_RGB(hue, sat, bri) + return (hue, sat) class LIFXStrip(LIFXColor): diff --git a/homeassistant/components/light/lifx_legacy.py b/homeassistant/components/light/lifx_legacy.py index cf3dba848a8..490eeb6ecab 100644 --- a/homeassistant/components/light/lifx_legacy.py +++ b/homeassistant/components/light/lifx_legacy.py @@ -7,14 +7,13 @@ not yet support Windows. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.lifx/ """ -import colorsys import logging import voluptuous as vol from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) from homeassistant.helpers.event import track_time_change from homeassistant.util.color import ( @@ -37,7 +36,7 @@ TEMP_MAX_HASS = 500 TEMP_MIN = 2500 TEMP_MIN_HASS = 154 -SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | +SUPPORT_LIFX = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -129,17 +128,6 @@ class LIFX(object): self._liffylights.probe(address) -def convert_rgb_to_hsv(rgb): - """Convert Home Assistant RGB values to HSV values.""" - red, green, blue = [_ / BYTE_MAX for _ in rgb] - - hue, saturation, brightness = colorsys.rgb_to_hsv(red, green, blue) - - return [int(hue * SHORT_MAX), - int(saturation * SHORT_MAX), - int(brightness * SHORT_MAX)] - - class LIFXLight(Light): """Representation of a LIFX light.""" @@ -170,11 +158,9 @@ class LIFXLight(Light): return self._ip @property - def rgb_color(self): - """Return the RGB value.""" - _LOGGER.debug( - "rgb_color: [%d %d %d]", self._rgb[0], self._rgb[1], self._rgb[2]) - return self._rgb + def hs_color(self): + """Return the hs value.""" + return (self._hue / 65535 * 360, self._sat / 65535 * 100) @property def brightness(self): @@ -209,13 +195,13 @@ class LIFXLight(Light): else: fade = 0 - if ATTR_RGB_COLOR in kwargs: - hue, saturation, brightness = \ - convert_rgb_to_hsv(kwargs[ATTR_RGB_COLOR]) + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + hue = hue / 360 * 65535 + saturation = saturation / 100 * 65535 else: hue = self._hue saturation = self._sat - brightness = self._bri if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] * (BYTE_MAX + 1) @@ -265,16 +251,3 @@ class LIFXLight(Light): self._sat = sat self._bri = bri self._kel = kel - - red, green, blue = colorsys.hsv_to_rgb(hue / SHORT_MAX, - sat / SHORT_MAX, - bri / SHORT_MAX) - - red = int(red * BYTE_MAX) - green = int(green * BYTE_MAX) - blue = int(blue * BYTE_MAX) - - _LOGGER.debug("set_color: %d %d %d %d [%d %d %d]", - hue, sat, bri, kel, red, green, blue) - - self._rgb = [red, green, blue] diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index f011792a15c..5a6a0a34959 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -12,12 +12,13 @@ import voluptuous as vol from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_TYPE, STATE_ON) from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_WHITE, FLASH_LONG, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) + SUPPORT_COLOR, SUPPORT_TRANSITION, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv -from homeassistant.util.color import color_temperature_mired_to_kelvin +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin, color_hs_to_RGB) from homeassistant.helpers.restore_state import async_get_last_state REQUIREMENTS = ['limitlessled==1.1.0'] @@ -40,19 +41,19 @@ LED_TYPE = ['rgbw', 'rgbww', 'white', 'bridge-led', 'dimmer'] EFFECT_NIGHT = 'night' -RGB_BOUNDARY = 40 +MIN_SATURATION = 10 -WHITE = [255, 255, 255] +WHITE = [0, 0] SUPPORT_LIMITLESSLED_WHITE = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_DIMMER = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_RGB = (SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | - SUPPORT_FLASH | SUPPORT_RGB_COLOR | + SUPPORT_FLASH | SUPPORT_COLOR | SUPPORT_TRANSITION) SUPPORT_LIMITLESSLED_RGBWW = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_FLASH | - SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) + SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_BRIDGES): vol.All(cv.ensure_list, [ @@ -239,7 +240,7 @@ class LimitlessLEDGroup(Light): return self._temperature @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" return self._color @@ -282,17 +283,17 @@ class LimitlessLEDGroup(Light): self._brightness = kwargs[ATTR_BRIGHTNESS] args['brightness'] = self.limitlessled_brightness() - if ATTR_RGB_COLOR in kwargs and self._supported & SUPPORT_RGB_COLOR: - self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs and self._supported & SUPPORT_COLOR: + self._color = kwargs[ATTR_HS_COLOR] # White is a special case. - if min(self._color) > 256 - RGB_BOUNDARY: + if self._color[1] < MIN_SATURATION: pipeline.white() self._color = WHITE else: args['color'] = self.limitlessled_color() if ATTR_COLOR_TEMP in kwargs: - if self._supported & SUPPORT_RGB_COLOR: + if self._supported & SUPPORT_COLOR: pipeline.white() self._color = WHITE if self._supported & SUPPORT_COLOR_TEMP: @@ -333,6 +334,6 @@ class LimitlessLEDGroup(Light): return self._brightness / 255 def limitlessled_color(self): - """Convert Home Assistant RGB list to Color tuple.""" + """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(*tuple(self._color)) + return Color(color_hs_to_RGB(*tuple(self._color))) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index f97e37127b1..a0534ba4e95 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -12,10 +12,9 @@ import voluptuous as vol from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, Light, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, - SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_WHITE_VALUE, Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, @@ -25,6 +24,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -191,14 +191,13 @@ class MqttLight(MqttAvailability, Light): self._on_command_type = on_command_type self._state = False self._brightness = None - self._rgb = None + self._hs = None self._color_temp = None self._effect = None self._white_value = None - self._xy = None self._supported_features = 0 self._supported_features |= ( - topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_RGB_COLOR) + topic[CONF_RGB_COMMAND_TOPIC] is not None and SUPPORT_COLOR) self._supported_features |= ( topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None and SUPPORT_BRIGHTNESS) @@ -212,7 +211,7 @@ class MqttLight(MqttAvailability, Light): topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None and SUPPORT_WHITE_VALUE) self._supported_features |= ( - topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_XY_COLOR) + topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -263,19 +262,18 @@ class MqttLight(MqttAvailability, Light): @callback def rgb_received(topic, payload, qos): """Handle new MQTT messages for RGB.""" - self._rgb = [int(val) for val in - templates[CONF_RGB](payload).split(',')] + rgb = [int(val) for val in + templates[CONF_RGB](payload).split(',')] + self._hs = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: yield from mqtt.async_subscribe( self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self._qos) - self._rgb = [255, 255, 255] + self._hs = (0, 0) if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: - self._rgb = [255, 255, 255] - else: - self._rgb = None + self._hs = (0, 0) @callback def color_temp_received(topic, payload, qos): @@ -330,19 +328,18 @@ class MqttLight(MqttAvailability, Light): @callback def xy_received(topic, payload, qos): """Handle new MQTT messages for color.""" - self._xy = [float(val) for val in + xy_color = [float(val) for val in templates[CONF_XY](payload).split(',')] + self._hs = color_util.color_xy_to_hs(*xy_color) self.async_schedule_update_ha_state() if self._topic[CONF_XY_STATE_TOPIC] is not None: yield from mqtt.async_subscribe( self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self._qos) - self._xy = [1, 1] + self._hs = (0, 0) if self._topic[CONF_XY_COMMAND_TOPIC] is not None: - self._xy = [1, 1] - else: - self._xy = None + self._hs = (0, 0) @property def brightness(self): @@ -350,9 +347,9 @@ class MqttLight(MqttAvailability, Light): return self._brightness @property - def rgb_color(self): - """Return the RGB color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def color_temp(self): @@ -364,11 +361,6 @@ class MqttLight(MqttAvailability, Light): """Return the white property.""" return self._white_value - @property - def xy_color(self): - """Return the RGB color value.""" - return self._xy - @property def should_poll(self): """No polling needed for a MQTT light.""" @@ -426,24 +418,43 @@ class MqttLight(MqttAvailability, Light): kwargs[ATTR_BRIGHTNESS] = self._brightness if \ self._brightness else 255 - if ATTR_RGB_COLOR in kwargs and \ + if ATTR_HS_COLOR in kwargs and \ self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) tpl = self._templates[CONF_RGB_COMMAND_TEMPLATE] if tpl: - colors = ('red', 'green', 'blue') - variables = {key: val for key, val in - zip(colors, kwargs[ATTR_RGB_COLOR])} - rgb_color_str = tpl.async_render(variables) + rgb_color_str = tpl.async_render({ + 'red': rgb[0], + 'green': rgb[1], + 'blue': rgb[2], + }) else: - rgb_color_str = '{},{},{}'.format(*kwargs[ATTR_RGB_COLOR]) + rgb_color_str = '{},{},{}'.format(*rgb) mqtt.async_publish( self.hass, self._topic[CONF_RGB_COMMAND_TOPIC], rgb_color_str, self._qos, self._retain) if self._optimistic_rgb: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] + should_update = True + + if ATTR_HS_COLOR in kwargs and \ + self._topic[CONF_XY_COMMAND_TOPIC] is not None: + + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + mqtt.async_publish( + self.hass, self._topic[CONF_XY_COMMAND_TOPIC], + '{},{}'.format(*xy_color), self._qos, + self._retain) + + if self._optimistic_xy: + self._hs = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_BRIGHTNESS in kwargs and \ @@ -493,18 +504,6 @@ class MqttLight(MqttAvailability, Light): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - if ATTR_XY_COLOR in kwargs and \ - self._topic[CONF_XY_COMMAND_TOPIC] is not None: - - mqtt.async_publish( - self.hass, self._topic[CONF_XY_COMMAND_TOPIC], - '{},{}'.format(*kwargs[ATTR_XY_COLOR]), self._qos, - self._retain) - - if self._optimistic_xy: - self._xy = kwargs[ATTR_XY_COLOR] - should_update = True - if self._on_command_type == 'last': mqtt.async_publish(self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload['on'], self._qos, self._retain) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 19747b89ca0..25212e45c60 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -13,10 +13,10 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_HS_COLOR, FLASH_LONG, FLASH_SHORT, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, SUPPORT_XY_COLOR) + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, @@ -26,6 +26,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -143,31 +144,26 @@ class MqttJson(MqttAvailability, Light): else: self._effect = None - if rgb: - self._rgb = [0, 0, 0] + if rgb or xy: + self._hs = [0, 0] else: - self._rgb = None + self._hs = None if white_value: self._white_value = 255 else: self._white_value = None - if xy: - self._xy = [1, 1] - else: - self._xy = None - self._flash_times = flash_times self._brightness_scale = brightness_scale self._supported_features = (SUPPORT_TRANSITION | SUPPORT_FLASH) - self._supported_features |= (rgb and SUPPORT_RGB_COLOR) + self._supported_features |= (rgb and SUPPORT_COLOR) self._supported_features |= (brightness and SUPPORT_BRIGHTNESS) self._supported_features |= (color_temp and SUPPORT_COLOR_TEMP) self._supported_features |= (effect and SUPPORT_EFFECT) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE) - self._supported_features |= (xy and SUPPORT_XY_COLOR) + self._supported_features |= (xy and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -184,17 +180,26 @@ class MqttJson(MqttAvailability, Light): elif values['state'] == 'OFF': self._state = False - if self._rgb is not None: + if self._hs is not None: try: red = int(values['color']['r']) green = int(values['color']['g']) blue = int(values['color']['b']) - self._rgb = [red, green, blue] + self._hs = color_util.color_RGB_to_hs(red, green, blue) except KeyError: pass except ValueError: _LOGGER.warning("Invalid RGB color value received") + try: + x_color = float(values['color']['x']) + y_color = float(values['color']['y']) + + self._hs = color_util.color_xy_to_hs(x_color, y_color) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid XY color value received") if self._brightness is not None: try: @@ -230,17 +235,6 @@ class MqttJson(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid white value received") - if self._xy is not None: - try: - x_color = float(values['color']['x']) - y_color = float(values['color']['y']) - - self._xy = [x_color, y_color] - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid XY color value received") - self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -269,20 +263,15 @@ class MqttJson(MqttAvailability, Light): return self._effect_list @property - def rgb_color(self): - """Return the RGB color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def white_value(self): """Return the white property.""" return self._white_value - @property - def xy_color(self): - """Return the XY color value.""" - return self._xy - @property def should_poll(self): """No polling needed for a MQTT light.""" @@ -318,15 +307,23 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_RGB_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) message['color'] = { - 'r': kwargs[ATTR_RGB_COLOR][0], - 'g': kwargs[ATTR_RGB_COLOR][1], - 'b': kwargs[ATTR_RGB_COLOR][2] + 'r': rgb[0], + 'g': rgb[1], + 'b': rgb[2], + 'x': xy_color[0], + 'y': xy_color[1], } if self._optimistic: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] should_update = True if ATTR_FLASH in kwargs: @@ -370,16 +367,6 @@ class MqttJson(MqttAvailability, Light): self._white_value = kwargs[ATTR_WHITE_VALUE] should_update = True - if ATTR_XY_COLOR in kwargs: - message['color'] = { - 'x': kwargs[ATTR_XY_COLOR][0], - 'y': kwargs[ATTR_XY_COLOR][1] - } - - if self._optimistic: - self._xy = kwargs[ATTR_XY_COLOR] - should_update = True - mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], json.dumps(message), self._qos, self._retain) diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index de0f6d934c6..06a94cd23b4 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -12,15 +12,16 @@ from homeassistant.core import callback import homeassistant.components.mqtt as mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, + ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) + SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, STATE_ON, STATE_OFF from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -142,9 +143,9 @@ class MqttTemplate(MqttAvailability, Light): if (self._templates[CONF_RED_TEMPLATE] is not None and self._templates[CONF_GREEN_TEMPLATE] is not None and self._templates[CONF_BLUE_TEMPLATE] is not None): - self._rgb = [0, 0, 0] + self._hs = [0, 0] else: - self._rgb = None + self._hs = None self._effect = None for tpl in self._templates.values(): @@ -186,17 +187,18 @@ class MqttTemplate(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid color temperature value received") - if self._rgb is not None: + if self._hs is not None: try: - self._rgb[0] = int( + red = int( self._templates[CONF_RED_TEMPLATE]. async_render_with_possible_json_value(payload)) - self._rgb[1] = int( + green = int( self._templates[CONF_GREEN_TEMPLATE]. async_render_with_possible_json_value(payload)) - self._rgb[2] = int( + blue = int( self._templates[CONF_BLUE_TEMPLATE]. async_render_with_possible_json_value(payload)) + self._hs = color_util.color_RGB_to_hs(red, green, blue) except ValueError: _LOGGER.warning("Invalid color value received") @@ -236,9 +238,9 @@ class MqttTemplate(MqttAvailability, Light): return self._color_temp @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" - return self._rgb + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs @property def white_value(self): @@ -300,13 +302,18 @@ class MqttTemplate(MqttAvailability, Light): if self._optimistic: self._color_temp = kwargs[ATTR_COLOR_TEMP] - if ATTR_RGB_COLOR in kwargs: - values['red'] = kwargs[ATTR_RGB_COLOR][0] - values['green'] = kwargs[ATTR_RGB_COLOR][1] - values['blue'] = kwargs[ATTR_RGB_COLOR][2] + if ATTR_HS_COLOR in kwargs: + hs_color = kwargs[ATTR_HS_COLOR] + brightness = kwargs.get( + ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + values['red'] = rgb[0] + values['green'] = rgb[1] + values['blue'] = rgb[2] if self._optimistic: - self._rgb = kwargs[ATTR_RGB_COLOR] + self._hs = kwargs[ATTR_HS_COLOR] if ATTR_WHITE_VALUE in kwargs: values['white_value'] = int(kwargs[ATTR_WHITE_VALUE]) @@ -360,8 +367,8 @@ class MqttTemplate(MqttAvailability, Light): features = (SUPPORT_FLASH | SUPPORT_TRANSITION) if self._brightness is not None: features = features | SUPPORT_BRIGHTNESS - if self._rgb is not None: - features = features | SUPPORT_RGB_COLOR + if self._hs is not None: + features = features | SUPPORT_COLOR if self._effect_list is not None: features = features | SUPPORT_EFFECT if self._color_temp is not None: diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index a37553017e7..26e20ff387d 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -6,12 +6,13 @@ https://home-assistant.io/components/light.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_WHITE_VALUE, DOMAIN, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list +import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | +SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) @@ -35,7 +36,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): super().__init__(*args) self._state = None self._brightness = None - self._rgb = None + self._hs = None self._white = None @property @@ -44,9 +45,9 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): return self._brightness @property - def rgb_color(self): - """Return the RGB color value [int, int, int].""" - return self._rgb + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs @property def white_value(self): @@ -103,10 +104,10 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" - rgb = self._rgb + rgb = color_util.color_hs_to_RGB(*self._hs) white = self._white hex_color = self._values.get(self.value_type) - new_rgb = kwargs.get(ATTR_RGB_COLOR) + new_rgb = color_util.color_hs_to_RGB(*kwargs.get(ATTR_HS_COLOR)) new_white = kwargs.get(ATTR_WHITE_VALUE) if new_rgb is None and new_white is None: @@ -126,7 +127,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): if self.gateway.optimistic: # optimistically assume that light has changed state - self._rgb = rgb + self._hs = color_util.color_RGB_to_hs(*rgb) self._white = white self._values[self.value_type] = hex_color @@ -160,7 +161,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): color_list = rgb_hex_to_rgb_list(value) if len(color_list) > 3: self._white = color_list.pop() - self._rgb = color_list + self._hs = color_util.color_RGB_to_hs(*color_list) class MySensorsLightDimmer(MySensorsLight): diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index 9f049dd2e8a..d9312e6aadc 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -11,10 +11,9 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( Light, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_RGB_COLOR, - ATTR_RGB_COLOR) + SUPPORT_EFFECT, ATTR_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, + ATTR_HS_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN -from homeassistant.util.color import color_RGB_to_hsv, color_hsv_to_RGB REQUIREMENTS = ['python-mystrom==0.3.8'] @@ -24,7 +23,7 @@ DEFAULT_NAME = 'myStrom bulb' SUPPORT_MYSTROM = ( SUPPORT_BRIGHTNESS | SUPPORT_EFFECT | SUPPORT_FLASH | - SUPPORT_RGB_COLOR + SUPPORT_COLOR ) EFFECT_RAINBOW = 'rainbow' @@ -91,9 +90,9 @@ class MyStromLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" - return color_hsv_to_RGB(self._color_h, self._color_s, self._brightness) + return self._color_h, self._color_s @property def available(self) -> bool: @@ -117,12 +116,8 @@ class MyStromLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) - if ATTR_RGB_COLOR in kwargs: - # New color, compute from RGB - color_h, color_s, brightness = color_RGB_to_hsv( - *kwargs[ATTR_RGB_COLOR] - ) - brightness = brightness / 100 * 255 + if ATTR_HS_COLOR in kwargs: + color_h, color_s = kwargs[ATTR_HS_COLOR] elif ATTR_BRIGHTNESS in kwargs: # Brightness update, keep color color_h, color_s = self._color_h, self._color_s diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index ff526c4783d..2c44620caca 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -13,15 +13,15 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_RGB_COLOR, - ATTR_TRANSITION, ATTR_XY_COLOR, EFFECT_RANDOM, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_TRANSITION, + Light) from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.util.color import ( - color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, - color_xy_brightness_to_RGB) + color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin) +import homeassistant.util.color as color_util REQUIREMENTS = ['lightify==1.0.6.1'] @@ -35,8 +35,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | - SUPPORT_EFFECT | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_XY_COLOR) + SUPPORT_EFFECT | SUPPORT_COLOR | + SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -113,7 +113,7 @@ class Luminary(Light): self.update_lights = update_lights self._luminary = luminary self._brightness = None - self._rgb = [None] + self._hs = None self._name = None self._temperature = None self._state = False @@ -125,9 +125,9 @@ class Luminary(Light): return self._name @property - def rgb_color(self): - """Last RGB color value set.""" - return self._rgb + def hs_color(self): + """Last hs color value set.""" + return self._hs @property def color_temp(self): @@ -158,42 +158,24 @@ class Luminary(Light): """Turn the device on.""" if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) - _LOGGER.debug("turn_on requested transition time for light: " - "%s is: %s", self._name, transition) else: transition = 0 - _LOGGER.debug("turn_on requested transition time for light: " - "%s is: %s", self._name, transition) if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - _LOGGER.debug("turn_on requested brightness for light: %s is: %s ", - self._name, self._brightness) self._luminary.set_luminance( int(self._brightness / 2.55), transition) else: self._luminary.set_onoff(1) - if ATTR_RGB_COLOR in kwargs: - red, green, blue = kwargs[ATTR_RGB_COLOR] - _LOGGER.debug("turn_on requested ATTR_RGB_COLOR for light:" - " %s is: %s %s %s ", - self._name, red, green, blue) - self._luminary.set_rgb(red, green, blue, transition) - - if ATTR_XY_COLOR in kwargs: - x_mired, y_mired = kwargs[ATTR_XY_COLOR] - _LOGGER.debug("turn_on requested ATTR_XY_COLOR for light:" - " %s is: %s,%s", self._name, x_mired, y_mired) - red, green, blue = color_xy_brightness_to_RGB( - x_mired, y_mired, self._brightness) + if ATTR_HS_COLOR in kwargs: + red, green, blue = \ + color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._luminary.set_rgb(red, green, blue, transition) if ATTR_COLOR_TEMP in kwargs: color_t = kwargs[ATTR_COLOR_TEMP] kelvin = int(color_temperature_mired_to_kelvin(color_t)) - _LOGGER.debug("turn_on requested set_temperature for light: " - "%s: %s", self._name, kelvin) self._luminary.set_temperature(kelvin, transition) if ATTR_EFFECT in kwargs: @@ -202,23 +184,16 @@ class Luminary(Light): self._luminary.set_rgb( random.randrange(0, 255), random.randrange(0, 255), random.randrange(0, 255), transition) - _LOGGER.debug("turn_on requested random effect for light: " - "%s with transition %s", self._name, transition) self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - _LOGGER.debug("Attempting to turn off light: %s", self._name) if ATTR_TRANSITION in kwargs: transition = int(kwargs[ATTR_TRANSITION] * 10) - _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", self._name, transition) self._luminary.set_luminance(0, transition) else: transition = 0 - _LOGGER.debug("turn_off requested transition time for light:" - " %s is: %s ", self._name, transition) self._luminary.set_onoff(0) self.schedule_update_ha_state() @@ -240,7 +215,8 @@ class OsramLightifyLight(Luminary): """Update status of a light.""" super().update() self._state = self._luminary.on() - self._rgb = self._luminary.rgb() + rgb = self._luminary.rgb() + self._hs = color_util.color_RGB_to_hs(*rgb) o_temp = self._luminary.temp() if o_temp == 0: self._temperature = None @@ -270,7 +246,8 @@ class OsramLightifyGroup(Luminary): self._light_ids = self._luminary.lights() light = self._bridge.lights()[self._light_ids[0]] self._brightness = int(light.lum() * 2.55) - self._rgb = light.rgb() + rgb = light.rgb() + self._hs = color_util.color_RGB_to_hs(*rgb) o_temp = light.temp() if o_temp == 0: self._temperature = None diff --git a/homeassistant/components/light/piglow.py b/homeassistant/components/light/piglow.py index 40798810c0e..755cf9dca66 100644 --- a/homeassistant/components/light/piglow.py +++ b/homeassistant/components/light/piglow.py @@ -11,15 +11,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['piglow==1.2.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_PIGLOW = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'Piglow' @@ -50,7 +51,7 @@ class PiglowLight(Light): self._name = name self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -63,9 +64,9 @@ class PiglowLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Read back the color of the light.""" - return self._rgb_color + return self._hs_color @property def supported_features(self): @@ -93,15 +94,15 @@ class PiglowLight(Light): if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] - self._piglow.red(int(self._rgb_color[0] * percent_bright)) - self._piglow.green(int(self._rgb_color[1] * percent_bright)) - self._piglow.blue(int(self._rgb_color[2] * percent_bright)) - else: - self._piglow.all(self._brightness) + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] + + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._piglow.red(rgb[0]) + self._piglow.green(rgb[1]) + self._piglow.blue(rgb[2]) self._piglow.show() self._is_on = True self.schedule_update_ha_state() diff --git a/homeassistant/components/light/rpi_gpio_pwm.py b/homeassistant/components/light/rpi_gpio_pwm.py index 55b64bf8a74..9385c4bfb80 100644 --- a/homeassistant/components/light/rpi_gpio_pwm.py +++ b/homeassistant/components/light/rpi_gpio_pwm.py @@ -10,9 +10,10 @@ import voluptuous as vol from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['pwmled==1.2.1'] @@ -33,10 +34,10 @@ CONF_LED_TYPE_RGB = 'rgb' CONF_LED_TYPE_RGBW = 'rgbw' CONF_LED_TYPES = [CONF_LED_TYPE_SIMPLE, CONF_LED_TYPE_RGB, CONF_LED_TYPE_RGBW] -DEFAULT_COLOR = [255, 255, 255] +DEFAULT_COLOR = [0, 0] SUPPORT_SIMPLE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) -SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION) +SUPPORT_RGB_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_LEDS): vol.All(cv.ensure_list, [ @@ -169,7 +170,7 @@ class PwmRgbLed(PwmSimpleLed): self._color = DEFAULT_COLOR @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" return self._color @@ -180,8 +181,8 @@ class PwmRgbLed(PwmSimpleLed): def turn_on(self, **kwargs): """Turn on a LED.""" - if ATTR_RGB_COLOR in kwargs: - self._color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._color = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -209,4 +210,5 @@ def _from_hass_brightness(brightness): def _from_hass_color(color): """Convert Home Assistant RGB list to Color tuple.""" from pwmled import Color - return Color(*tuple(color)) + rgb = color_util.color_hs_to_RGB(*color) + return Color(*tuple(rgb)) diff --git a/homeassistant/components/light/sensehat.py b/homeassistant/components/light/sensehat.py index 6c5467f8c6d..6ab2592cedf 100644 --- a/homeassistant/components/light/sensehat.py +++ b/homeassistant/components/light/sensehat.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) from homeassistant.const import CONF_NAME +import homeassistant.util.color as color_util REQUIREMENTS = ['sense-hat==2.2.0'] _LOGGER = logging.getLogger(__name__) -SUPPORT_SENSEHAT = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_SENSEHAT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEFAULT_NAME = 'sensehat' @@ -49,7 +50,7 @@ class SenseHatLight(Light): self._name = name self._is_on = False self._brightness = 255 - self._rgb_color = [255, 255, 255] + self._hs_color = [0, 0] @property def name(self): @@ -62,12 +63,9 @@ class SenseHatLight(Light): return self._brightness @property - def rgb_color(self): - """Read back the color of the light. - - Returns [r, g, b] list with values in range of 0-255. - """ - return self._rgb_color + def hs_color(self): + """Read back the color of the light.""" + return self._hs_color @property def supported_features(self): @@ -93,14 +91,13 @@ class SenseHatLight(Light): """Instruct the light to turn on and set correct brightness & color.""" if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] - percent_bright = (self._brightness / 255) - if ATTR_RGB_COLOR in kwargs: - self._rgb_color = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[ATTR_HS_COLOR] - self._sensehat.clear(int(self._rgb_color[0] * percent_bright), - int(self._rgb_color[1] * percent_bright), - int(self._rgb_color[2] * percent_bright)) + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100) + self._sensehat.clear(*rgb) self._is_on = True self.schedule_update_ha_state() diff --git a/homeassistant/components/light/skybell.py b/homeassistant/components/light/skybell.py index 012190023fa..d32183f1468 100644 --- a/homeassistant/components/light/skybell.py +++ b/homeassistant/components/light/skybell.py @@ -8,10 +8,11 @@ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.skybell import ( DOMAIN as SKYBELL_DOMAIN, SkybellDevice) +import homeassistant.util.color as color_util DEPENDENCIES = ['skybell'] @@ -54,8 +55,9 @@ class SkybellLight(SkybellDevice, Light): def turn_on(self, **kwargs): """Turn on the light.""" - if ATTR_RGB_COLOR in kwargs: - self._device.led_rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self._device.led_rgb = rgb elif ATTR_BRIGHTNESS in kwargs: self._device.led_intensity = _to_skybell_level( kwargs[ATTR_BRIGHTNESS]) @@ -77,11 +79,11 @@ class SkybellLight(SkybellDevice, Light): return _to_hass_level(self._device.led_intensity) @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" - return self._device.led_rgb + return color_util.color_RGB_to_hs(*self._device.led_rgb) @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index c39748e4430..2079638f7f1 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -10,15 +10,16 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, + ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['tikteck==0.4'] _LOGGER = logging.getLogger(__name__) -SUPPORT_TIKTECK_LED = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_TIKTECK_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -57,7 +58,7 @@ class TikteckLight(Light): self._address = device['address'] self._password = device['password'] self._brightness = 255 - self._rgb = [255, 255, 255] + self._hs = [0, 0] self._state = False self.is_valid = True self._bulb = tikteck.tikteck( @@ -88,9 +89,9 @@ class TikteckLight(Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._rgb + return self._hs @property def supported_features(self): @@ -115,16 +116,17 @@ class TikteckLight(Light): """Turn the specified light on.""" self._state = True - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) - if rgb is not None: - self._rgb = rgb + if hs_color is not None: + self._hs = hs_color if brightness is not None: self._brightness = brightness - self.set_state(self._rgb[0], self._rgb[1], self._rgb[2], - self.brightness) + rgb = color_util.color_hs_to_RGB(*self._hs) + + self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() def turn_off(self, **kwargs): diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index f87d624b83a..0bbec010282 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -5,23 +5,20 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.tplink/ """ import logging -import colorsys import time import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired) -from typing import Tuple - REQUIREMENTS = ['pyHS100==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -56,22 +53,6 @@ def brightness_from_percentage(percent): return (percent*255.0)/100.0 -# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 -# pylint: disable=invalid-sequence-index -def rgb_to_hsv(rgb: Tuple[float, float, float]) -> Tuple[int, int, int]: - """Convert RGB tuple (values 0-255) to HSV (degrees, %, %).""" - hue, sat, value = colorsys.rgb_to_hsv(rgb[0]/255, rgb[1]/255, rgb[2]/255) - return int(hue * 360), int(sat * 100), int(value * 100) - - -# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 -# pylint: disable=invalid-sequence-index -def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: - """Convert HSV tuple (degrees, %, %) to RGB (values 0-255).""" - red, green, blue = colorsys.hsv_to_rgb(hsv[0]/360, hsv[1]/100, hsv[2]/100) - return int(red * 255), int(green * 255), int(blue * 255) - - class TPLinkSmartBulb(Light): """Representation of a TPLink Smart Bulb.""" @@ -83,7 +64,7 @@ class TPLinkSmartBulb(Light): self._available = True self._color_temp = None self._brightness = None - self._rgb = None + self._hs = None self._supported_features = 0 self._emeter_params = {} @@ -114,9 +95,10 @@ class TPLinkSmartBulb(Light): if ATTR_BRIGHTNESS in kwargs: brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) self.smartbulb.brightness = brightness_to_percentage(brightness) - if ATTR_RGB_COLOR in kwargs: - rgb = kwargs.get(ATTR_RGB_COLOR) - self.smartbulb.hsv = rgb_to_hsv(rgb) + if ATTR_HS_COLOR in kwargs: + hue, sat = kwargs.get(ATTR_HS_COLOR) + hsv = (hue, sat, 100) + self.smartbulb.hsv = hsv def turn_off(self, **kwargs): """Turn the light off.""" @@ -133,9 +115,9 @@ class TPLinkSmartBulb(Light): return self._brightness @property - def rgb_color(self): - """Return the color in RGB.""" - return self._rgb + def hs_color(self): + """Return the color.""" + return self._hs @property def is_on(self): @@ -168,8 +150,9 @@ class TPLinkSmartBulb(Light): self._color_temp = kelvin_to_mired( self.smartbulb.color_temp) - if self._supported_features & SUPPORT_RGB_COLOR: - self._rgb = hsv_to_rgb(self.smartbulb.hsv) + if self._supported_features & SUPPORT_COLOR: + hue, sat, _ = self.smartbulb.hsv + self._hs = (hue, sat) if self.smartbulb.has_emeter: self._emeter_params[ATTR_CURRENT_POWER_W] = '{:.1f}'.format( @@ -203,4 +186,4 @@ class TPLinkSmartBulb(Light): if self.smartbulb.is_variable_color_temp: self._supported_features += SUPPORT_COLOR_TEMP if self.smartbulb.is_color: - self._supported_features += SUPPORT_RGB_COLOR + self._supported_features += SUPPORT_COLOR diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index bb2fa44c15c..1851579a172 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -10,9 +10,9 @@ import logging from homeassistant.core import callback from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ @@ -157,7 +157,7 @@ class TradfriLight(Light): self._light_control = None self._light_data = None self._name = None - self._rgb_color = None + self._hs_color = None self._features = SUPPORTED_FEATURES self._temp_supported = False self._available = True @@ -237,9 +237,9 @@ class TradfriLight(Light): ) @property - def rgb_color(self): - """RGB color of the light.""" - return self._rgb_color + def hs_color(self): + """HS color of the light.""" + return self._hs_color @asyncio.coroutine def async_turn_off(self, **kwargs): @@ -252,12 +252,12 @@ class TradfriLight(Light): Instruct the light to turn on. After adding "self._light_data.hexcolor is not None" - for ATTR_RGB_COLOR, this also supports Philips Hue bulbs. + for ATTR_HS_COLOR, this also supports Philips Hue bulbs. """ - if ATTR_RGB_COLOR in kwargs and self._light_data.hex_color is not None: + if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) yield from self._api( - self._light.light_control.set_rgb_color( - *kwargs[ATTR_RGB_COLOR])) + self._light.light_control.set_rgb_color(*rgb)) elif ATTR_COLOR_TEMP in kwargs and \ self._light_data.hex_color is not None and \ @@ -309,17 +309,17 @@ class TradfriLight(Light): self._light_control = light.light_control self._light_data = light.light_control.lights[0] self._name = light.name - self._rgb_color = None + self._hs_color = None self._features = SUPPORTED_FEATURES if self._light.device_info.manufacturer == IKEA: if self._light_control.can_set_kelvin: self._features |= SUPPORT_COLOR_TEMP if self._light_control.can_set_color: - self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_COLOR else: if self._light_data.hex_color is not None: - self._features |= SUPPORT_RGB_COLOR + self._features |= SUPPORT_COLOR self._temp_supported = self._light.device_info.manufacturer \ in ALLOWED_TEMPERATURES @@ -328,7 +328,8 @@ class TradfriLight(Light): def _observe_update(self, tradfri_device): """Receive new state data for this light.""" self._refresh(tradfri_device) - self._rgb_color = color_util.rgb_hex_to_rgb_list( + rgb = color_util.rgb_hex_to_rgb_list( self._light_data.hex_color_inferred ) + self._hs_color = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/light/vera.py b/homeassistant/components/light/vera.py index 102ca814882..6b12e69341d 100644 --- a/homeassistant/components/light/vera.py +++ b/homeassistant/components/light/vera.py @@ -7,10 +7,11 @@ https://home-assistant.io/components/light.vera/ import logging from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ENTITY_ID_FORMAT, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) from homeassistant.components.vera import ( VERA_CONTROLLER, VERA_DEVICES, VeraDevice) +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,7 @@ class VeraLight(VeraDevice, Light): return self._brightness @property - def rgb_color(self): + def hs_color(self): """Return the color of the light.""" return self._color @@ -50,13 +51,14 @@ class VeraLight(VeraDevice, Light): def supported_features(self): """Flag supported features.""" if self._color: - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_BRIGHTNESS def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_RGB_COLOR in kwargs and self._color: - self.vera_device.set_color(kwargs[ATTR_RGB_COLOR]) + if ATTR_HS_COLOR in kwargs and self._color: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + self.vera_device.set_color(rgb) elif ATTR_BRIGHTNESS in kwargs and self.vera_device.is_dimmable: self.vera_device.set_brightness(kwargs[ATTR_BRIGHTNESS]) else: @@ -83,4 +85,5 @@ class VeraLight(VeraDevice, Light): # If it is dimmable, both functions exist. In case color # is not supported, it will return None self._brightness = self.vera_device.get_brightness() - self._color = self.vera_device.get_color() + rgb = self.vera_device.get_color() + self._color = color_util.color_RGB_to_hs(*rgb) if rgb else None diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 540c718b04d..d0575105235 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -9,12 +9,11 @@ import logging from datetime import timedelta import homeassistant.util as util -import homeassistant.util.color as color_util from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, - SUPPORT_TRANSITION, SUPPORT_XY_COLOR) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) from homeassistant.loader import get_component +import homeassistant.util.color as color_util DEPENDENCIES = ['wemo'] @@ -23,8 +22,8 @@ MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) _LOGGER = logging.getLogger(__name__) -SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR | - SUPPORT_TRANSITION | SUPPORT_XY_COLOR) +SUPPORT_WEMO = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR | + SUPPORT_TRANSITION) def setup_platform(hass, config, add_devices, discovery_info=None): @@ -89,9 +88,10 @@ class WemoLight(Light): return self.device.state.get('level', 255) @property - def xy_color(self): - """Return the XY color values of this light.""" - return self.device.state.get('color_xy') + def hs_color(self): + """Return the hs color values of this light.""" + xy_color = self.device.state.get('color_xy') + return color_util.color_xy_to_hs(*xy_color) if xy_color else None @property def color_temp(self): @@ -112,17 +112,11 @@ class WemoLight(Light): """Turn the light on.""" transitiontime = int(kwargs.get(ATTR_TRANSITION, 0)) - if ATTR_XY_COLOR in kwargs: - xycolor = kwargs[ATTR_XY_COLOR] - elif ATTR_RGB_COLOR in kwargs: - xycolor = color_util.color_RGB_to_xy( - *(int(val) for val in kwargs[ATTR_RGB_COLOR])) - kwargs.setdefault(ATTR_BRIGHTNESS, xycolor[2]) - else: - xycolor = None + hs_color = kwargs.get(ATTR_HS_COLOR) - if xycolor is not None: - self.device.set_color(xycolor, transition=transitiontime) + if hs_color is not None: + xy_color = color_util.color_hs_to_xy(*hs_color) + self.device.set_color(xy_color, transition=transitiontime) if ATTR_COLOR_TEMP in kwargs: colortemp = kwargs[ATTR_COLOR_TEMP] diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index e329fa04837..fd957f8f11d 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -8,8 +8,8 @@ import asyncio import colorsys from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_RGB_COLOR, Light) + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) from homeassistant.components.wink import DOMAIN, WinkDevice from homeassistant.util import color as color_util from homeassistant.util.color import \ @@ -17,7 +17,7 @@ from homeassistant.util.color import \ DEPENDENCIES = ['wink'] -SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_RGB_COLOR +SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR def setup_platform(hass, config, add_devices, discovery_info=None): @@ -72,11 +72,11 @@ class WinkLight(WinkDevice, Light): return r_value, g_value, b_value @property - def xy_color(self): - """Define current bulb color in CIE 1931 (XY) color space.""" + def hs_color(self): + """Define current bulb color.""" if not self.wink.supports_xy_color(): return None - return self.wink.color_xy() + return color_util.color_xy_to_hs(*self.wink.color_xy()) @property def color_temp(self): @@ -94,21 +94,17 @@ class WinkLight(WinkDevice, Light): def turn_on(self, **kwargs): """Turn the switch on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - rgb_color = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) - state_kwargs = { - } + state_kwargs = {} - if rgb_color: + if hs_color: if self.wink.supports_xy_color(): - xyb = color_util.color_RGB_to_xy(*rgb_color) - state_kwargs['color_xy'] = xyb[0], xyb[1] - state_kwargs['brightness'] = xyb[2] + xy_color = color_util.color_hs_to_xy(*hs_color) + state_kwargs['color_xy'] = xy_color if self.wink.supports_hue_saturation(): - hsv = colorsys.rgb_to_hsv( - rgb_color[0], rgb_color[1], rgb_color[2]) - state_kwargs['color_hue_saturation'] = hsv[0], hsv[1] + state_kwargs['color_hue_saturation'] = hs_color if color_temp_mired: state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index efe37d3d577..125e791829f 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -4,9 +4,10 @@ import struct import binascii from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) -from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_RGB_COLOR, +from homeassistant.components.light import (ATTR_BRIGHTNESS, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_RGB_COLOR, Light) + SUPPORT_COLOR, Light) +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): def __init__(self, device, name, xiaomi_hub): """Initialize the XiaomiGatewayLight.""" self._data_key = 'rgb' - self._rgb = (255, 255, 255) + self._hs = (0, 0) self._brightness = 180 XiaomiDevice.__init__(self, device, name, xiaomi_hub) @@ -64,7 +65,7 @@ class XiaomiGatewayLight(XiaomiDevice, Light): rgb = rgba[1:] self._brightness = int(255 * brightness / 100) - self._rgb = rgb + self._hs = color_util.color_RGB_to_hs(*rgb) self._state = True return True @@ -74,24 +75,25 @@ class XiaomiGatewayLight(XiaomiDevice, Light): return self._brightness @property - def rgb_color(self): - """Return the RBG color value.""" - return self._rgb + def hs_color(self): + """Return the hs color value.""" + return self._hs @property def supported_features(self): """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR def turn_on(self, **kwargs): """Turn the light on.""" - if ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs: + self._hs = kwargs[ATTR_HS_COLOR] if ATTR_BRIGHTNESS in kwargs: self._brightness = int(100 * kwargs[ATTR_BRIGHTNESS] / 255) - rgba = (self._brightness,) + self._rgb + rgb = color_util.color_hs_to_RGB(*self._hs) + rgba = (self._brightness,) + rgb rgbhex = binascii.hexlify(struct.pack('BBBB', *rgba)).decode("ASCII") rgbhex = int(rgbhex, 16) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index ca10d246ce8..585db950efc 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -5,26 +5,20 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.yeelight/ """ import logging -import colorsys -from typing import Tuple import voluptuous as vol from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, - color_temperature_kelvin_to_mired as kelvin_to_mired, - color_temperature_to_rgb, - color_RGB_to_xy, - color_xy_brightness_to_RGB) + color_temperature_kelvin_to_mired as kelvin_to_mired) from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, - ATTR_FLASH, ATTR_XY_COLOR, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_XY_COLOR, - SUPPORT_TRANSITION, - SUPPORT_COLOR_TEMP, SUPPORT_FLASH, SUPPORT_EFFECT, - Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, + ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, + SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['yeelight==0.4.0'] @@ -53,8 +47,7 @@ SUPPORT_YEELIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_FLASH) SUPPORT_YEELIGHT_RGB = (SUPPORT_YEELIGHT | - SUPPORT_RGB_COLOR | - SUPPORT_XY_COLOR | + SUPPORT_COLOR | SUPPORT_EFFECT | SUPPORT_COLOR_TEMP) @@ -98,14 +91,6 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_STOP] -# Travis-CI runs too old astroid https://github.com/PyCQA/pylint/issues/1212 -# pylint: disable=invalid-sequence-index -def hsv_to_rgb(hsv: Tuple[float, float, float]) -> Tuple[int, int, int]: - """Convert HSV tuple (degrees, %, %) to RGB (values 0-255).""" - red, green, blue = colorsys.hsv_to_rgb(hsv[0]/360, hsv[1]/100, hsv[2]/100) - return int(red * 255), int(green * 255), int(blue * 255) - - def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" def _wrap(self, *args, **kwargs): @@ -157,8 +142,7 @@ class YeelightLight(Light): self._brightness = None self._color_temp = None self._is_on = None - self._rgb = None - self._xy = None + self._hs = None @property def available(self) -> bool: @@ -209,38 +193,32 @@ class YeelightLight(Light): return kelvin_to_mired(YEELIGHT_RGB_MIN_KELVIN) return kelvin_to_mired(YEELIGHT_MIN_KELVIN) - def _get_rgb_from_properties(self): + def _get_hs_from_properties(self): rgb = self._properties.get('rgb', None) color_mode = self._properties.get('color_mode', None) if not rgb or not color_mode: - return rgb + return None color_mode = int(color_mode) if color_mode == 2: # color temperature temp_in_k = mired_to_kelvin(self._color_temp) - return color_temperature_to_rgb(temp_in_k) + return color_util.color_temperature_to_hs(temp_in_k) if color_mode == 3: # hsv hue = int(self._properties.get('hue')) sat = int(self._properties.get('sat')) - val = int(self._properties.get('bright')) - return hsv_to_rgb((hue, sat, val)) + return (hue / 360 * 65536, sat / 100 * 255) rgb = int(rgb) blue = rgb & 0xff green = (rgb >> 8) & 0xff red = (rgb >> 16) & 0xff - return red, green, blue + return color_util.color_RGB_to_hs(red, green, blue) @property - def rgb_color(self) -> tuple: + def hs_color(self) -> tuple: """Return the color property.""" - return self._rgb - - @property - def xy_color(self) -> tuple: - """Return the XY color value.""" - return self._xy + return self._hs @property def _properties(self) -> dict: @@ -288,13 +266,7 @@ class YeelightLight(Light): if temp_in_k: self._color_temp = kelvin_to_mired(int(temp_in_k)) - self._rgb = self._get_rgb_from_properties() - - if self._rgb: - xyb = color_RGB_to_xy(*self._rgb) - self._xy = (xyb[0], xyb[1]) - else: - self._xy = None + self._hs = self._get_hs_from_properties() self._available = True except yeelight.BulbException as ex: @@ -313,7 +285,7 @@ class YeelightLight(Light): @_cmd def set_rgb(self, rgb, duration) -> None: """Set bulb's color.""" - if rgb and self.supported_features & SUPPORT_RGB_COLOR: + if rgb and self.supported_features & SUPPORT_COLOR: _LOGGER.debug("Setting RGB: %s", rgb) self._bulb.set_rgb(rgb[0], rgb[1], rgb[2], duration=duration) @@ -349,7 +321,7 @@ class YeelightLight(Light): count = 1 duration = transition * 2 - red, green, blue = self.rgb_color + red, green, blue = color_util.color_hs_to_RGB(*self._hs) transitions = list() transitions.append( @@ -419,10 +391,10 @@ class YeelightLight(Light): import yeelight brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) + rgb = color_util.color_hs_to_RGB(*hs_color) if hs_color else None flash = kwargs.get(ATTR_FLASH) effect = kwargs.get(ATTR_EFFECT) - xy_color = kwargs.get(ATTR_XY_COLOR) duration = int(self.config[CONF_TRANSITION]) # in ms if ATTR_TRANSITION in kwargs: # passed kwarg overrides config @@ -440,9 +412,6 @@ class YeelightLight(Light): except yeelight.BulbException as ex: _LOGGER.error("Unable to turn on music mode," "consider disabling it: %s", ex) - if xy_color and brightness: - rgb = color_xy_brightness_to_RGB(xy_color[0], xy_color[1], - brightness) try: # values checked for none in methods diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py index 5f48e3a0a71..88f86063c13 100644 --- a/homeassistant/components/light/yeelightsunflower.py +++ b/homeassistant/components/light/yeelightsunflower.py @@ -10,15 +10,16 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, ATTR_RGB_COLOR, SUPPORT_RGB_COLOR, ATTR_BRIGHTNESS, + Light, ATTR_HS_COLOR, SUPPORT_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA) from homeassistant.const import CONF_HOST +import homeassistant.util.color as color_util REQUIREMENTS = ['yeelightsunflower==0.0.8'] _LOGGER = logging.getLogger(__name__) -SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR) +SUPPORT_YEELIGHT_SUNFLOWER = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string @@ -48,7 +49,7 @@ class SunflowerBulb(Light): self._available = light.available self._brightness = light.brightness self._is_on = light.is_on - self._rgb_color = light.rgb_color + self._hs_color = light.rgb_color @property def name(self): @@ -71,9 +72,9 @@ class SunflowerBulb(Light): return int(self._brightness / 100 * 255) @property - def rgb_color(self): + def hs_color(self): """Return the color property.""" - return self._rgb_color + return self._hs_color @property def supported_features(self): @@ -86,12 +87,12 @@ class SunflowerBulb(Light): if not kwargs: self._light.turn_on() else: - if ATTR_RGB_COLOR in kwargs and ATTR_BRIGHTNESS in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + if ATTR_HS_COLOR in kwargs and ATTR_BRIGHTNESS in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) self._light.set_all(rgb[0], rgb[1], rgb[2], bright) - elif ATTR_RGB_COLOR in kwargs: - rgb = kwargs[ATTR_RGB_COLOR] + elif ATTR_HS_COLOR in kwargs: + rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._light.set_rgb_color(rgb[0], rgb[1], rgb[2]) elif ATTR_BRIGHTNESS in kwargs: bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) @@ -107,4 +108,4 @@ class SunflowerBulb(Light): self._available = self._light.available self._brightness = self._light.brightness self._is_on = self._light.is_on - self._rgb_color = self._light.rgb_color + self._hs_color = color_util.color_RGB_to_hs(*self._light.rgb_color) diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index 7071c8c43bb..3c77f2d8449 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -10,15 +10,16 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( - ATTR_RGB_COLOR, ATTR_WHITE_VALUE, - SUPPORT_RGB_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util REQUIREMENTS = ['zengge==0.2'] _LOGGER = logging.getLogger(__name__) -SUPPORT_ZENGGE_LED = (SUPPORT_RGB_COLOR | SUPPORT_WHITE_VALUE) +SUPPORT_ZENGGE_LED = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -56,7 +57,8 @@ class ZenggeLight(Light): self.is_valid = True self._bulb = zengge.zengge(self._address) self._white = 0 - self._rgb = (0, 0, 0) + self._brightness = 0 + self._hs_color = (0, 0) self._state = False if self._bulb.connect() is False: self.is_valid = False @@ -80,9 +82,14 @@ class ZenggeLight(Light): return self._state @property - def rgb_color(self): + def brightness(self): + """Return the brightness property.""" + return self._brightness + + @property + def hs_color(self): """Return the color property.""" - return self._rgb + return self._hs_color @property def white_value(self): @@ -117,21 +124,29 @@ class ZenggeLight(Light): self._state = True self._bulb.on() - rgb = kwargs.get(ATTR_RGB_COLOR) + hs_color = kwargs.get(ATTR_HS_COLOR) white = kwargs.get(ATTR_WHITE_VALUE) + brightness = kwargs.get(ATTR_BRIGHTNESS) if white is not None: self._white = white - self._rgb = (0, 0, 0) + self._hs_color = (0, 0) - if rgb is not None: + if hs_color is not None: self._white = 0 - self._rgb = rgb + self._hs_color = hs_color + + if brightness is not None: + self._white = 0 + self._brightness = brightness if self._white != 0: self.set_white(self._white) else: - self.set_rgb(self._rgb[0], self._rgb[1], self._rgb[2]) + rgb = color_util.color_hsv_to_RGB( + self._hs_color[0], self._hs_color[1], + self._brightness / 255 * 100) + self.set_rgb(*rgb) def turn_off(self, **kwargs): """Turn the specified light off.""" @@ -140,6 +155,9 @@ class ZenggeLight(Light): def update(self): """Synchronise internal state with the actual light state.""" - self._rgb = self._bulb.get_colour() + rgb = self._bulb.get_colour() + hsv = color_util.color_RGB_to_hsv(*rgb) + self._hs_color = hsv[:2] + self._brightness = hsv[2] self._white = self._bulb.get_white() self._state = self._bulb.get_on() diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 7958fcabf13..68c5bcc2a29 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -7,8 +7,8 @@ at https://home-assistant.io/components/light.zha/ import logging from homeassistant.components import light, zha -from homeassistant.util.color import color_RGB_to_xy from homeassistant.const import STATE_UNKNOWN +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -57,7 +57,7 @@ class Light(zha.Entity, light.Light): super().__init__(**kwargs) self._supported_features = 0 self._color_temp = None - self._xy_color = None + self._hs_color = None self._brightness = None import zigpy.zcl.clusters as zcl_clusters @@ -71,9 +71,8 @@ class Light(zha.Entity, light.Light): self._supported_features |= light.SUPPORT_COLOR_TEMP if color_capabilities & CAPABILITIES_COLOR_XY: - self._supported_features |= light.SUPPORT_XY_COLOR - self._supported_features |= light.SUPPORT_RGB_COLOR - self._xy_color = (1.0, 1.0) + self._supported_features |= light.SUPPORT_COLOR + self._hs_color = (0, 0) @property def is_on(self) -> bool: @@ -92,17 +91,12 @@ class Light(zha.Entity, light.Light): temperature, duration) self._color_temp = temperature - if light.ATTR_XY_COLOR in kwargs: - self._xy_color = kwargs[light.ATTR_XY_COLOR] - elif light.ATTR_RGB_COLOR in kwargs: - xyb = color_RGB_to_xy( - *(int(val) for val in kwargs[light.ATTR_RGB_COLOR])) - self._xy_color = (xyb[0], xyb[1]) - self._brightness = xyb[2] - if light.ATTR_XY_COLOR in kwargs or light.ATTR_RGB_COLOR in kwargs: + if light.ATTR_HS_COLOR in kwargs: + self._hs_color = kwargs[light.ATTR_HS_COLOR] + xy_color = color_util.color_hs_to_xy(*self._hs_color) await self._endpoint.light_color.move_to_color( - int(self._xy_color[0] * 65535), - int(self._xy_color[1] * 65535), + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), duration, ) @@ -135,9 +129,9 @@ class Light(zha.Entity, light.Light): return self._brightness @property - def xy_color(self): - """Return the XY color value [float, float].""" - return self._xy_color + def hs_color(self): + """Return the hs color value [int, int].""" + return self._hs_color @property def color_temp(self): @@ -165,11 +159,12 @@ class Light(zha.Entity, light.Light): self._color_temp = result.get('color_temperature', self._color_temp) - if self._supported_features & light.SUPPORT_XY_COLOR: + if self._supported_features & light.SUPPORT_COLOR: result = await zha.safe_read(self._endpoint.light_color, ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: - self._xy_color = (result['current_x'], result['current_y']) + xy_color = (result['current_x'], result['current_y']) + self._hs_color = color_util.color_xy_to_hs(*xy_color) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 64c6530dd2b..286ce73f1ed 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -9,14 +9,14 @@ import logging # Because we do not compile openzwave on CI # pylint: disable=import-error from threading import Timer -from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, \ - ATTR_RGB_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, \ - SUPPORT_RGB_COLOR, SUPPORT_TRANSITION, DOMAIN, Light +from homeassistant.components.light import ( + ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, DOMAIN, Light) from homeassistant.components import zwave from homeassistant.components.zwave import async_setup_platform # noqa # pylint: disable=unused-import from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import color_temperature_mired_to_kelvin, \ - color_temperature_to_rgb, color_rgb_to_rgbw, color_rgbw_to_rgb +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -65,10 +65,11 @@ def brightness_state(value): return 0, STATE_OFF -def ct_to_rgb(temp): - """Convert color temperature (mireds) to RGB.""" +def ct_to_hs(temp): + """Convert color temperature (mireds) to hs.""" colorlist = list( - color_temperature_to_rgb(color_temperature_mired_to_kelvin(temp))) + color_util.color_temperature_to_hs( + color_util.color_temperature_mired_to_kelvin(temp))) return [int(val) for val in colorlist] @@ -209,8 +210,9 @@ class ZwaveColorLight(ZwaveDimmer): def __init__(self, values, refresh, delay): """Initialize the light.""" self._color_channels = None - self._rgb = None + self._hs = None self._ct = None + self._white = None super().__init__(values, refresh, delay) @@ -218,9 +220,12 @@ class ZwaveColorLight(ZwaveDimmer): """Call when a new value is added to this entity.""" super().value_added() - self._supported_features |= SUPPORT_RGB_COLOR + self._supported_features |= SUPPORT_COLOR if self._zw098: self._supported_features |= SUPPORT_COLOR_TEMP + elif self._color_channels is not None and self._color_channels & ( + COLOR_CHANNEL_WARM_WHITE | COLOR_CHANNEL_COLD_WHITE): + self._supported_features |= SUPPORT_WHITE_VALUE def update_properties(self): """Update internal properties based on zwave values.""" @@ -238,10 +243,11 @@ class ZwaveColorLight(ZwaveDimmer): data = self.values.color.data # RGB is always present in the openzwave color data string. - self._rgb = [ + rgb = [ int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] + self._hs = color_util.color_RGB_to_hs(*rgb) # Parse remaining color channels. Openzwave appends white channels # that are present. @@ -267,30 +273,35 @@ class ZwaveColorLight(ZwaveDimmer): if self._zw098: if warm_white > 0: self._ct = TEMP_WARM_HASS - self._rgb = ct_to_rgb(self._ct) + self._hs = ct_to_hs(self._ct) elif cold_white > 0: self._ct = TEMP_COLD_HASS - self._rgb = ct_to_rgb(self._ct) + self._hs = ct_to_hs(self._ct) else: # RGB color is being used. Just report midpoint. self._ct = TEMP_MID_HASS elif self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=warm_white)) + self._white = warm_white elif self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._rgb = list(color_rgbw_to_rgb(*self._rgb, w=cold_white)) + self._white = cold_white # If no rgb channels supported, report None. if not (self._color_channels & COLOR_CHANNEL_RED or self._color_channels & COLOR_CHANNEL_GREEN or self._color_channels & COLOR_CHANNEL_BLUE): - self._rgb = None + self._hs = None @property - def rgb_color(self): - """Return the rgb color.""" - return self._rgb + def hs_color(self): + """Return the hs color.""" + return self._hs + + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._white @property def color_temp(self): @@ -301,6 +312,9 @@ class ZwaveColorLight(ZwaveDimmer): """Turn the device on.""" rgbw = None + if ATTR_WHITE_VALUE in kwargs: + self._white = kwargs[ATTR_WHITE_VALUE] + if ATTR_COLOR_TEMP in kwargs: # Color temperature. With the AEOTEC ZW098 bulb, only two color # temperatures are supported. The warm and cold channel values @@ -313,19 +327,16 @@ class ZwaveColorLight(ZwaveDimmer): self._ct = TEMP_COLD_HASS rgbw = '#00000000ff' - elif ATTR_RGB_COLOR in kwargs: - self._rgb = kwargs[ATTR_RGB_COLOR] - if (not self._zw098 and ( - self._color_channels & COLOR_CHANNEL_WARM_WHITE or - self._color_channels & COLOR_CHANNEL_COLD_WHITE)): - rgbw = '#' - for colorval in color_rgb_to_rgbw(*self._rgb): - rgbw += format(colorval, '02x') - rgbw += '00' + elif ATTR_HS_COLOR in kwargs: + self._hs = kwargs[ATTR_HS_COLOR] + + if ATTR_WHITE_VALUE in kwargs or ATTR_HS_COLOR in kwargs: + rgbw = '#' + for colorval in color_util.color_hs_to_RGB(*self._hs): + rgbw += format(colorval, '02x') + if self._white is not None: + rgbw += format(self._white, '02x') + '00' else: - rgbw = '#' - for colorval in self._rgb: - rgbw += format(colorval, '02x') rgbw += '0000' if rgbw and self.values.color: diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index acc0c3ac423..e0bfdeee030 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import track_time_change from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy, + color_temperature_to_rgb, color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired) from homeassistant.util.dt import now as dt_now @@ -234,7 +234,7 @@ class FluxSwitch(SwitchDevice): else: temp = self._sunset_colortemp + temp_offset rgb = color_temperature_to_rgb(temp) - x_val, y_val, b_val = color_RGB_to_xy(*rgb) + x_val, y_val, b_val = color_RGB_to_xy_brightness(*rgb) brightness = self._brightness if self._brightness else b_val if self._disable_brightness_adjust: brightness = None diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 70863a0ab90..c2e4ac737e8 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -173,11 +173,18 @@ def color_name_to_rgb(color_name): return hex_value +# pylint: disable=invalid-name, invalid-sequence-index +def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float]: + """Convert from RGB color to XY color.""" + return color_RGB_to_xy_brightness(iR, iG, iB)[:2] + + # Taken from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # License: Code is given as is. Use at your own risk and discretion. # pylint: disable=invalid-name, invalid-sequence-index -def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: +def color_RGB_to_xy_brightness( + iR: int, iG: int, iB: int) -> Tuple[float, float, int]: """Convert from RGB color to XY color.""" if iR + iG + iB == 0: return 0.0, 0.0, 0 @@ -210,6 +217,11 @@ def color_RGB_to_xy(iR: int, iG: int, iB: int) -> Tuple[float, float, int]: return round(x, 3), round(y, 3), brightness +def color_xy_to_RGB(vX: float, vY: float) -> Tuple[int, int, int]: + """Convert from XY to a normalized RGB.""" + return color_xy_brightness_to_RGB(vX, vY, 255) + + # Converted to Python from Obj-C, original source from: # http://www.developers.meethue.com/documentation/color-conversions-rgb-xy # pylint: disable=invalid-sequence-index @@ -307,6 +319,12 @@ def color_RGB_to_hsv(iR: int, iG: int, iB: int) -> Tuple[float, float, float]: return round(fHSV[0]*360, 3), round(fHSV[1]*100, 3), round(fHSV[2]*100, 3) +# pylint: disable=invalid-sequence-index +def color_RGB_to_hs(iR: int, iG: int, iB: int) -> Tuple[float, float]: + """Convert an rgb color to its hs representation.""" + return color_RGB_to_hsv(iR, iG, iB)[:2] + + # pylint: disable=invalid-sequence-index def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: """Convert an hsv color into its rgb representation. @@ -320,12 +338,24 @@ def color_hsv_to_RGB(iH: float, iS: float, iV: float) -> Tuple[int, int, int]: # pylint: disable=invalid-sequence-index -def color_xy_to_hs(vX: float, vY: float) -> Tuple[int, int]: +def color_hs_to_RGB(iH: float, iS: float) -> Tuple[int, int, int]: + """Convert an hsv color into its rgb representation.""" + return color_hsv_to_RGB(iH, iS, 100) + + +# pylint: disable=invalid-sequence-index +def color_xy_to_hs(vX: float, vY: float) -> Tuple[float, float]: """Convert an xy color to its hs representation.""" - h, s, _ = color_RGB_to_hsv(*color_xy_brightness_to_RGB(vX, vY, 255)) + h, s, _ = color_RGB_to_hsv(*color_xy_to_RGB(vX, vY)) return (h, s) +# pylint: disable=invalid-sequence-index +def color_hs_to_xy(iH: float, iS: float) -> Tuple[float, float]: + """Convert an hs color to its xy representation.""" + return color_RGB_to_xy(*color_hs_to_RGB(iH, iS)) + + # pylint: disable=invalid-sequence-index def _match_max_scale(input_colors: Tuple[int, ...], output_colors: Tuple[int, ...]) -> Tuple[int, ...]: @@ -374,6 +404,11 @@ def rgb_hex_to_rgb_list(hex_string): len(hex_string) // 3)] +def color_temperature_to_hs(color_temperature_kelvin): + """Return an hs color from a color temperature in Kelvin.""" + return color_RGB_to_hs(*color_temperature_to_rgb(color_temperature_kelvin)) + + def color_temperature_to_rgb(color_temperature_kelvin): """ Return an RGB color from a color temperature in Kelvin. diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index a5375ba2662..8199652d09e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -950,42 +950,6 @@ def test_api_set_color_rgb(hass): assert msg['header']['name'] == 'Response' -@asyncio.coroutine -def test_api_set_color_xy(hass): - """Test api set color process.""" - request = get_new_request( - 'Alexa.ColorController', 'SetColor', 'light#test') - - # add payload - request['directive']['payload']['color'] = { - 'hue': '120', - 'saturation': '0.612', - 'brightness': '0.342', - } - - # setup test devices - hass.states.async_set( - 'light.test', 'off', { - 'friendly_name': "Test light", - 'supported_features': 64, - }) - - call_light = async_mock_service(hass, 'light', 'turn_on') - - msg = yield from smart_home.async_handle_message( - hass, DEFAULT_CONFIG, request) - yield from hass.async_block_till_done() - - assert 'event' in msg - msg = msg['event'] - - assert len(call_light) == 1 - assert call_light[0].data['entity_id'] == 'light.test' - assert call_light[0].data['xy_color'] == (0.23, 0.585) - assert call_light[0].data['brightness'] == 18 - assert msg['header']['name'] == 'Response' - - @asyncio.coroutine def test_api_set_color_temperature(hass): """Test api set color temperature process.""" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24d74afa6da..6523c22fee1 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -21,7 +21,7 @@ async def test_sync_message(hass): light = DemoLight( None, 'Demo Light', state=False, - rgb=[237, 224, 33] + hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' @@ -88,7 +88,7 @@ async def test_query_message(hass): light = DemoLight( None, 'Demo Light', state=False, - rgb=[237, 224, 33] + hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' @@ -97,7 +97,7 @@ async def test_query_message(hass): light2 = DemoLight( None, 'Another Light', state=True, - rgb=[237, 224, 33], + hs_color=(180, 75), ct=400, brightness=78, ) @@ -137,7 +137,7 @@ async def test_query_message(hass): 'online': True, 'brightness': 30, 'color': { - 'spectrumRGB': 15589409, + 'spectrumRGB': 4194303, 'temperature': 2500, } }, @@ -197,7 +197,7 @@ async def test_execute(hass): "online": True, 'brightness': 20, 'color': { - 'spectrumRGB': 15589409, + 'spectrumRGB': 16773155, 'temperature': 2631, }, } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 4ffb273662e..e6336e05246 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -361,12 +361,10 @@ async def test_color_spectrum_light(hass): """Test ColorSpectrum trait support for light domain.""" assert not trait.ColorSpectrumTrait.supported(light.DOMAIN, 0) assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_RGB_COLOR) - assert trait.ColorSpectrumTrait.supported(light.DOMAIN, - light.SUPPORT_XY_COLOR) + light.SUPPORT_COLOR) trt = trait.ColorSpectrumTrait(State('light.bla', STATE_ON, { - light.ATTR_RGB_COLOR: [255, 10, 10] + light.ATTR_HS_COLOR: (0, 94), })) assert trt.sync_attributes() == { @@ -375,7 +373,7 @@ async def test_color_spectrum_light(hass): assert trt.query_attributes() == { 'color': { - 'spectrumRGB': 16714250 + 'spectrumRGB': 16715535 } } @@ -399,7 +397,7 @@ async def test_color_spectrum_light(hass): assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'light.bla', - light.ATTR_RGB_COLOR: [16, 16, 255] + light.ATTR_HS_COLOR: (240, 93.725), } diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 83456f459cd..b4d4d5a5945 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,8 +4,8 @@ import unittest from homeassistant.core import callback from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_RGB_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR) + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, @@ -108,16 +108,16 @@ class TestHomekitLights(unittest.TestCase): """Test light with rgb_color.""" entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_RGB_COLOR, - ATTR_RGB_COLOR: (120, 20, 255)}) + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, + ATTR_HS_COLOR: (260, 90)}) acc = Light(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) acc.run() self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 265.532) - self.assertEqual(acc.char_saturation.value, 92.157) + self.assertEqual(acc.char_hue.value, 260) + self.assertEqual(acc.char_saturation.value, 90) # Set from HomeKit acc.char_hue.set_value(145) @@ -129,4 +129,4 @@ class TestHomekitLights(unittest.TestCase): self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (63, 255, 143)}) + ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 8a7d648e6f2..ff984aff221 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -33,19 +33,22 @@ class TestDemoLight(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((.4, .6), state.attributes.get(light.ATTR_XY_COLOR)) + self.assertEqual((0.378, 0.574), state.attributes.get( + light.ATTR_XY_COLOR)) self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( - (76, 95, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( - self.hass, ENTITY_LIGHT, rgb_color=(251, 252, 253), + self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), white_value=254) self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(254, state.attributes.get(light.ATTR_WHITE_VALUE)) self.assertEqual( - (251, 252, 253), state.attributes.get(light.ATTR_RGB_COLOR)) + (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) + self.assertEqual( + (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py index 3c94fa2af3e..26b949720d9 100644 --- a/tests/components/light/test_group.py +++ b/tests/components/light/test_group.py @@ -20,8 +20,7 @@ async def test_default_state(hass): assert state.state == 'unavailable' assert state.attributes['supported_features'] == 0 assert state.attributes.get('brightness') is None - assert state.attributes.get('rgb_color') is None - assert state.attributes.get('xy_color') is None + assert state.attributes.get('hs_color') is None assert state.attributes.get('color_temp') is None assert state.attributes.get('white_value') is None assert state.attributes.get('effect_list') is None @@ -85,61 +84,32 @@ async def test_brightness(hass): assert state.attributes['brightness'] == 100 -async def test_xy_color(hass): - """Test XY reporting.""" - await async_setup_component(hass, 'light', {'light': { - 'platform': 'group', 'entities': ['light.test1', 'light.test2'] - }}) - - hass.states.async_set('light.test1', 'on', - {'xy_color': (1.0, 1.0), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['supported_features'] == 64 - assert state.attributes['xy_color'] == (1.0, 1.0) - - hass.states.async_set('light.test2', 'on', - {'xy_color': (0.5, 0.5), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['xy_color'] == (0.75, 0.75) - - hass.states.async_set('light.test1', 'off', - {'xy_color': (1.0, 1.0), 'supported_features': 64}) - await hass.async_block_till_done() - state = hass.states.get('light.light_group') - assert state.state == 'on' - assert state.attributes['xy_color'] == (0.5, 0.5) - - -async def test_rgb_color(hass): +async def test_color(hass): """Test RGB reporting.""" await async_setup_component(hass, 'light', {'light': { 'platform': 'group', 'entities': ['light.test1', 'light.test2'] }}) hass.states.async_set('light.test1', 'on', - {'rgb_color': (255, 0, 0), 'supported_features': 16}) + {'hs_color': (0, 100), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') assert state.state == 'on' assert state.attributes['supported_features'] == 16 - assert state.attributes['rgb_color'] == (255, 0, 0) + assert state.attributes['hs_color'] == (0, 100) hass.states.async_set('light.test2', 'on', - {'rgb_color': (255, 255, 255), + {'hs_color': (0, 50), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') - assert state.attributes['rgb_color'] == (255, 127, 127) + assert state.attributes['hs_color'] == (0, 75) hass.states.async_set('light.test1', 'off', - {'rgb_color': (255, 0, 0), 'supported_features': 16}) + {'hs_color': (0, 0), 'supported_features': 16}) await hass.async_block_till_done() state = hass.states.get('light.light_group') - assert state.attributes['rgb_color'] == (255, 255, 255) + assert state.attributes['hs_color'] == (0, 50) async def test_white_value(hass): @@ -413,5 +383,7 @@ async def test_invalid_service_calls(hass): } await grouped_light.async_turn_on(**data) data['entity_id'] = ['light.test1', 'light.test2'] + data.pop('rgb_color') + data.pop('xy_color') mock_call.assert_called_once_with('light', 'turn_on', data, blocking=True) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index d35321b4479..4e8fad261bd 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -188,23 +188,25 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_TRANSITION: 10, - light.ATTR_BRIGHTNESS: 20, - light.ATTR_RGB_COLOR: (0, 0, 255)}, - data) + self.assertEqual({ + light.ATTR_TRANSITION: 10, + light.ATTR_BRIGHTNESS: 20, + light.ATTR_HS_COLOR: (240, 100), + }, data) _, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 255}, - data) + self.assertEqual({ + light.ATTR_HS_COLOR: (0, 0), + light.ATTR_WHITE_VALUE: 255, + }, data) _, data = dev3.last_call('turn_on') - self.assertEqual({light.ATTR_XY_COLOR: (.4, .6)}, data) + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + }, data) # One of the light profiles - prof_name, prof_x, prof_y, prof_bri = 'relax', 0.5119, 0.4147, 144 + prof_name, prof_h, prof_s, prof_bri = 'relax', 35.932, 69.412, 144 # Test light profiles light.turn_on(self.hass, dev1.entity_id, profile=prof_name) @@ -216,16 +218,16 @@ class TestLight(unittest.TestCase): self.hass.block_till_done() _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_BRIGHTNESS: prof_bri, - light.ATTR_XY_COLOR: (prof_x, prof_y)}, - data) + self.assertEqual({ + light.ATTR_BRIGHTNESS: prof_bri, + light.ATTR_HS_COLOR: (prof_h, prof_s), + }, data) _, data = dev2.last_call('turn_on') - self.assertEqual( - {light.ATTR_BRIGHTNESS: 100, - light.ATTR_XY_COLOR: (.5119, .4147)}, - data) + self.assertEqual({ + light.ATTR_BRIGHTNESS: 100, + light.ATTR_HS_COLOR: (prof_h, prof_s), + }, data) # Test bad data light.turn_on(self.hass) @@ -301,15 +303,16 @@ class TestLight(unittest.TestCase): _, data = dev1.last_call('turn_on') - self.assertEqual( - {light.ATTR_XY_COLOR: (.4, .6), light.ATTR_BRIGHTNESS: 100}, - data) + self.assertEqual({ + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100 + }, data) async def test_intent_set_color(hass): """Test the set color intent.""" hass.states.async_set('light.hello_2', 'off', { - ATTR_SUPPORTED_FEATURES: light.SUPPORT_RGB_COLOR + ATTR_SUPPORTED_FEATURES: light.SUPPORT_COLOR }) hass.states.async_set('switch.hello', 'off') calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) @@ -364,7 +367,7 @@ async def test_intent_set_color_and_brightness(hass): """Test the set color intent.""" hass.states.async_set('light.hello_2', 'off', { ATTR_SUPPORTED_FEATURES: ( - light.SUPPORT_RGB_COLOR | light.SUPPORT_BRIGHTNESS) + light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS) }) hass.states.async_set('switch.hello', 'off') calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 6c56564df69..71fe77ef6be 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -250,12 +250,12 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) self.assertEqual(255, state.attributes.get('white_value')) - self.assertEqual([1, 1], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb/status', '0') self.hass.block_till_done() @@ -303,7 +303,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], + self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) fire_mqtt_message(self.hass, 'test_light_rgb/xy/status', @@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([0.675, 0.322], + self.assertEqual((0.652, 0.343), light_state.attributes.get('xy_color')) def test_brightness_controlling_scale(self): @@ -458,11 +458,11 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) self.assertEqual(50, state.attributes.get('brightness')) - self.assertEqual([1, 2, 3], state.attributes.get('rgb_color')) + self.assertEqual((0, 123, 255), state.attributes.get('rgb_color')) self.assertEqual(300, state.attributes.get('color_temp')) self.assertEqual('rainbow', state.attributes.get('effect')) self.assertEqual(75, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + self.assertEqual((0.14, 0.131), state.attributes.get('xy_color')) def test_sending_mqtt_commands_and_optimistic(self): \ # pylint: disable=invalid-name @@ -516,18 +516,18 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), - mock.call('test_light_rgb/rgb/set', '75,75,75', 2, False), + mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.123,0.123', 2, False), + mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.123, 0.123), state.attributes['xy_color']) + self.assertEqual((0.32, 0.336), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -554,12 +554,12 @@ class TestLightMQTT(unittest.TestCase): self.mock_publish.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 0, False), - mock.call('test_light_rgb/rgb/set', '#ff8040', 0, False), + mock.call('test_light_rgb/rgb/set', '#ff803f', 0, False), ], any_order=True) state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((255, 128, 64), state.attributes['rgb_color']) + self.assertEqual((255, 128, 63), state.attributes['rgb_color']) def test_show_brightness_if_only_command_topic(self): """Test the brightness if only a command topic is present.""" @@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([1, 1], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) def test_on_command_first(self): """Test on command being sent before brightness.""" @@ -799,7 +799,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() self.mock_publish.async_publish.assert_has_calls([ - mock.call('test_light/rgb', '75,75,75', 0, False), + mock.call('test_light/rgb', '50,50,50', 0, False), mock.call('test_light/bright', 50, 0, False) ], any_order=True) diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index ba306a81a34..cfeffc93108 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -180,7 +180,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_OFF, state.state) - self.assertEqual(255, state.attributes.get(ATTR_SUPPORTED_FEATURES)) + self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertIsNone(state.attributes.get('rgb_color')) self.assertIsNone(state.attributes.get('brightness')) self.assertIsNone(state.attributes.get('color_temp')) @@ -192,8 +192,7 @@ class TestLightMQTTJSON(unittest.TestCase): # Turn on the light, full white fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON",' - '"color":{"r":255,"g":255,"b":255,' - '"x":0.123,"y":0.123},' + '"color":{"r":255,"g":255,"b":255},' '"brightness":255,' '"color_temp":155,' '"effect":"colorloop",' @@ -202,12 +201,12 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(155, state.attributes.get('color_temp')) self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual([0.123, 0.123], state.attributes.get('xy_color')) + self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') @@ -232,7 +231,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([125, 125, 125], + self.assertEqual((255, 255, 255), light_state.attributes.get('rgb_color')) fire_mqtt_message(self.hass, 'test_light_rgb', @@ -241,7 +240,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([0.135, 0.135], + self.assertEqual((0.141, 0.14), light_state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb', @@ -503,7 +502,7 @@ class TestLightMQTTJSON(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(255, state.attributes.get('white_value')) @@ -516,7 +515,7 @@ class TestLightMQTTJSON(unittest.TestCase): # Color should not have changed state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) # Bad brightness values fire_mqtt_message(self.hass, 'test_light_rgb', diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 5a01aa15fa2..90d68dd10d2 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -151,7 +151,7 @@ class TestLightMQTTTemplate(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual([255, 128, 64], state.attributes.get('rgb_color')) + self.assertEqual((255, 128, 63), state.attributes.get('rgb_color')) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(145, state.attributes.get('color_temp')) self.assertEqual(123, state.attributes.get('white_value')) @@ -185,7 +185,8 @@ class TestLightMQTTTemplate(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual([41, 42, 43], light_state.attributes.get('rgb_color')) + self.assertEqual((243, 249, 255), + light_state.attributes.get('rgb_color')) # change the white value fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,134') @@ -254,7 +255,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.hass.block_till_done() self.mock_publish.async_publish.assert_called_once_with( - 'test_light_rgb/set', 'on,50,,,75-75-75', 2, False) + 'test_light_rgb/set', 'on,50,,,50-50-50', 2, False) self.mock_publish.async_publish.reset_mock() # turn on the light with color temp and white val @@ -267,7 +268,7 @@ class TestLightMQTTTemplate(unittest.TestCase): # check the state state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((75, 75, 75), state.attributes['rgb_color']) + self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(200, state.attributes['color_temp']) self.assertEqual(139, state.attributes['white_value']) @@ -387,7 +388,7 @@ class TestLightMQTTTemplate(unittest.TestCase): self.assertEqual(STATE_ON, state.state) self.assertEqual(255, state.attributes.get('brightness')) self.assertEqual(215, state.attributes.get('color_temp')) - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) self.assertEqual(222, state.attributes.get('white_value')) self.assertEqual('rainbow', state.attributes.get('effect')) @@ -421,7 +422,7 @@ class TestLightMQTTTemplate(unittest.TestCase): # color should not have changed state = self.hass.states.get('light.test') - self.assertEqual([255, 255, 255], state.attributes.get('rgb_color')) + self.assertEqual((255, 255, 255), state.attributes.get('rgb_color')) # bad white value values fire_mqtt_message(self.hass, 'test_light_rgb', 'on,,,off,255-255-255') diff --git a/tests/components/light/test_zwave.py b/tests/components/light/test_zwave.py index b925b74a7f0..4966b161360 100644 --- a/tests/components/light/test_zwave.py +++ b/tests/components/light/test_zwave.py @@ -4,9 +4,9 @@ from unittest.mock import patch, MagicMock import homeassistant.components.zwave from homeassistant.components.zwave import const from homeassistant.components.light import ( - zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_RGB_COLOR, - SUPPORT_COLOR_TEMP) + zwave, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR, ATTR_WHITE_VALUE, + SUPPORT_COLOR_TEMP, SUPPORT_WHITE_VALUE) from tests.mock.zwave import ( MockNode, MockValue, MockEntityValues, value_changed) @@ -42,7 +42,7 @@ def test_get_device_detects_colorlight(mock_openzwave): device = zwave.get_device(node=node, values=values, node_config={}) assert isinstance(device, zwave.ZwaveColorLight) - assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR + assert device.supported_features == SUPPORT_BRIGHTNESS | SUPPORT_COLOR def test_get_device_detects_zw098(mock_openzwave): @@ -54,7 +54,23 @@ def test_get_device_detects_zw098(mock_openzwave): device = zwave.get_device(node=node, values=values, node_config={}) assert isinstance(device, zwave.ZwaveColorLight) assert device.supported_features == ( - SUPPORT_BRIGHTNESS | SUPPORT_RGB_COLOR | SUPPORT_COLOR_TEMP) + SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP) + + +def test_get_device_detects_rgbw_light(mock_openzwave): + """Test get_device returns a color light.""" + node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) + value = MockValue(data=0, node=node) + color = MockValue(data='#0000000000', node=node) + color_channels = MockValue(data=0x1d, node=node) + values = MockLightValues( + primary=value, color=color, color_channels=color_channels) + + device = zwave.get_device(node=node, values=values, node_config={}) + device.value_added() + assert isinstance(device, zwave.ZwaveColorLight) + assert device.supported_features == ( + SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE) def test_dimmer_turn_on(mock_openzwave): @@ -203,7 +219,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert device.brightness == 118 -def test_set_rgb_color(mock_openzwave): +def test_set_hs_color(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -216,12 +232,12 @@ def test_set_rgb_color(mock_openzwave): assert color.data == '#0000000000' - device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) + device.turn_on(**{ATTR_HS_COLOR: (30, 50)}) - assert color.data == '#c896640000' + assert color.data == '#ffbf7f0000' -def test_set_rgbw_color(mock_openzwave): +def test_set_white_value(mock_openzwave): """Test setting zwave light color.""" node = MockNode(command_classes=[const.COMMAND_CLASS_SWITCH_COLOR]) value = MockValue(data=0, node=node) @@ -234,9 +250,9 @@ def test_set_rgbw_color(mock_openzwave): assert color.data == '#0000000000' - device.turn_on(**{ATTR_RGB_COLOR: (200, 150, 100)}) + device.turn_on(**{ATTR_WHITE_VALUE: 200}) - assert color.data == '#c86400c800' + assert color.data == '#ffffffc800' def test_zw098_set_color_temp(mock_openzwave): @@ -273,7 +289,7 @@ def test_rgb_not_supported(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_no_color_value(mock_openzwave): @@ -283,7 +299,7 @@ def test_no_color_value(mock_openzwave): values = MockLightValues(primary=value) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_no_color_channels_value(mock_openzwave): @@ -294,7 +310,7 @@ def test_no_color_channels_value(mock_openzwave): values = MockLightValues(primary=value, color=color) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color is None + assert device.hs_color is None def test_rgb_value_changed(mock_openzwave): @@ -308,12 +324,12 @@ def test_rgb_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) - color.data = '#c896640000' + color.data = '#ffbf800000' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (29.764, 49.804) def test_rgbww_value_changed(mock_openzwave): @@ -327,12 +343,14 @@ def test_rgbww_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) + assert device.white_value == 0 color.data = '#c86400c800' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (30, 100) + assert device.white_value == 200 def test_rgbcw_value_changed(mock_openzwave): @@ -346,12 +364,14 @@ def test_rgbcw_value_changed(mock_openzwave): color_channels=color_channels) device = zwave.get_device(node=node, values=values, node_config={}) - assert device.rgb_color == [0, 0, 0] + assert device.hs_color == (0, 0) + assert device.white_value == 0 color.data = '#c86400c800' value_changed(color) - assert device.rgb_color == [200, 150, 100] + assert device.hs_color == (30, 100) + assert device.white_value == 200 def test_ct_value_changed(mock_openzwave): diff --git a/tests/testing_config/.remember_the_milk.conf b/tests/testing_config/.remember_the_milk.conf new file mode 100644 index 00000000000..272ac0903bd --- /dev/null +++ b/tests/testing_config/.remember_the_milk.conf @@ -0,0 +1 @@ +{"myprofile": {"id_map": {}}} \ No newline at end of file diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index fafe88eecbe..71625dfdf93 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -1,5 +1,5 @@ """ -Provide a mock switch platform. +Provide a mock light platform. Call init before using it in your tests to ensure clean test data. """ diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 86d303c23b7..b64cf0acf80 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -10,26 +10,52 @@ class TestColorUtil(unittest.TestCase): """Test color util methods.""" # pylint: disable=invalid-name - def test_color_RGB_to_xy(self): - """Test color_RGB_to_xy.""" - self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy(0, 0, 0)) + def test_color_RGB_to_xy_brightness(self): + """Test color_RGB_to_xy_brightness.""" + self.assertEqual((0, 0, 0), + color_util.color_RGB_to_xy_brightness(0, 0, 0)) self.assertEqual((0.32, 0.336, 255), - color_util.color_RGB_to_xy(255, 255, 255)) + color_util.color_RGB_to_xy_brightness(255, 255, 255)) self.assertEqual((0.136, 0.04, 12), - color_util.color_RGB_to_xy(0, 0, 255)) + color_util.color_RGB_to_xy_brightness(0, 0, 255)) self.assertEqual((0.172, 0.747, 170), - color_util.color_RGB_to_xy(0, 255, 0)) + color_util.color_RGB_to_xy_brightness(0, 255, 0)) self.assertEqual((0.679, 0.321, 80), + color_util.color_RGB_to_xy_brightness(255, 0, 0)) + + self.assertEqual((0.679, 0.321, 17), + color_util.color_RGB_to_xy_brightness(128, 0, 0)) + + def test_color_RGB_to_xy(self): + """Test color_RGB_to_xy.""" + self.assertEqual((0, 0), + color_util.color_RGB_to_xy(0, 0, 0)) + self.assertEqual((0.32, 0.336), + color_util.color_RGB_to_xy(255, 255, 255)) + + self.assertEqual((0.136, 0.04), + color_util.color_RGB_to_xy(0, 0, 255)) + + self.assertEqual((0.172, 0.747), + color_util.color_RGB_to_xy(0, 255, 0)) + + self.assertEqual((0.679, 0.321), color_util.color_RGB_to_xy(255, 0, 0)) + self.assertEqual((0.679, 0.321), + color_util.color_RGB_to_xy(128, 0, 0)) + def test_color_xy_brightness_to_RGB(self): - """Test color_RGB_to_xy.""" + """Test color_xy_brightness_to_RGB.""" self.assertEqual((0, 0, 0), color_util.color_xy_brightness_to_RGB(1, 1, 0)) + self.assertEqual((194, 186, 169), + color_util.color_xy_brightness_to_RGB(.35, .35, 128)) + self.assertEqual((255, 243, 222), color_util.color_xy_brightness_to_RGB(.35, .35, 255)) @@ -42,6 +68,20 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0, 63, 255), color_util.color_xy_brightness_to_RGB(0, 0, 255)) + def test_color_xy_to_RGB(self): + """Test color_xy_to_RGB.""" + self.assertEqual((255, 243, 222), + color_util.color_xy_to_RGB(.35, .35)) + + self.assertEqual((255, 0, 60), + color_util.color_xy_to_RGB(1, 0)) + + self.assertEqual((0, 255, 0), + color_util.color_xy_to_RGB(0, 1)) + + self.assertEqual((0, 63, 255), + color_util.color_xy_to_RGB(0, 0)) + def test_color_RGB_to_hsv(self): """Test color_RGB_to_hsv.""" self.assertEqual((0, 0, 0), @@ -110,6 +150,23 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((225.176, 100), color_util.color_xy_to_hs(0, 0)) + def test_color_hs_to_xy(self): + """Test color_hs_to_xy.""" + self.assertEqual((0.151, 0.343), + color_util.color_hs_to_xy(180, 100)) + + self.assertEqual((0.352, 0.329), + color_util.color_hs_to_xy(350, 12.5)) + + self.assertEqual((0.228, 0.476), + color_util.color_hs_to_xy(140, 50)) + + self.assertEqual((0.465, 0.33), + color_util.color_hs_to_xy(0, 40)) + + self.assertEqual((0.32, 0.336), + color_util.color_hs_to_xy(360, 0)) + def test_rgb_hex_to_rgb_list(self): """Test rgb_hex_to_rgb_list.""" self.assertEqual([255, 255, 255], From 49683181d161fa0cf079a86ba493101a56873ae0 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Mon, 19 Mar 2018 03:42:23 +0000 Subject: [PATCH 083/924] Superfluous None (#13326) --- homeassistant/components/sensor/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index aad7fec26a0..3faf51a5f47 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -74,7 +74,7 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_TIME_SMA, vol.Optional(CONF_TIME_SMA_TYPE, default=TIME_SMA_LAST): vol.In( - [None, TIME_SMA_LAST]), + [TIME_SMA_LAST]), vol.Required(CONF_FILTER_WINDOW_SIZE): vol.All(cv.time_period, cv.positive_timedelta) From 947218d51c94566feeb32c6f549493fa07683252 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 19 Mar 2018 10:47:10 +0100 Subject: [PATCH 084/924] pytest 3.4.0 cache gitignore (#13308) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b3774b06bc8..33a1f4f9a4b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ Icon *.iml # pytest -.cache +.pytest_cache # GITHUB Proposed Python stuff: *.py[cod] From 2bc7e587808043be82514077ac3215e10f5b6d8b Mon Sep 17 00:00:00 2001 From: Qxlkdr <33372537+Qxlkdr@users.noreply.github.com> Date: Mon, 19 Mar 2018 21:38:07 +0100 Subject: [PATCH 085/924] Add trafikverket_weatherstation sensor platform (#12115) * Create trafikverket_weatherstation.py Created PR 12111 but due to permission issue I'm creating a new fork and PR. * Added dot Added dot to the first (second) row of the file, after the description. * Corrections based on feedback Done: - Run flake8 before this commit - Fixed invalid variables - Shortened the xml variable/query via if statement (air_vs_road) - Moved imports if update() to top of the file - Imported CONF_API_KEY and CONF_TYPE - Updated documentation (api_key): home-assistant/home-assistant.github.io#4562 Actions left: - Error handling - Request timeout - Add sensor (file) to .coveragerc * Multiple corrections Done: - Executed pylint and flake8 tests before commit - Fixed import order - Implemented request timeout - Used variable air_vs_road in the return as well Actions left: - Error handling - Add sensor (file) to .coveragerc * Error handling Done: - Error handling for network - Error handling for JSON response * Added trafikverket_weatherstation.py Added trafikverket_weatherstation.py in correct order. * Road as default Changed if statement to check for 'road' which means it will always defaulting to 'air' in other cases. Even if it will only accept 'air' and 'road' from the PLATFORM_SCHEMA. * Updated variable names Updated variable names to be more understandable as requested by @MartinHjelmare * Standard Libraries Grouped Standard Libraries * Return None Changed return None to only return as suggested. --- .coveragerc | 1 + .../sensor/trafikverket_weatherstation.py | 124 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 homeassistant/components/sensor/trafikverket_weatherstation.py diff --git a/.coveragerc b/.coveragerc index 40fccd5e921..1dcde0ded14 100644 --- a/.coveragerc +++ b/.coveragerc @@ -665,6 +665,7 @@ omit = homeassistant/components/sensor/tibber.py homeassistant/components/sensor/time_date.py homeassistant/components/sensor/torque.py + homeassistant/components/sensor/trafikverket_weatherstation.py homeassistant/components/sensor/transmission.py homeassistant/components/sensor/travisci.py homeassistant/components/sensor/twitch.py diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py new file mode 100644 index 00000000000..fba16c27c7e --- /dev/null +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -0,0 +1,124 @@ +""" +Weather information for air and road temperature, provided by Trafikverket. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.trafikverket_weatherstation/ +""" +import json +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_API_KEY, CONF_TYPE) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +CONF_ATTRIBUTION = "Data provided by Trafikverket API" + +CONF_STATION = 'station' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +SCAN_INTERVAL = timedelta(seconds=300) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_STATION): cv.string, + vol.Required(CONF_TYPE): vol.In(['air', 'road']), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + sensor_name = config.get(CONF_NAME) + sensor_api = config.get(CONF_API_KEY) + sensor_station = config.get(CONF_STATION) + sensor_type = config.get(CONF_TYPE) + + add_devices([TrafikverketWeatherStation( + sensor_name, sensor_api, sensor_station, sensor_type)], True) + + +class TrafikverketWeatherStation(Entity): + """Representation of a Sensor.""" + + def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type): + """Initialize the sensor.""" + self._name = sensor_name + self._api = sensor_api + self._station = sensor_station + self._type = sensor_type + self._state = None + self._attributes = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json' + + if self._type == 'road': + air_vs_road = 'Road' + else: + air_vs_road = 'Air' + + xml = """ + + + + + + + Measurement.""" + air_vs_road + """.Temp + + """ + + # Testing JSON post request. + try: + post = requests.post(url, data=xml.encode('utf-8'), timeout=5) + except requests.exceptions.RequestException as err: + _LOGGER.error("Please check network connection: %s", err) + return + + # Checking JSON respons. + try: + data = json.loads(post.text) + result = data["RESPONSE"]["RESULT"][0] + final = result["WeatherStation"][0]["Measurement"] + except KeyError: + _LOGGER.error("Incorrect weather station or API key.") + return + + # air_vs_road contains "Air" or "Road" depending on user input. + self._state = final[air_vs_road]["Temp"] From f2879554228a036d47d5477984179754f01a5a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Mon, 19 Mar 2018 22:12:53 +0100 Subject: [PATCH 086/924] zha: catch the exception from bellows if a device isn't available. (#13314) * catch the exception from bellows if a device isn't available. * fix import of zigpy exception * fix lint import --- homeassistant/components/light/zha.py | 16 +++++++++++++--- homeassistant/components/switch/zha.py | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 68c5bcc2a29..8eb1b3dc9b6 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -5,7 +5,6 @@ For more details on this platform, please refer to the documentation at https://home-assistant.io/components/light.zha/ """ import logging - from homeassistant.components import light, zha from homeassistant.const import STATE_UNKNOWN import homeassistant.util.color as color_util @@ -112,14 +111,25 @@ class Light(zha.Entity, light.Light): self._state = 1 self.async_schedule_update_ha_state() return + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.on() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the light on: %s", ex) + return - await self._endpoint.on_off.on() self._state = 1 self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._endpoint.on_off.off() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.off() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the light off: %s", ex) + return + self._state = 0 self.async_schedule_update_ha_state() diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index 7de9f1459b1..22eb50be86b 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -57,12 +57,24 @@ class Switch(zha.Entity, SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - await self._endpoint.on_off.on() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.on() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the switch on: %s", ex) + return + self._state = 1 async def async_turn_off(self, **kwargs): """Turn the entity off.""" - await self._endpoint.on_off.off() + from zigpy.exceptions import DeliveryError + try: + await self._endpoint.on_off.off() + except DeliveryError as ex: + _LOGGER.error("Unable to turn the switch off: %s", ex) + return + self._state = 0 async def async_update(self): From a04c6d583038b6303ebaaee4e6c96bcc1dffc443 Mon Sep 17 00:00:00 2001 From: Ryan McLean Date: Mon, 19 Mar 2018 21:15:21 +0000 Subject: [PATCH 087/924] Plex unavailable client cleanup (#13156) * Added timestamp for when device was marked unavailable * protect time 1st marked * client removal, no errors * Optional removal interval added * Linting error fix * Implemented guard to prevent indentation Removed vars in favour of inline calcs * Using hass.add_job() for cleanup * Lint * Revert removal interval to 600sec * Changed datetime to hass implementation * Forgot to include one of the references to dt --- homeassistant/components/media_player/plex.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index 48e532074f7..edb8aa147fb 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -23,6 +23,8 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_utc_time_change from homeassistant.util.json import load_json, save_json +from homeassistant.util import dt as dt_util + REQUIREMENTS = ['plexapi==3.0.6'] @@ -38,6 +40,8 @@ CONF_INCLUDE_NON_CLIENTS = 'include_non_clients' CONF_USE_EPISODE_ART = 'use_episode_art' CONF_USE_CUSTOM_ENTITY_IDS = 'use_custom_entity_ids' CONF_SHOW_ALL_CONTROLS = 'show_all_controls' +CONF_REMOVE_UNAVAILABLE_CLIENTS = 'remove_unavailable_clients' +CONF_CLIENT_REMOVE_INTERVAL = 'client_remove_interval' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_INCLUDE_NON_CLIENTS, default=False): @@ -46,6 +50,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ cv.boolean, vol.Optional(CONF_USE_CUSTOM_ENTITY_IDS, default=False): cv.boolean, + vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): + cv.boolean, + vol.Optional(CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)): + vol.All(cv.time_period, cv.positive_timedelta), }) PLEX_DATA = "plex" @@ -184,6 +192,7 @@ def setup_plexserver( else: plex_clients[machine_identifier].refresh(None, session) + clients_to_remove = [] for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: @@ -192,6 +201,18 @@ def setup_plexserver( client.set_availability(client.machine_identifier in available_client_ids) + if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) \ + or client.available: + continue + + if (dt_util.utcnow() - client.marked_unavailable) >= \ + (config.get(CONF_CLIENT_REMOVE_INTERVAL)): + hass.add_job(client.async_remove()) + clients_to_remove.append(client.machine_identifier) + + while clients_to_remove: + del plex_clients[clients_to_remove.pop()] + if new_plex_clients: add_devices_callback(new_plex_clients) @@ -266,6 +287,7 @@ class PlexClient(MediaPlayerDevice): self._app_name = '' self._device = None self._available = False + self._marked_unavailable = None self._device_protocol_capabilities = None self._is_player_active = False self._is_player_available = False @@ -418,6 +440,11 @@ class PlexClient(MediaPlayerDevice): """Set the device as available/unavailable noting time.""" if not available: self._clear_media_details() + if self._marked_unavailable is None: + self._marked_unavailable = dt_util.utcnow() + else: + self._marked_unavailable = None + self._available = available def _set_player_state(self): @@ -506,6 +533,11 @@ class PlexClient(MediaPlayerDevice): """Return the device, if any.""" return self._device + @property + def marked_unavailable(self): + """Return time device was marked unavailable.""" + return self._marked_unavailable + @property def session(self): """Return the session, if any.""" From 4270bc7abb7a2fe6aa4f8fbca8908c03338dd3c2 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 19 Mar 2018 23:20:04 +0200 Subject: [PATCH 088/924] Perform check_config service in current process (#13017) * Perform check_config service in current process * feedback --- homeassistant/config.py | 22 ++++++---------------- tests/test_config.py | 34 ++++++++++++---------------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index e94fc297f48..58cfe845e8f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,5 +1,4 @@ """Module to help with parsing and generating configuration files.""" -import asyncio from collections import OrderedDict # pylint: disable=no-name-in-module from distutils.version import LooseVersion # pylint: disable=import-error @@ -7,7 +6,6 @@ import logging import os import re import shutil -import sys # pylint: disable=unused-import from typing import Any, List, Tuple # NOQA @@ -665,22 +663,14 @@ async def async_check_ha_config_file(hass): This method is a coroutine. """ - proc = await asyncio.create_subprocess_exec( - sys.executable, '-m', 'homeassistant', '--script', - 'check_config', '--config', hass.config.config_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, loop=hass.loop) + from homeassistant.scripts.check_config import check_ha_config_file - # Wait for the subprocess exit - log, _ = await proc.communicate() - exit_code = await proc.wait() + res = await hass.async_add_job( + check_ha_config_file, hass.config.config_dir) - # Convert to ASCII - log = RE_ASCII.sub('', log.decode()) - - if exit_code != 0 or RE_YAML_ERROR.search(log): - return log - return None + if not res.errors: + return None + return '\n'.join([err.message for err in res.errors]) @callback diff --git a/tests/test_config.py b/tests/test_config.py index ab6b860ea8f..aaa793f91a9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -27,9 +27,9 @@ from homeassistant.components.config.script import ( CONFIG_PATH as SCRIPTS_CONFIG_PATH) from homeassistant.components.config.customize import ( CONFIG_PATH as CUSTOMIZE_CONFIG_PATH) +import homeassistant.scripts.check_config as check_config -from tests.common import ( - get_test_config_dir, get_test_home_assistant, mock_coro) +from tests.common import get_test_config_dir, get_test_home_assistant CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -514,35 +514,25 @@ class TestConfig(unittest.TestCase): assert len(self.hass.config.whitelist_external_dirs) == 1 assert "/test/config/www" in self.hass.config.whitelist_external_dirs - @mock.patch('asyncio.create_subprocess_exec') - def test_check_ha_config_file_correct(self, mock_create): + @mock.patch('homeassistant.scripts.check_config.check_ha_config_file') + def test_check_ha_config_file_correct(self, mock_check): """Check that restart propagates to stop.""" - process_mock = mock.MagicMock() - attrs = { - 'communicate.return_value': mock_coro((b'output', None)), - 'wait.return_value': mock_coro(0)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_coro(process_mock) - + mock_check.return_value = check_config.HomeAssistantConfig() assert run_coroutine_threadsafe( - config_util.async_check_ha_config_file(self.hass), self.hass.loop + config_util.async_check_ha_config_file(self.hass), + self.hass.loop ).result() is None - @mock.patch('asyncio.create_subprocess_exec') - def test_check_ha_config_file_wrong(self, mock_create): + @mock.patch('homeassistant.scripts.check_config.check_ha_config_file') + def test_check_ha_config_file_wrong(self, mock_check): """Check that restart with a bad config doesn't propagate to stop.""" - process_mock = mock.MagicMock() - attrs = { - 'communicate.return_value': - mock_coro(('\033[34mhello'.encode('utf-8'), None)), - 'wait.return_value': mock_coro(1)} - process_mock.configure_mock(**attrs) - mock_create.return_value = mock_coro(process_mock) + mock_check.return_value = check_config.HomeAssistantConfig() + mock_check.return_value.add_error("bad") assert run_coroutine_threadsafe( config_util.async_check_ha_config_file(self.hass), self.hass.loop - ).result() == 'hello' + ).result() == 'bad' # pylint: disable=redefined-outer-name From 0977be1842be1358c8687fd213584da6ad2c53a5 Mon Sep 17 00:00:00 2001 From: Sergio Viudes Date: Tue, 20 Mar 2018 08:43:31 +0100 Subject: [PATCH 089/924] Added switch for DoorBird second relay (#13339) --- homeassistant/components/doorbird.py | 2 +- homeassistant/components/switch/doorbird.py | 10 ++++++++++ requirements_all.txt | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index be7adc034a0..34758023f60 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_HOST, CONF_USERNAME, CONF_PASSWORD from homeassistant.components.http import HomeAssistantView import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['DoorBirdPy==0.1.2'] +REQUIREMENTS = ['DoorBirdPy==0.1.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/doorbird.py b/homeassistant/components/switch/doorbird.py index 4ab8eea6ec4..9886b3a586d 100644 --- a/homeassistant/components/switch/doorbird.py +++ b/homeassistant/components/switch/doorbird.py @@ -22,6 +22,14 @@ SWITCHES = { }, "time": datetime.timedelta(seconds=3) }, + "open_door_2": { + "name": "Open Door 2", + "icon": { + True: "lock-open", + False: "lock" + }, + "time": datetime.timedelta(seconds=3) + }, "light_on": { "name": "Light On", "icon": { @@ -80,6 +88,8 @@ class DoorBirdSwitch(SwitchDevice): """Power the relay.""" if self._switch == "open_door": self._state = self._device.open_door() + elif self._switch == "open_door_2": + self._state = self._device.open_door(2) elif self._switch == "light_on": self._state = self._device.turn_light_on() diff --git a/requirements_all.txt b/requirements_all.txt index 91173a1825c..e2696bdd934 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -19,7 +19,7 @@ attrs==17.4.0 # Adafruit_BBIO==1.0.0 # homeassistant.components.doorbird -DoorBirdPy==0.1.2 +DoorBirdPy==0.1.3 # homeassistant.components.homekit HAP-python==1.1.7 From 3fa080a795064c7181b007b107257b977d53b5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 20 Mar 2018 08:46:10 +0100 Subject: [PATCH 090/924] Add min and max price as attribute for Tibber sensor (#13313) --- homeassistant/components/sensor/tibber.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 435003f76d0..aaaa8366909 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -73,14 +73,25 @@ class TibberSensor(Entity): return def _find_current_price(): + state = None + max_price = None + min_price = None for key, price_total in self._tibber_home.price_total.items(): price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_total = round(price_total, 3) time_diff = (now - price_time).total_seconds()/60 if time_diff >= 0 and time_diff < 60: - self._state = round(price_total, 3) + state = price_total self._last_updated = key - return True - return False + if now.date() == price_time.date(): + if max_price is None or price_total > max_price: + max_price = price_total + if min_price is None or price_total < min_price: + min_price = price_total + self._state = state + self._device_state_attributes['max_price'] = max_price + self._device_state_attributes['min_price'] = min_price + return state is not None if _find_current_price(): return From 5c4529d044463083bad73cdbf9d17d8cb2b29afa Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Tue, 20 Mar 2018 14:04:24 +0100 Subject: [PATCH 091/924] Bugfix: Zwave set_config_parameter failed when config list contained int (#13301) * Cast list and bool options to STR * Add button options to STR * Add TYPE_BUTTON to value types * Adjust comparison * Remove Logging * Remove Empty line * Update tests * Update tests * Mistake --- homeassistant/components/zwave/__init__.py | 13 +++++++-- homeassistant/components/zwave/const.py | 1 + tests/components/zwave/test_init.py | 34 +++++++++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 43aa996c799..a85160e8bde 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -442,9 +442,16 @@ def setup(hass, config): if value.index != param: continue if value.type in [const.TYPE_LIST, const.TYPE_BOOL]: - value.data = selection - _LOGGER.info("Setting config list parameter %s on Node %s " - "with selection %s", param, node_id, + value.data = str(selection) + _LOGGER.info("Setting config parameter %s on Node %s " + "with list/bool selection %s", param, node_id, + str(selection)) + return + if value.type == const.TYPE_BUTTON: + network.manager.pressButton(value.value_id) + network.manager.releaseButton(value.value_id) + _LOGGER.info("Setting config parameter %s on Node %s " + "with button selection %s", param, node_id, selection) return value.data = int(selection) diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index bb4b33300e5..8e1a22047c1 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -327,6 +327,7 @@ TYPE_DECIMAL = "Decimal" TYPE_INT = "Int" TYPE_LIST = "List" TYPE_STRING = "String" +TYPE_BUTTON = "Button" DISC_COMMAND_CLASS = "command_class" DISC_COMPONENT = "component" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 30c9d3ba489..bb073459b48 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -995,8 +995,21 @@ class TestZWaveServices(unittest.TestCase): type=const.TYPE_LIST, data_items=['item1', 'item2', 'item3'], ) + value_list_int = MockValue( + index=15, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_LIST, + data_items=['1', '2', '3'], + ) + value_button = MockValue( + index=14, + command_class=const.COMMAND_CLASS_CONFIGURATION, + type=const.TYPE_BUTTON, + ) node = MockNode(node_id=14) - node.get_values.return_value = {12: value, 13: value_list} + node.get_values.return_value = {12: value, 13: value_list, + 14: value_button, + 15: value_list_int} self.zwave_network.nodes = {14: node} self.hass.services.call('zwave', 'set_config_parameter', { @@ -1008,6 +1021,15 @@ class TestZWaveServices(unittest.TestCase): assert value_list.data == 'item3' + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 15, + const.ATTR_CONFIG_VALUE: 3, + }) + self.hass.block_till_done() + + assert value_list_int.data == '3' + self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 12, @@ -1017,6 +1039,16 @@ class TestZWaveServices(unittest.TestCase): assert value.data == 7 + self.hass.services.call('zwave', 'set_config_parameter', { + const.ATTR_NODE_ID: 14, + const.ATTR_CONFIG_PARAMETER: 14, + const.ATTR_CONFIG_VALUE: True, + }) + self.hass.block_till_done() + + assert self.zwave_network.manager.pressButton.called + assert self.zwave_network.manager.releaseButton.called + self.hass.services.call('zwave', 'set_config_parameter', { const.ATTR_NODE_ID: 14, const.ATTR_CONFIG_PARAMETER: 19, From 05c9c5750017b147617eff153c0b5fbe984d7e7f Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 20 Mar 2018 20:32:59 +0100 Subject: [PATCH 092/924] Update pyhomematic to 0.1.40 (#13354) * Update __init__.py * Update requirements_all.txt --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index b913b58864d..1accf038575 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.39'] +REQUIREMENTS = ['pyhomematic==0.1.40'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e2696bdd934..f3637a40f64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ pyhik==0.1.8 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.39 +pyhomematic==0.1.40 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.1.0 From 852eef8046f9950900dd3bd86549a3a90185d36d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 02:05:03 +0100 Subject: [PATCH 093/924] Fix Sonos radio stations with ampersand (#13293) --- homeassistant/components/media_player/sonos.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 34f30b5c2f4..091046a6e7a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -798,7 +798,9 @@ class SonosDevice(MediaPlayerDevice): src = fav.pop() uri = src.reference.get_uri() if _is_radio_uri(uri): - self.soco.play_uri(uri, title=source) + # SoCo 0.14 fails to XML escape the title parameter + from xml.sax.saxutils import escape + self.soco.play_uri(uri, title=escape(source)) else: self.soco.clear_queue() self.soco.add_to_queue(src.reference) From cfb0b00c0cf22dbb2a5dc9c832d4dab1fa58398d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Mar 2018 18:09:34 -0700 Subject: [PATCH 094/924] Do not include unavailable entities in Google Assistant SYNC (#13358) --- .../components/google_assistant/smart_home.py | 9 +++++- homeassistant/components/light/demo.py | 3 +- .../google_assistant/test_smart_home.py | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 834d40c367c..7e746d48bed 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -94,9 +94,16 @@ class _GoogleEntity: https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ - traits = self.traits() state = self.state + # When a state is unavailable, the attributes that describe + # capabilities will be stripped. For example, a light entity will miss + # the min/max mireds. Therefore they will be excluded from a sync. + if state.state == STATE_UNAVAILABLE: + return None + + traits = self.traits() + # Found no supported traits for this entity if not traits: return None diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index 05aecd542e2..ba27cbd3ac5 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -52,6 +52,7 @@ class DemoLight(Light): self._white = white self._effect_list = effect_list self._effect = effect + self._available = True @property def should_poll(self) -> bool: @@ -73,7 +74,7 @@ class DemoLight(Light): """Return availability.""" # This demo light is always available, but well-behaving components # should implement this to inform Home Assistant accordingly. - return True + return self._available @property def brightness(self) -> int: diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6523c22fee1..d7684dc90e0 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -259,3 +259,31 @@ def test_serialize_input_boolean(): 'type': 'action.devices.types.SWITCH', 'willReportState': False, } + + +async def test_unavailable_state_doesnt_sync(hass): + """Test that an unavailable entity does not sync over.""" + light = DemoLight( + None, 'Demo Light', + state=False, + hs_color=(180, 75), + ) + light.hass = hass + light.entity_id = 'light.demo_light' + light._available = False + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From 3426487277de4da44103eb3d3d1f2cf2332f782c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 Mar 2018 02:26:56 +0100 Subject: [PATCH 095/924] Fix mysensors light turn on hs color (#13349) --- homeassistant/components/light/mysensors.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 26e20ff387d..d595fc4b184 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -107,7 +107,11 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): rgb = color_util.color_hs_to_RGB(*self._hs) white = self._white hex_color = self._values.get(self.value_type) - new_rgb = color_util.color_hs_to_RGB(*kwargs.get(ATTR_HS_COLOR)) + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None: + new_rgb = color_util.color_hs_to_RGB(*hs_color) + else: + new_rgb = None new_white = kwargs.get(ATTR_WHITE_VALUE) if new_rgb is None and new_white is None: From f8127a3902abaa6cb654ca7c28905adc81ee0623 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 03:27:07 +0100 Subject: [PATCH 096/924] Add a polling fallback for Sonos (#13310) * Prepare for poll * Add a polling fallback for Sonos --- .../components/media_player/sonos.py | 280 +++++++++--------- tests/components/media_player/test_sonos.py | 6 +- 2 files changed, 151 insertions(+), 135 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 091046a6e7a..34ef146fc05 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -208,9 +208,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif service.service == SERVICE_CLEAR_TIMER: device.clear_sleep_timer() elif service.service == SERVICE_UPDATE_ALARM: - device.update_alarm(**service.data) + device.set_alarm(**service.data) elif service.service == SERVICE_SET_OPTION: - device.update_option(**service.data) + device.set_option(**service.data) device.schedule_update_ha_state(True) @@ -330,12 +330,13 @@ class SonosDevice(MediaPlayerDevice): def __init__(self, player): """Initialize the Sonos device.""" + self._receives_events = False self._volume_increment = 5 self._unique_id = player.uid self._player = player self._model = None self._player_volume = None - self._player_volume_muted = None + self._player_muted = None self._play_mode = None self._name = None self._coordinator = None @@ -420,11 +421,9 @@ class SonosDevice(MediaPlayerDevice): speaker_info = self.soco.get_speaker_info(True) self._name = speaker_info['zone_name'] self._model = speaker_info['model_name'] - self._player_volume = self.soco.volume - self._player_volume_muted = self.soco.mute self._play_mode = self.soco.play_mode - self._night_sound = self.soco.night_mode - self._speech_enhance = self.soco.dialog_mode + + self.update_volume() self._favorites = [] for fav in self.soco.music_library.get_sonos_favorites(): @@ -437,124 +436,6 @@ class SonosDevice(MediaPlayerDevice): except Exception: _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) - def _subscribe_to_player_events(self): - """Add event subscriptions.""" - player = self.soco - - # New player available, build the current group topology - for device in self.hass.data[DATA_SONOS].devices: - device.process_zonegrouptopology_event(None) - - queue = _ProcessSonosEventQueue(self.process_avtransport_event) - player.avTransport.subscribe(auto_renew=True, event_queue=queue) - - queue = _ProcessSonosEventQueue(self.process_rendering_event) - player.renderingControl.subscribe(auto_renew=True, event_queue=queue) - - queue = _ProcessSonosEventQueue(self.process_zonegrouptopology_event) - player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) - - def update(self): - """Retrieve latest state.""" - available = self._check_available() - if self._available != available: - self._available = available - if available: - self._set_basic_information() - self._subscribe_to_player_events() - else: - self._player_volume = None - self._player_volume_muted = None - self._status = 'OFF' - self._coordinator = None - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - self._media_image_url = None - self._media_artist = None - self._media_album_name = None - self._media_title = None - self._source_name = None - - def process_avtransport_event(self, event): - """Process a track change event coming from a coordinator.""" - transport_info = self.soco.get_current_transport_info() - new_status = transport_info.get('current_transport_state') - - # Ignore transitions, we should get the target state soon - if new_status == 'TRANSITIONING': - return - - self._play_mode = self.soco.play_mode - - if self.soco.is_playing_tv: - self._refresh_linein(SOURCE_TV) - elif self.soco.is_playing_line_in: - self._refresh_linein(SOURCE_LINEIN) - else: - track_info = self.soco.get_current_track_info() - - if _is_radio_uri(track_info['uri']): - self._refresh_radio(event.variables, track_info) - else: - update_position = (new_status != self._status) - self._refresh_music(update_position, track_info) - - self._status = new_status - - self.schedule_update_ha_state() - - # Also update slaves - for entity in self.hass.data[DATA_SONOS].devices: - coordinator = entity.coordinator - if coordinator and coordinator.unique_id == self.unique_id: - entity.schedule_update_ha_state() - - def process_rendering_event(self, event): - """Process a volume change event coming from a player.""" - variables = event.variables - - if 'volume' in variables: - self._player_volume = int(variables['volume']['Master']) - - if 'mute' in variables: - self._player_volume_muted = (variables['mute']['Master'] == '1') - - if 'night_mode' in variables: - self._night_sound = (variables['night_mode'] == '1') - - if 'dialog_level' in variables: - self._speech_enhance = (variables['dialog_level'] == '1') - - self.schedule_update_ha_state() - - def process_zonegrouptopology_event(self, event): - """Process a zone group topology event coming from a player.""" - if event and not hasattr(event, 'zone_player_uui_ds_in_group'): - return - - with self.hass.data[DATA_SONOS].topology_lock: - group = event and event.zone_player_uui_ds_in_group - if group: - # New group information is pushed - coordinator_uid, *slave_uids = group.split(',') - else: - # Use SoCo cache for existing topology - coordinator_uid = self.soco.group.coordinator.uid - slave_uids = [p.uid for p in self.soco.group.members - if p.uid != coordinator_uid] - - if self.unique_id == coordinator_uid: - self._coordinator = None - self.schedule_update_ha_state() - - for slave_uid in slave_uids: - slave = _get_entity_from_soco_uid(self.hass, slave_uid) - if slave: - # pylint: disable=protected-access - slave._coordinator = self - slave.schedule_update_ha_state() - def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" if url not in ('', 'NOT_IMPLEMENTED', None): @@ -568,7 +449,88 @@ class SonosDevice(MediaPlayerDevice): ) return url - def _refresh_linein(self, source): + def _subscribe_to_player_events(self): + """Add event subscriptions.""" + self._receives_events = False + + # New player available, build the current group topology + for device in self.hass.data[DATA_SONOS].devices: + device.update_groups() + + player = self.soco + + queue = _ProcessSonosEventQueue(self.update_media) + player.avTransport.subscribe(auto_renew=True, event_queue=queue) + + queue = _ProcessSonosEventQueue(self.update_volume) + player.renderingControl.subscribe(auto_renew=True, event_queue=queue) + + queue = _ProcessSonosEventQueue(self.update_groups) + player.zoneGroupTopology.subscribe(auto_renew=True, event_queue=queue) + + def update(self): + """Retrieve latest state.""" + available = self._check_available() + if self._available != available: + self._available = available + if available: + self._set_basic_information() + self._subscribe_to_player_events() + else: + self._player_volume = None + self._player_muted = None + self._status = 'OFF' + self._coordinator = None + self._media_duration = None + self._media_position = None + self._media_position_updated_at = None + self._media_image_url = None + self._media_artist = None + self._media_album_name = None + self._media_title = None + self._source_name = None + elif available and not self._receives_events: + self.update_groups() + self.update_volume() + if self.is_coordinator: + self.update_media() + + def update_media(self, event=None): + """Update information about currently playing media.""" + transport_info = self.soco.get_current_transport_info() + new_status = transport_info.get('current_transport_state') + + # Ignore transitions, we should get the target state soon + if new_status == 'TRANSITIONING': + return + + self._play_mode = self.soco.play_mode + + if self.soco.is_playing_tv: + self.update_media_linein(SOURCE_TV) + elif self.soco.is_playing_line_in: + self.update_media_linein(SOURCE_LINEIN) + else: + track_info = self.soco.get_current_track_info() + + if _is_radio_uri(track_info['uri']): + variables = event and event.variables + self.update_media_radio(variables, track_info) + else: + update_position = (new_status != self._status) + self.update_media_music(update_position, track_info) + + self._status = new_status + + self.schedule_update_ha_state() + + # Also update slaves + for entity in self.hass.data[DATA_SONOS].devices: + coordinator = entity.coordinator + if coordinator and coordinator.unique_id == self.unique_id: + entity.schedule_update_ha_state() + + def update_media_linein(self, source): """Update state when playing from line-in/tv.""" self._media_duration = None self._media_position = None @@ -582,7 +544,7 @@ class SonosDevice(MediaPlayerDevice): self._source_name = source - def _refresh_radio(self, variables, track_info): + def update_media_radio(self, variables, track_info): """Update state when streaming radio.""" self._media_duration = None self._media_position = None @@ -603,7 +565,7 @@ class SonosDevice(MediaPlayerDevice): artist=self._media_artist, title=self._media_title ) - else: + elif variables: # "On Now" field in the sonos pc app current_track_metadata = variables.get('current_track_meta_data') if current_track_metadata: @@ -643,7 +605,7 @@ class SonosDevice(MediaPlayerDevice): if fav.reference.get_uri() == media_info['CurrentURI']: self._source_name = fav.title - def _refresh_music(self, update_media_position, track_info): + def update_media_music(self, update_media_position, track_info): """Update state when playing music tracks.""" self._media_duration = _timespan_secs(track_info.get('duration')) @@ -682,6 +644,60 @@ class SonosDevice(MediaPlayerDevice): self._source_name = None + def update_volume(self, event=None): + """Update information about currently volume settings.""" + if event: + variables = event.variables + + if 'volume' in variables: + self._player_volume = int(variables['volume']['Master']) + + if 'mute' in variables: + self._player_muted = (variables['mute']['Master'] == '1') + + if 'night_mode' in variables: + self._night_sound = (variables['night_mode'] == '1') + + if 'dialog_level' in variables: + self._speech_enhance = (variables['dialog_level'] == '1') + + self.schedule_update_ha_state() + else: + self._player_volume = self.soco.volume + self._player_muted = self.soco.mute + self._night_sound = self.soco.night_mode + self._speech_enhance = self.soco.dialog_mode + + def update_groups(self, event=None): + """Process a zone group topology event coming from a player.""" + if event: + self._receives_events = True + + if not hasattr(event, 'zone_player_uui_ds_in_group'): + return + + with self.hass.data[DATA_SONOS].topology_lock: + group = event and event.zone_player_uui_ds_in_group + if group: + # New group information is pushed + coordinator_uid, *slave_uids = group.split(',') + else: + # Use SoCo cache for existing topology + coordinator_uid = self.soco.group.coordinator.uid + slave_uids = [p.uid for p in self.soco.group.members + if p.uid != coordinator_uid] + + if self.unique_id == coordinator_uid: + self._coordinator = None + self.schedule_update_ha_state() + + for slave_uid in slave_uids: + slave = _get_entity_from_soco_uid(self.hass, slave_uid) + if slave: + # pylint: disable=protected-access + slave._coordinator = self + slave.schedule_update_ha_state() + @property def volume_level(self): """Volume level of the media player (0..1).""" @@ -690,7 +706,7 @@ class SonosDevice(MediaPlayerDevice): @property def is_volume_muted(self): """Return true if volume is muted.""" - return self._player_volume_muted + return self._player_muted @property @soco_coordinator @@ -988,7 +1004,7 @@ class SonosDevice(MediaPlayerDevice): @soco_error() @soco_coordinator - def update_alarm(self, **data): + def set_alarm(self, **data): """Set the alarm clock on the player.""" from soco import alarms alarm = None @@ -1011,7 +1027,7 @@ class SonosDevice(MediaPlayerDevice): alarm.save() @soco_error() - def update_option(self, **data): + def set_option(self, **data): """Modify playback options.""" if ATTR_NIGHT_SOUND in data and self._night_sound is not None: self.soco.night_mode = data[ATTR_NIGHT_SOUND] diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index f741898d15e..7d0d675f66f 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -276,7 +276,7 @@ class TestSonosMediaPlayer(unittest.TestCase): @mock.patch('soco.SoCo', new=SoCoMock) @mock.patch('soco.alarms.Alarm') @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_update_alarm(self, soco_mock, alarm_mock, *args): + def test_set_alarm(self, soco_mock, alarm_mock, *args): """Ensuring soco methods called for sonos_set_sleep_timer service.""" sonos.setup_platform(self.hass, {}, add_devices_factory(self.hass), { 'host': '192.0.2.1' @@ -293,9 +293,9 @@ class TestSonosMediaPlayer(unittest.TestCase): 'include_linked_zones': True, 'volume': 0.30, } - device.update_alarm(alarm_id=2) + device.set_alarm(alarm_id=2) alarm1.save.assert_not_called() - device.update_alarm(alarm_id=1, **attrs) + device.set_alarm(alarm_id=1, **attrs) self.assertEqual(alarm1.enabled, attrs['enabled']) self.assertEqual(alarm1.start_time, attrs['time']) self.assertEqual(alarm1.include_linked_zones, From fab958d789cf7f5727451722af95b2dc0fd7855d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Mar 2018 10:14:28 -0700 Subject: [PATCH 097/924] Version bump to 0.65.6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7e2b9f3061a..7fe26e6c334 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 65 -PATCH_VERSION = '5' +PATCH_VERSION = '6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 2388d62755252ac4e35159880fe4b6c17c954789 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Wed, 14 Mar 2018 21:44:13 -0700 Subject: [PATCH 098/924] More robust MJPEG parser. Fixes #13138. (#13226) * More robust MJPEG parser. Fixes ##13138. * Reimplement image extraction from mjpeg without ascy generator to support python 3.5 --- homeassistant/components/camera/proxy.py | 52 ++++++++---------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/camera/proxy.py b/homeassistant/components/camera/proxy.py index 56b9db5c0ec..9f261b89bb2 100644 --- a/homeassistant/components/camera/proxy.py +++ b/homeassistant/components/camera/proxy.py @@ -56,34 +56,6 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([ProxyCamera(hass, config)]) -async def _read_frame(req): - """Read a single frame from an MJPEG stream.""" - # based on https://gist.github.com/russss/1143799 - import cgi - # Read in HTTP headers: - stream = req.content - # multipart/x-mixed-replace; boundary=--frameboundary - _mimetype, options = cgi.parse_header(req.headers['content-type']) - boundary = options.get('boundary').encode('utf-8') - if not boundary: - _LOGGER.error("Malformed MJPEG missing boundary") - raise Exception("Can't find content-type") - - line = await stream.readline() - # Seek ahead to the first chunk - while line.strip() != boundary: - line = await stream.readline() - # Read in chunk headers - while line.strip() != b'': - parts = line.split(b':') - if len(parts) > 1 and parts[0].lower() == b'content-length': - # Grab chunk length - length = int(parts[1].strip()) - line = await stream.readline() - image = await stream.read(length) - return image - - def _resize_image(image, opts): """Resize image.""" from PIL import Image @@ -227,9 +199,9 @@ class ProxyCamera(Camera): 'boundary=--frameboundary') await response.prepare(request) - def write(img_bytes): + async def write(img_bytes): """Write image to stream.""" - response.write(bytes( + await response.write(bytes( '--frameboundary\r\n' 'Content-Type: {}\r\n' 'Content-Length: {}\r\n\r\n'.format( @@ -240,13 +212,23 @@ class ProxyCamera(Camera): req = await stream_coro try: + # This would be nicer as an async generator + # But that would only be supported for python >=3.6 + data = b'' + stream = req.content while True: - image = await _read_frame(req) - if not image: + chunk = await stream.read(102400) + if not chunk: break - image = await self.hass.async_add_job( - _resize_image, image, self._stream_opts) - write(image) + data += chunk + jpg_start = data.find(b'\xff\xd8') + jpg_end = data.find(b'\xff\xd9') + if jpg_start != -1 and jpg_end != -1: + image = data[jpg_start:jpg_end + 2] + image = await self.hass.async_add_job( + _resize_image, image, self._stream_opts) + await write(image) + data = data[jpg_end + 2:] except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") req.close() From 7e08e8bd51930100d3ec621b007167de6005c93d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 15 Mar 2018 13:53:59 -0700 Subject: [PATCH 099/924] Tado: don't reference unset hass var (#13237) Tado: don't reference unset hass var --- homeassistant/components/device_tracker/tado.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/tado.py b/homeassistant/components/device_tracker/tado.py index 11d12322ff5..ef816338ce9 100644 --- a/homeassistant/components/device_tracker/tado.py +++ b/homeassistant/components/device_tracker/tado.py @@ -100,7 +100,7 @@ class TadoDeviceScanner(DeviceScanner): last_results = [] try: - with async_timeout.timeout(10, loop=self.hass.loop): + with async_timeout.timeout(10): # Format the URL here, so we can log the template URL if # anything goes wrong without exposing username and password. url = self.tadoapiurl.format( From 0de26817833c4f430e4a94045fca489e6ceac64e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 16 Mar 2018 00:12:43 +0100 Subject: [PATCH 100/924] Fix Sonos join/unjoin in scripts (#13248) --- homeassistant/components/media_player/sonos.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 2a12b59e7c7..b2cbffed891 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -194,13 +194,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): master = [device for device in hass.data[DATA_SONOS].devices if device.entity_id == service.data[ATTR_MASTER]] if master: - master[0].join(devices) + with hass.data[DATA_SONOS].topology_lock: + master[0].join(devices) + return + + if service.service == SERVICE_UNJOIN: + with hass.data[DATA_SONOS].topology_lock: + for device in devices: + device.unjoin() return for device in devices: - if service.service == SERVICE_UNJOIN: - device.unjoin() - elif service.service == SERVICE_SNAPSHOT: + if service.service == SERVICE_SNAPSHOT: device.snapshot(service.data[ATTR_WITH_GROUP]) elif service.service == SERVICE_RESTORE: device.restore(service.data[ATTR_WITH_GROUP]) @@ -893,16 +898,19 @@ class SonosDevice(MediaPlayerDevice): def join(self, slaves): """Form a group with other players.""" if self._coordinator: - self.soco.unjoin() + self.unjoin() for slave in slaves: if slave.unique_id != self.unique_id: slave.soco.join(self.soco) + # pylint: disable=protected-access + slave._coordinator = self @soco_error() def unjoin(self): """Unjoin the player from a group.""" self.soco.unjoin() + self._coordinator = None @soco_error() def snapshot(self, with_group=True): From 7718f70c5f54b32821f4a52fa53054073b20a354 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 21 Mar 2018 02:05:03 +0100 Subject: [PATCH 101/924] Fix Sonos radio stations with ampersand (#13293) --- homeassistant/components/media_player/sonos.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b2cbffed891..448c66c4e45 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -804,7 +804,9 @@ class SonosDevice(MediaPlayerDevice): src = fav.pop() uri = src.reference.get_uri() if _is_radio_uri(uri): - self.soco.play_uri(uri, title=source) + # SoCo 0.14 fails to XML escape the title parameter + from xml.sax.saxutils import escape + self.soco.play_uri(uri, title=escape(source)) else: self.soco.clear_queue() self.soco.add_to_queue(src.reference) From ffbafa687ab25fd3563841c104a5a160a3f8347f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 20 Mar 2018 18:09:34 -0700 Subject: [PATCH 102/924] Do not include unavailable entities in Google Assistant SYNC (#13358) --- .../components/google_assistant/smart_home.py | 9 +++++- homeassistant/components/light/demo.py | 3 +- .../google_assistant/test_smart_home.py | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 834d40c367c..7e746d48bed 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -94,9 +94,16 @@ class _GoogleEntity: https://developers.google.com/actions/smarthome/create-app#actiondevicessync """ - traits = self.traits() state = self.state + # When a state is unavailable, the attributes that describe + # capabilities will be stripped. For example, a light entity will miss + # the min/max mireds. Therefore they will be excluded from a sync. + if state.state == STATE_UNAVAILABLE: + return None + + traits = self.traits() + # Found no supported traits for this entity if not traits: return None diff --git a/homeassistant/components/light/demo.py b/homeassistant/components/light/demo.py index acc70a57ff4..37a354bb3f2 100644 --- a/homeassistant/components/light/demo.py +++ b/homeassistant/components/light/demo.py @@ -54,6 +54,7 @@ class DemoLight(Light): self._white = white self._effect_list = effect_list self._effect = effect + self._available = True @property def should_poll(self) -> bool: @@ -75,7 +76,7 @@ class DemoLight(Light): """Return availability.""" # This demo light is always available, but well-behaving components # should implement this to inform Home Assistant accordingly. - return True + return self._available @property def brightness(self) -> int: diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24d74afa6da..8c9824a32f8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -259,3 +259,31 @@ def test_serialize_input_boolean(): 'type': 'action.devices.types.SWITCH', 'willReportState': False, } + + +async def test_unavailable_state_doesnt_sync(hass): + """Test that an unavailable entity does not sync over.""" + light = DemoLight( + None, 'Demo Light', + state=False, + hs_color=(180, 75), + ) + light.hass = hass + light.entity_id = 'light.demo_light' + light._available = False + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From da4e630f5476de38ab422597e8b1941b26678969 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Mar 2018 10:38:06 -0700 Subject: [PATCH 103/924] Fix tests --- tests/components/google_assistant/test_smart_home.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8c9824a32f8..0c69e453092 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -266,7 +266,6 @@ async def test_unavailable_state_doesnt_sync(hass): light = DemoLight( None, 'Demo Light', state=False, - hs_color=(180, 75), ) light.hass = hass light.entity_id = 'light.demo_light' From 74c249e57d16340ebd89fcd989942ff8b2fac26f Mon Sep 17 00:00:00 2001 From: Scott Reston Date: Wed, 21 Mar 2018 13:44:53 -0400 Subject: [PATCH 104/924] Fix retrieval of track URL into medi_content_id (#13333) 'current.item' was returning blank. --- homeassistant/components/media_player/spotify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 734285d918a..963258f1861 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -194,7 +194,7 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._title = item.get('name') self._artist = ', '.join([artist.get('name') for artist in item.get('artists')]) - self._uri = current.get('uri') + self._uri = item.get('uri') images = item.get('album').get('images') self._image_url = images[0].get('url') if images else None # Playing state From 0396725fe965fc186aa39fc13f82e2de6e64b47b Mon Sep 17 00:00:00 2001 From: maxclaey Date: Wed, 21 Mar 2018 19:06:46 +0100 Subject: [PATCH 105/924] Homekit Bugfix: Use get instead of indexing (#13353) Fixes bug for alarm_control_panel if not code is required. --- homeassistant/components/homekit/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index b74171b08f7..02449607bf2 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -107,7 +107,8 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') return TYPES['SecuritySystem'](hass, state.entity_id, state.name, - alarm_code=config[ATTR_CODE], aid=aid) + alarm_code=config.get(ATTR_CODE), + aid=aid) elif state.domain == 'climate': features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) From 36bc7f8175e8ecb11a9881ed2b39f8922c5c1716 Mon Sep 17 00:00:00 2001 From: maxclaey Date: Wed, 21 Mar 2018 20:29:58 +0100 Subject: [PATCH 106/924] Configuration options for IFTTT alarm control panel (#13352) * Improvements * Use optimistic instead of await callback * Fix default value for optimistic --- .../components/alarm_control_panel/ifttt.py | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index eb1a8f8ed7d..5303c24876e 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -15,7 +15,7 @@ from homeassistant.components.ifttt import ( ATTR_EVENT, DOMAIN as IFTTT_DOMAIN, SERVICE_TRIGGER) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, CONF_NAME, CONF_CODE, - STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, + CONF_OPTIMISTIC, STATE_ALARM_DISARMED, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY) import homeassistant.helpers.config_validation as cv @@ -30,14 +30,24 @@ ALLOWED_STATES = [ DATA_IFTTT_ALARM = 'ifttt_alarm' DEFAULT_NAME = "Home" -EVENT_ALARM_ARM_AWAY = "alarm_arm_away" -EVENT_ALARM_ARM_HOME = "alarm_arm_home" -EVENT_ALARM_ARM_NIGHT = "alarm_arm_night" -EVENT_ALARM_DISARM = "alarm_disarm" +CONF_EVENT_AWAY = "event_arm_away" +CONF_EVENT_HOME = "event_arm_home" +CONF_EVENT_NIGHT = "event_arm_night" +CONF_EVENT_DISARM = "event_disarm" + +DEFAULT_EVENT_AWAY = "alarm_arm_away" +DEFAULT_EVENT_HOME = "alarm_arm_home" +DEFAULT_EVENT_NIGHT = "alarm_arm_night" +DEFAULT_EVENT_DISARM = "alarm_disarm" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, + vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, + vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, + vol.Optional(CONF_EVENT_DISARM, default=DEFAULT_EVENT_DISARM): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, }) SERVICE_PUSH_ALARM_STATE = "ifttt_push_alarm_state" @@ -55,8 +65,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) code = config.get(CONF_CODE) + event_away = config.get(CONF_EVENT_AWAY) + event_home = config.get(CONF_EVENT_HOME) + event_night = config.get(CONF_EVENT_NIGHT) + event_disarm = config.get(CONF_EVENT_DISARM) + optimistic = config.get(CONF_OPTIMISTIC) - alarmpanel = IFTTTAlarmPanel(name, code) + alarmpanel = IFTTTAlarmPanel(name, code, event_away, event_home, + event_night, event_disarm, optimistic) hass.data[DATA_IFTTT_ALARM].append(alarmpanel) add_devices([alarmpanel]) @@ -79,10 +95,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Representation of an alarm control panel controlled throught IFTTT.""" - def __init__(self, name, code): + def __init__(self, name, code, event_away, event_home, event_night, + event_disarm, optimistic): """Initialize the alarm control panel.""" self._name = name self._code = code + self._event_away = event_away + self._event_home = event_home + self._event_night = event_night + self._event_disarm = event_disarm + self._optimistic = optimistic self._state = None @property @@ -109,32 +131,34 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): """Send disarm command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_DISARM) + self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED) def alarm_arm_away(self, code=None): """Send arm away command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_ARM_AWAY) + self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) def alarm_arm_home(self, code=None): """Send arm home command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_ARM_HOME) + self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) def alarm_arm_night(self, code=None): """Send arm night command.""" if not self._check_code(code): return - self.set_alarm_state(EVENT_ALARM_ARM_NIGHT) + self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) - def set_alarm_state(self, event): + def set_alarm_state(self, event, state): """Call the IFTTT trigger service to change the alarm state.""" data = {ATTR_EVENT: event} self.hass.services.call(IFTTT_DOMAIN, SERVICE_TRIGGER, data) _LOGGER.debug("Called IFTTT component to trigger event %s", event) + if self._optimistic: + self._state = state def push_alarm_state(self, value): """Push the alarm state to the given value.""" From 39394608143c864340645683af344be2195a2f7c Mon Sep 17 00:00:00 2001 From: Thomas Svedberg <36861881+ThomasSvedberg@users.noreply.github.com> Date: Wed, 21 Mar 2018 23:21:51 +0100 Subject: [PATCH 107/924] =?UTF-8?q?Add=20the=20possibility=20to=20filter?= =?UTF-8?q?=20on=20line(s)=20in=20V=C3=A4sttrafik=20Public=20Transport=20s?= =?UTF-8?q?ensor=20(#13317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add the possibility to filter on line(s) in Västtrafik Public Transport sensor Add a config entry "lines" to be able to filter departures on line(s) for the vasttrafik sensor. * Change log level to debug if no departures found. * Remove extra None argument from dict().get() calls as it is already the default. * Ensure "lines" is a list of strings. Also fix an indentation error. * Correct to long line --- homeassistant/components/sensor/vasttrafik.py | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sensor/vasttrafik.py b/homeassistant/components/sensor/vasttrafik.py index 983c589c98b..8cd084e1b71 100644 --- a/homeassistant/components/sensor/vasttrafik.py +++ b/homeassistant/components/sensor/vasttrafik.py @@ -30,6 +30,7 @@ CONF_DELAY = 'delay' CONF_DEPARTURES = 'departures' CONF_FROM = 'from' CONF_HEADING = 'heading' +CONF_LINES = 'lines' CONF_KEY = 'key' CONF_SECRET = 'secret' @@ -46,6 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FROM): cv.string, vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): cv.positive_int, vol.Optional(CONF_HEADING): cv.string, + vol.Optional(CONF_LINES, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_NAME): cv.string}] }) @@ -61,14 +64,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): VasttrafikDepartureSensor( vasttrafik, planner, departure.get(CONF_NAME), departure.get(CONF_FROM), departure.get(CONF_HEADING), - departure.get(CONF_DELAY))) + departure.get(CONF_LINES), departure.get(CONF_DELAY))) add_devices(sensors, True) class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - def __init__(self, vasttrafik, planner, name, departure, heading, delay): + def __init__(self, vasttrafik, planner, name, departure, heading, + lines, delay): """Initialize the sensor.""" self._vasttrafik = vasttrafik self._planner = planner @@ -76,6 +80,7 @@ class VasttrafikDepartureSensor(Entity): self._departure = planner.location_name(departure)[0] self._heading = (planner.location_name(heading)[0] if heading else None) + self._lines = lines if lines else None self._delay = timedelta(minutes=delay) self._departureboard = None @@ -94,15 +99,18 @@ class VasttrafikDepartureSensor(Entity): """Return the state attributes.""" if not self._departureboard: return - departure = self._departureboard[0] - params = { - ATTR_ACCESSIBILITY: departure.get('accessibility', None), - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_DIRECTION: departure.get('direction', None), - ATTR_LINE: departure.get('sname', None), - ATTR_TRACK: departure.get('track', None), - } - return {k: v for k, v in params.items() if v} + + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + params = { + ATTR_ACCESSIBILITY: departure.get('accessibility'), + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_DIRECTION: departure.get('direction'), + ATTR_LINE: departure.get('sname'), + ATTR_TRACK: departure.get('track'), + } + return {k: v for k, v in params.items() if v} @property def state(self): @@ -113,9 +121,18 @@ class VasttrafikDepartureSensor(Entity): self._departure['name'], self._heading['name'] if self._heading else 'ANY') return - if 'rtTime' in self._departureboard[0]: - return self._departureboard[0]['rtTime'] - return self._departureboard[0]['time'] + for departure in self._departureboard: + line = departure.get('sname') + if not self._lines or line in self._lines: + if 'rtTime' in self._departureboard[0]: + return self._departureboard[0]['rtTime'] + return self._departureboard[0]['time'] + # No departures of given lines found + _LOGGER.debug( + "No departures from %s heading %s on line(s) %s", + self._departure['name'], + self._heading['name'] if self._heading else 'ANY', + ', '.join((str(line) for line in self._lines))) @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): From 2d7d8848cba8f6e78b8b27c14197e344d356c189 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 Mar 2018 23:48:50 +0100 Subject: [PATCH 108/924] Fix mysensors RGBW (#13364) Tuple doesn't have append method. --- homeassistant/components/light/mysensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index d595fc4b184..14a770b7632 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -104,7 +104,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): def _turn_on_rgb_and_w(self, hex_template, **kwargs): """Turn on RGB or RGBW child device.""" - rgb = color_util.color_hs_to_RGB(*self._hs) + rgb = list(color_util.color_hs_to_RGB(*self._hs)) white = self._white hex_color = self._values.get(self.value_type) hs_color = kwargs.get(ATTR_HS_COLOR) From 17cbd0f3c9d9d5d3ef2caa7d15ada64266206c94 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 21 Mar 2018 23:55:49 +0100 Subject: [PATCH 109/924] Add watt to mysensors switch attributes (#13370) --- homeassistant/components/switch/mysensors.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index 51184859fc6..b4a1dcde3e6 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -72,6 +72,12 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Return True if unable to access real state of entity.""" return self.gateway.optimistic + @property + def current_power_w(self): + """Return the current power usage in W.""" + set_req = self.gateway.const.SetReq + return self._values.get(set_req.V_WATT) + @property def is_on(self): """Return True if switch is on.""" From 1676df6a5f9af8767713eaffa6f4dce23e8d488e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 22 Mar 2018 01:06:41 +0000 Subject: [PATCH 110/924] Mediaroom async (#13321) * Initial commit on new asyncio based version * Async version * Lint * updated to lasted pymediaroom version * bump version * optimistic state updates * bump version * Moved class import to method import * async schedule and name correction * Addresses @balloob comments * missed fixes * no unique_id for configuration based STB * hound * handle 2 mediaroom platforms --- .../components/media_player/mediaroom.py | 340 ++++++++++++------ requirements_all.txt | 2 +- 2 files changed, 230 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index 3cf0ecdb232..a6d5841bb0f 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -9,134 +9,182 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, - SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, - SUPPORT_VOLUME_STEP, SUPPORT_VOLUME_MUTE, - MediaPlayerDevice) + MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, + SUPPORT_VOLUME_MUTE, MediaPlayerDevice, +) +from homeassistant.helpers.dispatcher import ( + dispatcher_send, async_dispatcher_connect +) from homeassistant.const import ( - CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, CONF_TIMEOUT, - STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_ON) + CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF, + CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, + STATE_UNAVAILABLE +) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymediaroom==0.5'] + +REQUIREMENTS = ['pymediaroom==0.6'] _LOGGER = logging.getLogger(__name__) -NOTIFICATION_TITLE = 'Mediaroom Media Player Setup' -NOTIFICATION_ID = 'mediaroom_notification' DEFAULT_NAME = 'Mediaroom STB' DEFAULT_TIMEOUT = 9 DATA_MEDIAROOM = "mediaroom_known_stb" +DISCOVERY_MEDIAROOM = "mediaroom_discovery_installed" +SIGNAL_STB_NOTIFY = 'mediaroom_stb_discovered' +SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ + | SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_PLAY_MEDIA \ + | SUPPORT_STOP | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ + | SUPPORT_PLAY -SUPPORT_MEDIAROOM = SUPPORT_PAUSE | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ - SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | \ - SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_NEXT_TRACK | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, -}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the Mediaroom platform.""" - hosts = [] - known_hosts = hass.data.get(DATA_MEDIAROOM) if known_hosts is None: known_hosts = hass.data[DATA_MEDIAROOM] = [] - host = config.get(CONF_HOST, None) - if host is None: - _LOGGER.info("Trying to discover Mediaroom STB") + if host: + async_add_devices([MediaroomDevice(host=host, + device_id=None, + optimistic=config[CONF_OPTIMISTIC], + timeout=config[CONF_TIMEOUT])]) + hass.data[DATA_MEDIAROOM].append(host) - from pymediaroom import Remote + _LOGGER.debug("Trying to discover Mediaroom STB") - host = Remote.discover(known_hosts) - if host is None: - _LOGGER.warning("Can't find any STB") + def callback_notify(notify): + """Process NOTIFY message from STB.""" + if notify.ip_address in hass.data[DATA_MEDIAROOM]: + dispatcher_send(hass, SIGNAL_STB_NOTIFY, notify) return - hosts.append(host) - known_hosts.append(host) - stbs = [] + _LOGGER.debug("Discovered new stb %s", notify.ip_address) + hass.data[DATA_MEDIAROOM].append(notify.ip_address) + new_stb = MediaroomDevice( + host=notify.ip_address, device_id=notify.device_uuid, + optimistic=False + ) + async_add_devices([new_stb]) - try: - for host in hosts: - stbs.append(MediaroomDevice( - config.get(CONF_NAME), - host, - config.get(CONF_OPTIMISTIC), - config.get(CONF_TIMEOUT) - )) + if not config[CONF_OPTIMISTIC]: + from pymediaroom import install_mediaroom_protocol - except ConnectionRefusedError: - hass.components.persistent_notification.create( - 'Error: Unable to initialize mediaroom at {}
' - 'Check its network connection or consider ' - 'using auto discovery.
' - 'You will need to restart hass after fixing.' - ''.format(host), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - add_devices(stbs) + already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False) + if not already_installed: + await install_mediaroom_protocol( + responses_callback=callback_notify) + _LOGGER.debug("Auto discovery installed") + hass.data[DISCOVERY_MEDIAROOM] = True class MediaroomDevice(MediaPlayerDevice): """Representation of a Mediaroom set-up-box on the network.""" - def __init__(self, name, host, optimistic=False, timeout=DEFAULT_TIMEOUT): + def set_state(self, mediaroom_state): + """Helper method to map pymediaroom states to HA states.""" + from pymediaroom import State + + state_map = { + State.OFF: STATE_OFF, + State.STANDBY: STATE_STANDBY, + State.PLAYING_LIVE_TV: STATE_PLAYING, + State.PLAYING_RECORDED_TV: STATE_PLAYING, + State.PLAYING_TIMESHIFT_TV: STATE_PLAYING, + State.STOPPED: STATE_PAUSED, + State.UNKNOWN: STATE_UNAVAILABLE + } + + self._state = state_map[mediaroom_state] + + def __init__(self, host, device_id, optimistic=False, + timeout=DEFAULT_TIMEOUT): """Initialize the device.""" from pymediaroom import Remote - self.stb = Remote(host, timeout=timeout) - _LOGGER.info( - "Found %s at %s%s", name, host, - " - I'm optimistic" if optimistic else "") - self._name = name - self._is_standby = not optimistic - self._current = None + self.host = host + self.stb = Remote(host) + _LOGGER.info("Found STB at %s%s", host, + " - I'm optimistic" if optimistic else "") + self._channel = None self._optimistic = optimistic - self._state = STATE_STANDBY + self._state = STATE_PLAYING if optimistic else STATE_STANDBY + self._name = 'Mediaroom {}'.format(device_id) + self._available = True + if device_id: + self._unique_id = device_id + else: + self._unique_id = None - def update(self): + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + async def async_added_to_hass(self): """Retrieve latest state.""" - if not self._optimistic: - self._is_standby = self.stb.get_standby() - if self._is_standby: - self._state = STATE_STANDBY - elif self._state not in [STATE_PLAYING, STATE_PAUSED]: - self._state = STATE_PLAYING - _LOGGER.debug( - "%s(%s) is [%s]", - self._name, self.stb.stb_ip, self._state) + async def async_notify_received(notify): + """Process STB state from NOTIFY message.""" + stb_state = self.stb.notify_callback(notify) + # stb_state is None in case the notify is not from the current stb + if not stb_state: + return + self.set_state(stb_state) + _LOGGER.debug("STB(%s) is [%s]", self.host, self._state) + self._available = True + self.async_schedule_update_ha_state() - def play_media(self, media_type, media_id, **kwargs): + async_dispatcher_connect(self.hass, SIGNAL_STB_NOTIFY, + async_notify_received) + + async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - _LOGGER.debug( - "%s(%s) Play media: %s (%s)", - self._name, self.stb.stb_ip, media_id, media_type) + from pymediaroom import PyMediaroomError + + _LOGGER.debug("STB(%s) Play media: %s (%s)", self.stb.stb_ip, + media_id, media_type) if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error('invalid media type') return - if media_id.isdigit(): - media_id = int(media_id) - else: + if not media_id.isdigit(): + _LOGGER.error("media_id must be a channel number") return - self.stb.send_cmd(media_id) - self._state = STATE_PLAYING + + try: + await self.stb.send_cmd(int(media_id)) + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): """Return the name of the device.""" return self._name - # MediaPlayerDevice properties and methods @property def state(self): """Return the state of the device.""" @@ -152,50 +200,120 @@ class MediaroomDevice(MediaPlayerDevice): """Return the content type of current playing media.""" return MEDIA_TYPE_CHANNEL - def turn_on(self): + @property + def media_channel(self): + """Channel currently playing.""" + return self._channel + + async def async_turn_on(self): """Turn on the receiver.""" - self.stb.send_cmd('Power') - self._state = STATE_ON + from pymediaroom import PyMediaroomError + try: + self.set_state(await self.stb.turn_on()) + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def turn_off(self): + async def async_turn_off(self): """Turn off the receiver.""" - self.stb.send_cmd('Power') - self._state = STATE_STANDBY + from pymediaroom import PyMediaroomError + try: + self.set_state(await self.stb.turn_off()) + if self._optimistic: + self._state = STATE_STANDBY + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_play(self): + async def async_media_play(self): """Send play command.""" - _LOGGER.debug("media_play()") - self.stb.send_cmd('PlayPause') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + _LOGGER.debug("media_play()") + await self.stb.send_cmd('PlayPause') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_pause(self): + async def async_media_pause(self): """Send pause command.""" - self.stb.send_cmd('PlayPause') - self._state = STATE_PAUSED + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('PlayPause') + if self._optimistic: + self._state = STATE_PAUSED + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_stop(self): + async def async_media_stop(self): """Send stop command.""" - self.stb.send_cmd('Stop') - self._state = STATE_PAUSED + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('Stop') + if self._optimistic: + self._state = STATE_PAUSED + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_previous_track(self): + async def async_media_previous_track(self): """Send Program Down command.""" - self.stb.send_cmd('ProgDown') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('ProgDown') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def media_next_track(self): + async def async_media_next_track(self): """Send Program Up command.""" - self.stb.send_cmd('ProgUp') - self._state = STATE_PLAYING + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('ProgUp') + if self._optimistic: + self._state = STATE_PLAYING + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - self.stb.send_cmd('VolUp') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('VolUp') + self._available = True + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def volume_down(self): + async def async_volume_down(self): """Send volume up command.""" - self.stb.send_cmd('VolDown') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('VolDown') + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Send mute command.""" - self.stb.send_cmd('Mute') + from pymediaroom import PyMediaroomError + try: + await self.stb.send_cmd('Mute') + except PyMediaroomError: + self._available = False + self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index f3637a40f64..0c68d6a29ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -804,7 +804,7 @@ pylutron==0.1.0 pymailgunner==1.4 # homeassistant.components.media_player.mediaroom -pymediaroom==0.5 +pymediaroom==0.6 # homeassistant.components.media_player.xiaomi_tv pymitv==1.0.0 From 6e75c5427cec9e97624368befadd4eb819799d5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 21 Mar 2018 21:22:16 -0700 Subject: [PATCH 111/924] Update frontend to 20180322.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index eccc47e05c7..9107e64a040 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180316.0'] +REQUIREMENTS = ['home-assistant-frontend==20180322.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0c68d6a29ae..793403f26d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180316.0 +home-assistant-frontend==20180322.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a7c3b493d4..d2c1df2d3bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180316.0 +home-assistant-frontend==20180322.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e9cdbe5d8c5b659a3c80dbd1346192418a6839d8 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 22 Mar 2018 13:34:02 +0100 Subject: [PATCH 112/924] Add language parameter to darksky sensor (#13297) --- .coveragerc | 1 - homeassistant/components/sensor/darksky.py | 24 ++++- tests/components/sensor/test_darksky.py | 120 ++++++++++++++++++--- 3 files changed, 125 insertions(+), 20 deletions(-) diff --git a/.coveragerc b/.coveragerc index 1dcde0ded14..a2c0dde77b1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -561,7 +561,6 @@ omit = homeassistant/components/sensor/crimereports.py homeassistant/components/sensor/cups.py homeassistant/components/sensor/currencylayer.py - homeassistant/components/sensor/darksky.py homeassistant/components/sensor/deluge.py homeassistant/components/sensor/deutsche_bahn.py homeassistant/components/sensor/dht.py diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 3049415c754..261e0a62409 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -27,6 +27,9 @@ CONF_ATTRIBUTION = "Powered by Dark Sky" CONF_UNITS = 'units' CONF_UPDATE_INTERVAL = 'update_interval' CONF_FORECAST = 'forecast' +CONF_LANGUAGE = 'language' + +DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' @@ -118,6 +121,16 @@ CONDITION_PICTURES = { 'partly-cloudy-night': '/static/images/darksky/weather-cloudy.svg', } +# Language Supported Codes +LANGUAGE_CODES = [ + 'ar', 'az', 'be', 'bg', 'bs', 'ca', + 'cs', 'da', 'de', 'el', 'en', 'es', + 'et', 'fi', 'fr', 'hr', 'hu', 'id', + 'is', 'it', 'ja', 'ka', 'kw', 'nb', + 'nl', 'pl', 'pt', 'ro', 'ru', 'sk', + 'sl', 'sr', 'sv', 'tet', 'tr', 'uk', + 'x-pig-latin', 'zh', 'zh-tw', +] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MONITORED_CONDITIONS): @@ -125,6 +138,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']), + vol.Optional(CONF_LANGUAGE, + default=DEFAULT_LANGUAGE): vol.In(LANGUAGE_CODES), vol.Inclusive(CONF_LATITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', @@ -140,6 +155,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Dark Sky sensor.""" latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + language = config.get(CONF_LANGUAGE) if CONF_UNITS in config: units = config[CONF_UNITS] @@ -153,6 +169,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): latitude=latitude, longitude=longitude, units=units, + language=language, interval=config.get(CONF_UPDATE_INTERVAL)) forecast_data.update() forecast_data.update_currently() @@ -332,12 +349,14 @@ def convert_to_camel(data): class DarkSkyData(object): """Get the latest data from Darksky.""" - def __init__(self, api_key, latitude, longitude, units, interval): + def __init__(self, api_key, latitude, longitude, units, language, + interval): """Initialize the data object.""" self._api_key = api_key self.latitude = latitude self.longitude = longitude self.units = units + self.language = language self.data = None self.unit_system = None @@ -359,7 +378,8 @@ class DarkSkyData(object): try: self.data = forecastio.load_forecast( - self._api_key, self.latitude, self.longitude, units=self.units) + self._api_key, self.latitude, self.longitude, units=self.units, + lang=self.language) except (ConnectError, HTTPError, Timeout, ValueError) as error: _LOGGER.error("Unable to connect to Dark Sky. %s", error) self.data = None diff --git a/tests/components/sensor/test_darksky.py b/tests/components/sensor/test_darksky.py index 7ee04b0df4c..9300ecef432 100644 --- a/tests/components/sensor/test_darksky.py +++ b/tests/components/sensor/test_darksky.py @@ -2,16 +2,69 @@ import re import unittest from unittest.mock import MagicMock, patch +from datetime import timedelta -import forecastio from requests.exceptions import HTTPError import requests_mock -from datetime import timedelta + +import forecastio from homeassistant.components.sensor import darksky from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant +from tests.common import (load_fixture, get_test_home_assistant, + MockDependency) + +VALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'monitored_conditions': ['summary', 'icon', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + +INVALID_CONFIG_MINIMAL = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'monitored_conditions': ['sumary', 'iocn', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + +VALID_CONFIG_LANG_DE = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'units': 'us', + 'language': 'de', + 'monitored_conditions': ['summary', 'icon', 'temperature_max', + 'minutely_summary', 'hourly_summary', + 'daily_summary', 'humidity', ], + 'update_interval': timedelta(seconds=120), + } +} + +INVALID_CONFIG_LANG = { + 'sensor': { + 'platform': 'darksky', + 'api_key': 'foo', + 'forecast': [1, 2], + 'language': 'yz', + 'monitored_conditions': ['summary', 'icon', 'temperature_max'], + 'update_interval': timedelta(seconds=120), + } +} + + +def load_forecastMock(key, lat, lon, + units, lang): # pylint: disable=invalid-name + """Mock darksky forecast loading.""" + return '' class TestDarkSkySetup(unittest.TestCase): @@ -30,12 +83,6 @@ class TestDarkSkySetup(unittest.TestCase): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() self.key = 'foo' - self.config = { - 'api_key': 'foo', - 'forecast': [1, 2], - 'monitored_conditions': ['summary', 'icon', 'temperature_max'], - 'update_interval': timedelta(seconds=120), - } self.lat = self.hass.config.latitude = 37.8267 self.lon = self.hass.config.longitude = -122.423 self.entities = [] @@ -44,10 +91,41 @@ class TestDarkSkySetup(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - def test_setup_with_config(self): + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_config(self, mock_forecastio): """Test the platform setup with configuration.""" - self.assertTrue( - setup_component(self.hass, 'sensor', {'darksky': self.config})) + setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_invalid_config(self, mock_forecastio): + """Test the platform setup with invalid configuration.""" + setup_component(self.hass, 'sensor', INVALID_CONFIG_MINIMAL) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_language_config(self, mock_forecastio): + """Test the platform setup with language configuration.""" + setup_component(self.hass, 'sensor', VALID_CONFIG_LANG_DE) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + + @MockDependency('forecastio') + @patch('forecastio.load_forecast', new=load_forecastMock) + def test_setup_with_invalid_language_config(self, mock_forecastio): + """Test the platform setup with language configuration.""" + setup_component(self.hass, 'sensor', INVALID_CONFIG_LANG) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is None @patch('forecastio.api.get_forecast') def test_setup_bad_api_key(self, mock_get_forecast): @@ -60,7 +138,8 @@ class TestDarkSkySetup(unittest.TestCase): msg = '400 Client Error: Bad Request for url: {}'.format(url) mock_get_forecast.side_effect = HTTPError(msg,) - response = darksky.setup_platform(self.hass, self.config, MagicMock()) + response = darksky.setup_platform(self.hass, VALID_CONFIG_MINIMAL, + MagicMock()) self.assertFalse(response) @requests_mock.Mocker() @@ -69,9 +148,16 @@ class TestDarkSkySetup(unittest.TestCase): """Test for successfully setting up the forecast.io platform.""" uri = (r'https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/' r'(-?\d+\.?\d*),(-?\d+\.?\d*)') - mock_req.get(re.compile(uri), - text=load_fixture('darksky.json')) - darksky.setup_platform(self.hass, self.config, self.add_entities) + mock_req.get(re.compile(uri), text=load_fixture('darksky.json')) + + assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL) + self.assertTrue(mock_get_forecast.called) self.assertEqual(mock_get_forecast.call_count, 1) - self.assertEqual(len(self.entities), 7) + self.assertEqual(len(self.hass.states.entity_ids()), 7) + + state = self.hass.states.get('sensor.dark_sky_summary') + assert state is not None + self.assertEqual(state.state, 'Clear') + self.assertEqual(state.attributes.get('friendly_name'), + 'Dark Sky Summary') From 98620d8ce848ded4de5d67e90d8333c0782a25bd Mon Sep 17 00:00:00 2001 From: Jeroen ter Heerdt Date: Thu, 22 Mar 2018 18:53:52 +0100 Subject: [PATCH 113/924] Fixing Egardia 'home armed' state not shown correctly. (#13335) * Fixing Egardia 'home armed' state not shown correctly. * Updating requirements_all. * Adding DEPEDENCY list to Egardia components. * updating requirements_all --- homeassistant/components/alarm_control_panel/egardia.py | 7 +++++-- homeassistant/components/binary_sensor/egardia.py | 2 +- homeassistant/components/egardia.py | 2 +- requirements_all.txt | 3 +-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/egardia.py b/homeassistant/components/alarm_control_panel/egardia.py index 845eb81bbe0..f0db378ec15 100644 --- a/homeassistant/components/alarm_control_panel/egardia.py +++ b/homeassistant/components/alarm_control_panel/egardia.py @@ -12,13 +12,14 @@ import requests import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_TRIGGERED, + STATE_ALARM_ARMED_NIGHT) from homeassistant.components.egardia import ( EGARDIA_DEVICE, EGARDIA_SERVER, REPORT_SERVER_CODES_IGNORE, CONF_REPORT_SERVER_CODES, CONF_REPORT_SERVER_ENABLED, CONF_REPORT_SERVER_PORT ) -REQUIREMENTS = ['pythonegardia==1.0.38'] +DEPENDENCIES = ['egardia'] _LOGGER = logging.getLogger(__name__) @@ -27,6 +28,8 @@ STATES = { 'DAY HOME': STATE_ALARM_ARMED_HOME, 'DISARM': STATE_ALARM_DISARMED, 'ARMHOME': STATE_ALARM_ARMED_HOME, + 'HOME': STATE_ALARM_ARMED_HOME, + 'NIGHT HOME': STATE_ALARM_ARMED_NIGHT, 'TRIGGERED': STATE_ALARM_TRIGGERED } diff --git a/homeassistant/components/binary_sensor/egardia.py b/homeassistant/components/binary_sensor/egardia.py index ab88de9d3c9..76d90e78376 100644 --- a/homeassistant/components/binary_sensor/egardia.py +++ b/homeassistant/components/binary_sensor/egardia.py @@ -12,7 +12,7 @@ from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.egardia import ( EGARDIA_DEVICE, ATTR_DISCOVER_DEVICES) _LOGGER = logging.getLogger(__name__) - +DEPENDENCIES = ['egardia'] EGARDIA_TYPE_TO_DEVICE_CLASS = {'IR Sensor': 'motion', 'Door Contact': 'opening', 'IR': 'motion'} diff --git a/homeassistant/components/egardia.py b/homeassistant/components/egardia.py index 2cfc44a407b..f350ea56bb4 100644 --- a/homeassistant/components/egardia.py +++ b/homeassistant/components/egardia.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_PORT, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, EVENT_HOMEASSISTANT_STOP) -REQUIREMENTS = ['pythonegardia==1.0.38'] +REQUIREMENTS = ['pythonegardia==1.0.39'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 793403f26d7..837770d7c11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1006,8 +1006,7 @@ python_opendata_transport==0.0.3 python_openzwave==0.4.3 # homeassistant.components.egardia -# homeassistant.components.alarm_control_panel.egardia -pythonegardia==1.0.38 +pythonegardia==1.0.39 # homeassistant.components.sensor.whois pythonwhois==2.4.3 From fb1fafefabe151421f7f4b94835b05936e2e577c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 22 Mar 2018 12:21:33 -0700 Subject: [PATCH 114/924] Include all config flow translations with backend translations (#13394) --- homeassistant/helpers/translation.py | 3 ++- tests/helpers/test_translation.py | 28 +++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 9d1773de4d2..26cb34ede8c 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -4,6 +4,7 @@ import logging from typing import Optional # NOQA from os import path +from homeassistant import config_entries from homeassistant.loader import get_component, bind_hass from homeassistant.util.json import load_json @@ -89,7 +90,7 @@ async def async_get_component_resources(hass, language): translation_cache = hass.data[TRANSLATION_STRING_CACHE][language] # Get the set of components - components = hass.config.components + components = hass.config.components | set(config_entries.FLOWS) # Calculate the missing components missing_components = components - set(translation_cache) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 840f665f410..c72efca8c29 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -1,11 +1,23 @@ """Test the translation helper.""" # pylint: disable=protected-access from os import path +from unittest.mock import patch +import pytest + +from homeassistant import config_entries import homeassistant.helpers.translation as translation from homeassistant.setup import async_setup_component +@pytest.fixture +def mock_config_flows(): + """Mock the config flows.""" + flows = [] + with patch.object(config_entries, 'FLOWS', flows): + yield flows + + def test_flatten(): """Test the flatten function.""" data = { @@ -71,7 +83,7 @@ def test_load_translations_files(hass): } -async def test_get_translations(hass): +async def test_get_translations(hass, mock_config_flows): """Test the get translations helper.""" translations = await translation.async_get_translations(hass, 'en') assert translations == {} @@ -106,3 +118,17 @@ async def test_get_translations(hass): 'component.switch.state.string1': 'Value 1', 'component.switch.state.string2': 'Value 2', } + + +async def test_get_translations_loads_config_flows(hass, mock_config_flows): + """Test the get translations helper loads config flow translations.""" + mock_config_flows.append('component1') + + with patch.object(translation, 'component_translation_file', + return_value='bla.json'), \ + patch.object(translation, 'load_translations_files', return_value={ + 'component1': {'hello': 'world'}}): + translations = await translation.async_get_translations(hass, 'en') + assert translations == { + 'component.component1.hello': 'world' + } From c50b00226c221c395143d2a92c8696eacdf60e01 Mon Sep 17 00:00:00 2001 From: Gerard Date: Fri, 23 Mar 2018 07:32:33 +0100 Subject: [PATCH 115/924] Avoid breaking change for BMW ConnectedDrive sensors in #12591 (#13380) --- .../binary_sensor/bmw_connected_drive.py | 15 ++++++++------- .../components/lock/bmw_connected_drive.py | 3 ++- .../components/sensor/bmw_connected_drive.py | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0c848a57fbf..0f3edd86dcd 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -15,8 +15,8 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'all_lids_closed': ['Doors', 'opening'], - 'all_windows_closed': ['Windows', 'opening'], + 'lids': ['Doors', 'opening'], + 'windows': ['Windows', 'opening'], 'door_lock_state': ['Door lock state', 'safety'] } @@ -45,7 +45,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = sensor_name + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name self._device_class = device_class self._state = None @@ -77,10 +78,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): 'car': self._vehicle.modelName } - if self._attribute == 'all_lids_closed': + if self._attribute == 'lids': for lid in vehicle_state.lids: result[lid.name] = lid.state.value - elif self._attribute == 'all_windows_closed': + elif self._attribute == 'windows': for window in vehicle_state.windows: result[window.name] = window.state.value elif self._attribute == 'door_lock_state': @@ -93,10 +94,10 @@ class BMWConnectedDriveSensor(BinarySensorDevice): vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed - if self._attribute == 'all_lids_closed': + if self._attribute == 'lids': _LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed) self._state = not vehicle_state.all_lids_closed - if self._attribute == 'all_windows_closed': + if self._attribute == 'windows': self._state = not vehicle_state.all_windows_closed # device class safety: On means unsafe, Off means safe if self._attribute == 'door_lock_state': diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index 4592fd7cae9..c500e02b2f7 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -37,7 +37,8 @@ class BMWLock(LockDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = sensor_name + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name self._state = None @property diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 76719763931..3208c7377df 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -51,7 +51,8 @@ class BMWConnectedDriveSensor(Entity): self._attribute = attribute self._state = None self._unit_of_measurement = None - self._name = sensor_name + self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._sensor_name = sensor_name self._icon = icon @property From 2c7bc6eaf8953b222c4f4b99eaf36bd58349b05e Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Fri, 23 Mar 2018 11:30:44 +0100 Subject: [PATCH 116/924] Support setting icon when configuring MQTT entity (#13304) --- homeassistant/components/sensor/mqtt.py | 12 ++++++++++-- homeassistant/components/switch/mqtt.py | 13 +++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index b19f5721e4f..d191b9a22e8 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON) from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -36,6 +36,7 @@ DEPENDENCIES = ['mqtt'] PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -59,6 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_UNIT_OF_MEASUREMENT), config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), + config.get(CONF_ICON), value_template, config.get(CONF_JSON_ATTRS), config.get(CONF_AVAILABILITY_TOPIC), @@ -71,7 +73,7 @@ class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, value_template, + force_update, expire_after, icon, value_template, json_attributes, availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" @@ -85,6 +87,7 @@ class MqttSensor(MqttAvailability, Entity): self._force_update = force_update self._template = value_template self._expire_after = expire_after + self._icon = icon self._expiration_trigger = None self._json_attributes = set(json_attributes) self._attributes = None @@ -170,3 +173,8 @@ class MqttSensor(MqttAvailability, Entity): def device_state_attributes(self): """Return the state attributes.""" return self._attributes + + @property + def icon(self): + """Return the icon.""" + return self._icon diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index a4aea1ded9f..f3bd0bef012 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -17,7 +17,7 @@ from homeassistant.components.mqtt import ( from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON) + CONF_PAYLOAD_ON, CONF_ICON) import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -32,6 +32,7 @@ DEFAULT_OPTIMISTIC = False PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -50,6 +51,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices([MqttSwitch( config.get(CONF_NAME), + config.get(CONF_ICON), config.get(CONF_STATE_TOPIC), config.get(CONF_COMMAND_TOPIC), config.get(CONF_AVAILABILITY_TOPIC), @@ -67,7 +69,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class MqttSwitch(MqttAvailability, SwitchDevice): """Representation of a switch that can be toggled using MQTT.""" - def __init__(self, name, state_topic, command_topic, availability_topic, + def __init__(self, name, icon, + state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" @@ -75,6 +78,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): payload_not_available) self._state = False self._name = name + self._icon = icon self._state_topic = state_topic self._command_topic = command_topic self._qos = qos @@ -130,6 +134,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return true if we do optimistic updates.""" return self._optimistic + @property + def icon(self): + """Return the icon.""" + return self._icon + @asyncio.coroutine def async_turn_on(self, **kwargs): """Turn the device on. From ba7178dc0cc792707463d1e1ceb17fcf1aff69e7 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 23 Mar 2018 18:06:07 +0100 Subject: [PATCH 117/924] Enhance mysensors sensor units and icons (#13365) --- homeassistant/components/sensor/mysensors.py | 83 +++++++++++--------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index a8daf212e57..3876b260dfc 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -8,6 +8,31 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +SENSORS = { + 'V_TEMP': [None, 'mdi:thermometer'], + 'V_HUM': ['%', 'mdi:water-percent'], + 'V_DIMMER': ['%', 'mdi:percent'], + 'V_LIGHT_LEVEL': ['%', 'white-balance-sunny'], + 'V_DIRECTION': ['°', 'mdi:compass'], + 'V_WEIGHT': ['kg', 'mdi:weight-kilogram'], + 'V_DISTANCE': ['m', 'mdi:ruler'], + 'V_IMPEDANCE': ['ohm', None], + 'V_WATT': ['W', None], + 'V_KWH': ['kWh', None], + 'V_FLOW': ['m', None], + 'V_VOLUME': ['m³', None], + 'V_VOLTAGE': ['V', 'mdi:flash'], + 'V_CURRENT': ['A', 'mdi:flash-auto'], + 'V_PERCENTAGE': ['%', 'mdi:percent'], + 'V_LEVEL': { + 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], + 'S_LIGHT_LEVEL': ['lux', 'white-balance-sunny']}, + 'V_ORP': ['mV', None], + 'V_EC': ['μS/cm', None], + 'V_VAR': ['var', None], + 'V_VA': ['VA', None], +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" @@ -32,45 +57,29 @@ class MySensorsSensor(mysensors.MySensorsEntity): """Return the state of the device.""" return self._values.get(self.value_type) + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + _, icon = self._get_sensor_type() + return icon + @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" + set_req = self.gateway.const.SetReq + if (float(self.gateway.protocol_version) >= 1.5 and + set_req.V_UNIT_PREFIX in self._values): + return self._values[set_req.V_UNIT_PREFIX] + unit, _ = self._get_sensor_type() + return unit + + def _get_sensor_type(self): + """Return list with unit and icon of sensor type.""" pres = self.gateway.const.Presentation set_req = self.gateway.const.SetReq - unit_map = { - set_req.V_TEMP: (TEMP_CELSIUS - if self.gateway.metric else TEMP_FAHRENHEIT), - set_req.V_HUM: '%', - set_req.V_DIMMER: '%', - set_req.V_LIGHT_LEVEL: '%', - set_req.V_DIRECTION: '°', - set_req.V_WEIGHT: 'kg', - set_req.V_DISTANCE: 'm', - set_req.V_IMPEDANCE: 'ohm', - set_req.V_WATT: 'W', - set_req.V_KWH: 'kWh', - set_req.V_FLOW: 'm', - set_req.V_VOLUME: 'm³', - set_req.V_VOLTAGE: 'V', - set_req.V_CURRENT: 'A', - } - if float(self.gateway.protocol_version) >= 1.5: - if set_req.V_UNIT_PREFIX in self._values: - return self._values[ - set_req.V_UNIT_PREFIX] - unit_map.update({ - set_req.V_PERCENTAGE: '%', - set_req.V_LEVEL: { - pres.S_SOUND: 'dB', pres.S_VIBRATION: 'Hz', - pres.S_LIGHT_LEVEL: 'lux'}}) - if float(self.gateway.protocol_version) >= 2.0: - unit_map.update({ - set_req.V_ORP: 'mV', - set_req.V_EC: 'μS/cm', - set_req.V_VAR: 'var', - set_req.V_VA: 'VA', - }) - unit = unit_map.get(self.value_type) - if isinstance(unit, dict): - unit = unit.get(self.child_type) - return unit + SENSORS[set_req.V_TEMP.name][0] = ( + TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) + sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) + if isinstance(sensor_type, dict): + sensor_type = sensor_type.get(pres(self.child_type).name) + return sensor_type From 79c9d3ba102dcc23f32504e2f0d1774f44e9bf81 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 23 Mar 2018 18:08:16 +0100 Subject: [PATCH 118/924] Fix incorrect unit of measurement for precip_intensity. (#13415) --- homeassistant/components/sensor/darksky.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 261e0a62409..7d535c5f1d9 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -54,7 +54,8 @@ SENSOR_TYPES = { 'mdi:weather-pouring', ['currently', 'minutely', 'hourly', 'daily']], 'precip_intensity': ['Precip Intensity', - 'mm', 'in', 'mm', 'mm', 'mm', 'mdi:weather-rainy', + 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', + 'mdi:weather-rainy', ['currently', 'minutely', 'hourly', 'daily']], 'precip_probability': ['Precip Probability', '%', '%', '%', '%', '%', 'mdi:water-percent', @@ -100,7 +101,8 @@ SENSOR_TYPES = { '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], 'precip_intensity_max': ['Daily Max Precip Intensity', - 'mm', 'in', 'mm', 'mm', 'mm', 'mdi:thermometer', + 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', + 'mdi:thermometer', ['currently', 'hourly', 'daily']], 'uv_index': ['UV Index', UNIT_UV_INDEX, UNIT_UV_INDEX, UNIT_UV_INDEX, From 5ec6f25d4e1c179931c5b171702a9231eef34bfe Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 23 Mar 2018 18:09:18 +0100 Subject: [PATCH 119/924] Fix Sonos playing Sveriges Radio (#13401) --- homeassistant/components/media_player/sonos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 34ef146fc05..b10c761d532 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -321,7 +321,7 @@ def _is_radio_uri(uri): """Return whether the URI is a radio stream.""" radio_schemes = ( 'x-rincon-mp3radio:', 'x-sonosapi-stream:', 'x-sonosapi-radio:', - 'hls-radio:') + 'x-sonosapi-hls:', 'hls-radio:') return uri.startswith(radio_schemes) From 23f06b0040649e41dc4ff3fcbf509455b90fa7e8 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Fri, 23 Mar 2018 11:10:52 -0600 Subject: [PATCH 120/924] Cache LaMetric devices for offline use (#13379) If the connection to the LaMetric server fails, we should still be able to send notifications to known and reachable devices. --- homeassistant/components/notify/lametric.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index f4c9c391408..895ffd9db10 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/notify.lametric/ """ import logging +from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.notify import ( @@ -49,6 +50,7 @@ class LaMetricNotificationService(BaseNotificationService): self._icon = icon self._lifetime = lifetime self._cycles = cycles + self._devices = [] # pylint: disable=broad-except def send_message(self, message="", **kwargs): @@ -86,12 +88,15 @@ class LaMetricNotificationService(BaseNotificationService): model = Model(frames=frames, cycles=cycles, sound=sound) lmn = self.hasslametricmanager.manager try: - devices = lmn.get_devices() + self._devices = lmn.get_devices() except TokenExpiredError: _LOGGER.debug("Token expired, fetching new token") lmn.get_token() - devices = lmn.get_devices() - for dev in devices: + self._devices = lmn.get_devices() + except RequestsConnectionError: + _LOGGER.warning("Problem connecting to LaMetric, " + "using cached devices instead") + for dev in self._devices: if targets is None or dev["name"] in targets: try: lmn.set_device(dev) From 23165cbd1ac8ba1528649c04b56d598664e1da8b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 23 Mar 2018 18:11:53 +0100 Subject: [PATCH 121/924] Enhance mysensors binary sensor device classes (#13367) --- .../components/binary_sensor/mysensors.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 19fa02f63df..1e9359b6902 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -9,6 +9,17 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, DOMAIN, BinarySensorDevice) from homeassistant.const import STATE_ON +SENSORS = { + 'S_DOOR': 'door', + 'S_MOTION': 'motion', + 'S_SMOKE': 'smoke', + 'S_SPRINKLER': 'safety', + 'S_WATER_LEAK': 'safety', + 'S_SOUND': 'sound', + 'S_VIBRATION': 'vibration', + 'S_MOISTURE': 'moisture', +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MySensors platform for binary sensors.""" @@ -29,18 +40,7 @@ class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" pres = self.gateway.const.Presentation - class_map = { - pres.S_DOOR: 'opening', - pres.S_MOTION: 'motion', - pres.S_SMOKE: 'smoke', - } - if float(self.gateway.protocol_version) >= 1.5: - class_map.update({ - pres.S_SPRINKLER: 'sprinkler', - pres.S_WATER_LEAK: 'leak', - pres.S_SOUND: 'sound', - pres.S_VIBRATION: 'vibration', - pres.S_MOISTURE: 'moisture', - }) - if class_map.get(self.child_type) in DEVICE_CLASSES: - return class_map.get(self.child_type) + device_class = SENSORS.get(pres(self.child_type).name) + if device_class in DEVICE_CLASSES: + return device_class + return None From 8852e526010ef549ed288209056034092870020f Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Fri, 23 Mar 2018 10:22:01 -0700 Subject: [PATCH 122/924] Switched to async/await. Bumped pyxeoma version (#13404) --- homeassistant/components/camera/xeoma.py | 16 +++++++--------- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/xeoma.py b/homeassistant/components/camera/xeoma.py index 5836a9c94dc..cec04b52047 100644 --- a/homeassistant/components/camera/xeoma.py +++ b/homeassistant/components/camera/xeoma.py @@ -4,7 +4,6 @@ Support for Xeoma Cameras. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/camera.xeoma/ """ -import asyncio import logging import voluptuous as vol @@ -14,7 +13,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['pyxeoma==1.3'] +REQUIREMENTS = ['pyxeoma==1.4.0'] _LOGGER = logging.getLogger(__name__) @@ -41,8 +40,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Discover and setup Xeoma Cameras.""" from pyxeoma.xeoma import Xeoma, XeomaError @@ -53,8 +52,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): xeoma = Xeoma(host, login, password) try: - yield from xeoma.async_test_connection() - discovered_image_names = yield from xeoma.async_get_image_names() + await xeoma.async_test_connection() + discovered_image_names = await xeoma.async_get_image_names() discovered_cameras = [ { CONF_IMAGE_NAME: image_name, @@ -103,12 +102,11 @@ class XeomaCamera(Camera): self._password = password self._last_image = None - @asyncio.coroutine - def async_camera_image(self): + async def async_camera_image(self): """Return a still image response from the camera.""" from pyxeoma.xeoma import XeomaError try: - image = yield from self._xeoma.async_get_camera_image( + image = await self._xeoma.async_get_camera_image( self._image, self._username, self._password) self._last_image = image except XeomaError as err: diff --git a/requirements_all.txt b/requirements_all.txt index 837770d7c11..75fd6de8f46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1048,7 +1048,7 @@ pywebpush==1.6.0 pywemo==0.4.25 # homeassistant.components.camera.xeoma -pyxeoma==1.3 +pyxeoma==1.4.0 # homeassistant.components.zabbix pyzabbix==0.7.4 From 553920780f99c56dcbd3ee7dd7f61b48c0d81e18 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 23 Mar 2018 18:22:48 +0100 Subject: [PATCH 123/924] Added default return value for HS_Color (#13395) --- homeassistant/components/homekit/type_lights.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 6cd60698110..c723fcc08a6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -134,7 +134,8 @@ class Light(HomeAccessory): # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: - hue, saturation = new_state.attributes.get(ATTR_HS_COLOR) + hue, saturation = new_state.attributes.get( + ATTR_HS_COLOR, (None, None)) if not self._flag[RGB_COLOR] and ( hue != self._hue or saturation != self._saturation): self.char_hue.set_value(hue, should_callback=False) From 2497dd5e33d227e24d152360e39fefa926a167ae Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Fri, 23 Mar 2018 14:01:40 -0400 Subject: [PATCH 124/924] Hue: Use the currently active color mode (#13376) * Hue: Use the currently active color mode * Round hue/sat colors before reporting to API * .gitignore cache fix --- .gitignore | 1 + homeassistant/components/light/__init__.py | 4 +++ homeassistant/components/light/hue.py | 15 +++++++++ tests/components/light/test_hue.py | 37 +++++++++++++++++++++- 4 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 33a1f4f9a4b..bf49a1b61c1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ Icon # pytest .pytest_cache +.cache # GITHUB Proposed Python stuff: *.py[cod] diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f03521947b7..eea6c821fc0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -501,6 +501,10 @@ class Light(ToggleEntity): *data[ATTR_HS_COLOR]) data[ATTR_XY_COLOR] = color_util.color_hs_to_xy( *data[ATTR_HS_COLOR]) + data[ATTR_HS_COLOR] = ( + round(data[ATTR_HS_COLOR][0], 3), + round(data[ATTR_HS_COLOR][1], 3), + ) return data diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index b1562aaba8f..71e3d4fa30b 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -225,9 +225,20 @@ class HueLight(Light): return self.light.action.get('bri') return self.light.state.get('bri') + @property + def _color_mode(self): + """Return the hue color mode.""" + if self.is_group: + return self.light.action.get('colormode') + return self.light.state.get('colormode') + @property def hs_color(self): """Return the hs color value.""" + # Don't return hue/sat if in color temperature mode + if self._color_mode == "ct": + return None + if self.is_group: return ( self.light.action.get('hue') / 65535 * 360, @@ -241,6 +252,10 @@ class HueLight(Light): @property def color_temp(self): """Return the CT color value.""" + # Don't return color temperature unless in color temperature mode + if self._color_mode != "ct": + return None + if self.is_group: return self.light.action.get('ct') return self.light.state.get('ct') diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 8abf51fdf0c..54bb2184a64 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -227,13 +227,48 @@ async def test_lights(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['color_temp'] == 467 + assert lamp_1.attributes['hs_color'] == (71.896, 83.137) lamp_2 = hass.states.get('light.hue_lamp_2') assert lamp_2 is not None assert lamp_2.state == 'off' +async def test_lights_color_mode(hass, mock_bridge): + """Test that lights only report appropriate color mode.""" + mock_bridge.mock_light_responses.append(LIGHT_RESPONSE) + await setup_bridge(hass, mock_bridge) + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert 'color_temp' not in lamp_1.attributes + + new_light1_on = LIGHT_1_ON.copy() + new_light1_on['state'] = new_light1_on['state'].copy() + new_light1_on['state']['colormode'] = 'ct' + mock_bridge.mock_light_responses.append({ + "1": new_light1_on, + }) + mock_bridge.mock_group_responses.append({}) + + # Calling a service will trigger the updates to run + await hass.services.async_call('light', 'turn_on', { + 'entity_id': 'light.hue_lamp_2' + }, blocking=True) + # 2x light update, 1 turn on request + assert len(mock_bridge.mock_requests) == 3 + + lamp_1 = hass.states.get('light.hue_lamp_1') + assert lamp_1 is not None + assert lamp_1.state == 'on' + assert lamp_1.attributes['brightness'] == 144 + assert lamp_1.attributes['color_temp'] == 467 + assert 'hs_color' not in lamp_1.attributes + + async def test_groups(hass, mock_bridge): """Test the update_lights function with some lights.""" mock_bridge.allow_groups = True From df8596e896af9c4858d3eb477bf5119bbe2e2086 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Fri, 23 Mar 2018 19:05:02 +0100 Subject: [PATCH 125/924] Cleanup homematicip_cloud (#13356) * Cleanup and proposed changes from MartinHjelmare * Removed coroutine decorator from async_added_to_hass * Added blank line * Fix of component url * Fix of component url * Fix url of the sensor component --- homeassistant/components/homematicip_cloud.py | 16 ++++-- .../components/sensor/homematicip_cloud.py | 57 ++++++++++--------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index a89678624eb..180d6943d8a 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -2,13 +2,14 @@ Support for HomematicIP components. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip/ +https://home-assistant.io/components/homematicip_cloud/ """ import logging from socket import timeout import voluptuous as vol + from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import (dispatcher_send, @@ -49,12 +50,14 @@ ATTR_FIRMWARE_STATE = 'firmware_state' ATTR_LOW_BATTERY = 'low_battery' ATTR_SABOTAGE = 'sabotage' ATTR_RSSI = 'rssi' +ATTR_TYPE = 'type' def setup(hass, config): """Set up the HomematicIP component.""" # pylint: disable=import-error, no-name-in-module from homematicip.home import Home + hass.data.setdefault(DOMAIN, {}) homes = hass.data[DOMAIN] accesspoints = config.get(DOMAIN, []) @@ -100,19 +103,21 @@ def setup(hass, config): _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) for component in ['sensor']: - load_platform(hass, component, DOMAIN, - {'homeid': home.id}, config) + load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) + return True class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, hass, home, device, signal=None): + def __init__(self, home, device): """Initialize the generic device.""" - self.hass = hass self._home = home self._device = device + + async def async_added_to_hass(self): + """Register callbacks.""" async_dispatcher_connect( self.hass, EVENT_DEVICE_CHANGED, self._device_changed) @@ -162,6 +167,7 @@ class HomematicipGenericDevice(Entity): ATTR_FIRMWARE_STATE: self._device.updateState.lower(), ATTR_LOW_BATTERY: self._device.lowBat, ATTR_RSSI: self._device.rssiDeviceValue, + ATTR_TYPE: self._device.modelType } @property diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index 8f298bbb3f6..1a37aa1ad4e 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -2,14 +2,14 @@ Support for HomematicIP sensors. For more details about this component, please refer to the documentation at -https://home-assistant.io/components/homematicip/ +https://home-assistant.io/components/sensor.homematicip_cloud/ """ import logging from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.homematicip_cloud import ( HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) @@ -38,41 +38,43 @@ def setup_platform(hass, config, add_devices, discovery_info=None): HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay) - _LOGGER.info('Setting up HomeMaticIP accespoint & generic devices') homeid = discovery_info['homeid'] home = hass.data[DOMAIN][homeid] - devices = [HomematicipAccesspoint(hass, home)] - if home.devices is None: - return + devices = [HomematicipAccesspoint(home)] + for device in home.devices: - devices.append(HomematicipDeviceStatus(hass, home, device)) + devices.append(HomematicipDeviceStatus(home, device)) if isinstance(device, HeatingThermostat): - devices.append(HomematicipHeatingThermostat(hass, home, device)) + devices.append(HomematicipHeatingThermostat(home, device)) if isinstance(device, TemperatureHumiditySensorWithoutDisplay): - devices.append(HomematicipSensorThermometer(hass, home, device)) - devices.append(HomematicipSensorHumidity(hass, home, device)) + devices.append(HomematicipSensorThermometer(home, device)) + devices.append(HomematicipSensorHumidity(home, device)) if isinstance(device, TemperatureHumiditySensorDisplay): - devices.append(HomematicipSensorThermometer(hass, home, device)) - devices.append(HomematicipSensorHumidity(hass, home, device)) - add_devices(devices) + devices.append(HomematicipSensorThermometer(home, device)) + devices.append(HomematicipSensorHumidity(home, device)) + + if home.devices: + add_devices(devices) class HomematicipAccesspoint(Entity): """Representation of an HomeMaticIP access point.""" - def __init__(self, hass, home): + def __init__(self, home): """Initialize the access point sensor.""" - self.hass = hass self._home = home - dispatcher_connect( - self.hass, EVENT_HOME_CHANGED, self._home_changed) _LOGGER.debug('Setting up access point %s', home.label) + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, EVENT_HOME_CHANGED, self._home_changed) + @callback def _home_changed(self, deviceid): """Handle device state changes.""" if deviceid is None or deviceid == self._home.id: - _LOGGER.debug('Event access point %s', self._home.label) + _LOGGER.debug('Event home %s', self._home.label) self.async_schedule_update_ha_state() @property @@ -109,9 +111,9 @@ class HomematicipAccesspoint(Entity): class HomematicipDeviceStatus(HomematicipGenericDevice): """Representation of an HomematicIP device status.""" - def __init__(self, hass, home, device, signal=None): + def __init__(self, home, device): """Initialize the device.""" - super().__init__(hass, home, device) + super().__init__(home, device) _LOGGER.debug('Setting up sensor device status: %s', device.label) @property @@ -147,9 +149,9 @@ class HomematicipDeviceStatus(HomematicipGenericDevice): class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" - def __init__(self, hass, home, device): + def __init__(self, home, device): """"Initialize heating thermostat.""" - super().__init__(hass, home, device) + super().__init__(home, device) _LOGGER.debug('Setting up heating thermostat device: %s', device.label) @property @@ -185,11 +187,10 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipSensorHumidity(HomematicipGenericDevice): """MomematicIP thermometer device.""" - def __init__(self, hass, home, device): + def __init__(self, home, device): """"Initialize the thermometer device.""" - super().__init__(hass, home, device) - _LOGGER.debug('Setting up humidity device: %s', - device.label) + super().__init__(home, device) + _LOGGER.debug('Setting up humidity device: %s', device.label) @property def name(self): @@ -223,9 +224,9 @@ class HomematicipSensorHumidity(HomematicipGenericDevice): class HomematicipSensorThermometer(HomematicipGenericDevice): """MomematicIP thermometer device.""" - def __init__(self, hass, home, device): + def __init__(self, home, device): """"Initialize the thermometer device.""" - super().__init__(hass, home, device) + super().__init__(home, device) _LOGGER.debug('Setting up thermometer device: %s', device.label) @property From 2532d67b9ae097b631b3950c85629f246e43f73c Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 23 Mar 2018 19:16:57 +0100 Subject: [PATCH 126/924] Add send sticker service to telegram bot (#13387) * Add send sticker service to telegram bot * A caption is not supported --- .../components/telegram_bot/__init__.py | 20 +++++++------ .../components/telegram_bot/services.yaml | 28 +++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 3041e7b41e0..e43640e4df2 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -63,6 +63,7 @@ DOMAIN = 'telegram_bot' SERVICE_SEND_MESSAGE = 'send_message' SERVICE_SEND_PHOTO = 'send_photo' +SERVICE_SEND_STICKER = 'send_sticker' SERVICE_SEND_VIDEO = 'send_video' SERVICE_SEND_DOCUMENT = 'send_document' SERVICE_SEND_LOCATION = 'send_location' @@ -154,6 +155,7 @@ SERVICE_SCHEMA_DELETE_MESSAGE = vol.Schema({ SERVICE_MAP = { SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION, @@ -167,10 +169,10 @@ SERVICE_MAP = { def load_data(hass, url=None, filepath=None, username=None, password=None, authentication=None, num_retries=5): - """Load photo/document into ByteIO/File container from a source.""" + """Load data into ByteIO/File container from a source.""" try: if url is not None: - # Load photo from URL + # Load data from URL params = {"timeout": 15} if username is not None and password is not None: if authentication == HTTP_DIGEST_AUTHENTICATION: @@ -192,7 +194,7 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, _LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url) retry_num += 1 - _LOGGER.warning("Can't load photo in %s after %s retries", + _LOGGER.warning("Can't load data in %s after %s retries", url, retry_num) elif filepath is not None: if hass.config.is_allowed_path(filepath): @@ -200,10 +202,10 @@ def load_data(hass, url=None, filepath=None, username=None, password=None, _LOGGER.warning("'%s' are not secure to load data from!", filepath) else: - _LOGGER.warning("Can't load photo. No photo found in params!") + _LOGGER.warning("Can't load data. No data found in params!") except (OSError, TypeError) as error: - _LOGGER.error("Can't load photo into ByteIO: %s", error) + _LOGGER.error("Can't load data into ByteIO: %s", error) return None @@ -274,9 +276,8 @@ def async_setup(hass, config): if msgtype == SERVICE_SEND_MESSAGE: yield from hass.async_add_job( partial(notify_service.send_message, **kwargs)) - elif (msgtype == SERVICE_SEND_PHOTO or - msgtype == SERVICE_SEND_VIDEO or - msgtype == SERVICE_SEND_DOCUMENT): + elif msgtype in [SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, + SERVICE_SEND_VIDEO, SERVICE_SEND_DOCUMENT]: yield from hass.async_add_job( partial(notify_service.send_file, msgtype, **kwargs)) elif msgtype == SERVICE_SEND_LOCATION: @@ -524,11 +525,12 @@ class TelegramNotificationService: text=message, show_alert=show_alert, **params) def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): - """Send a photo, video, or document.""" + """Send a photo, sticker, video, or document.""" params = self._get_msg_kwargs(kwargs) caption = kwargs.get(ATTR_CAPTION) func_send = { SERVICE_SEND_PHOTO: self.bot.sendPhoto, + SERVICE_SEND_STICKER: self.bot.sendSticker, SERVICE_SEND_VIDEO: self.bot.sendVideo, SERVICE_SEND_DOCUMENT: self.bot.sendDocument }.get(file_type) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 4c144fe42db..d8039c0b384 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -59,6 +59,34 @@ send_photo: description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' +send_sticker: + description: Send a sticker. + fields: + url: + description: Remote path to an webp sticker. + example: 'http://example.org/path/to/the/sticker.webp' + file: + description: Local path to an webp sticker. + example: '/path/to/the/sticker.webp' + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: '[12345, 67890] or 12345' + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + send_video: description: Send a video. fields: From 4bd6776443d6a56e20ac290d7b692878a2d49577 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 12:13:52 -0700 Subject: [PATCH 127/924] Google assistant sync (#13392) * Add Google Assistant Sync API * Update const.py * Async/await --- homeassistant/components/cloud/__init__.py | 6 +- homeassistant/components/cloud/const.py | 4 +- homeassistant/components/cloud/http_api.py | 66 ++++++++++++++-------- tests/components/cloud/test_http_api.py | 19 +++++++ tests/components/cloud/test_init.py | 2 + 5 files changed, 71 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index adf0b8f51b6..e73d043d366 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -37,6 +37,7 @@ CONF_FILTER = 'filter' CONF_GOOGLE_ACTIONS = 'google_actions' CONF_RELAYER = 'relayer' CONF_USER_POOL_ID = 'user_pool_id' +CONF_GOOGLE_ACTIONS_SYNC_URL = 'google_actions_sync_url' DEFAULT_MODE = 'production' DEPENDENCIES = ['http'] @@ -75,6 +76,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_REGION): str, vol.Optional(CONF_RELAYER): str, + vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): str, vol.Optional(CONF_ALEXA): ALEXA_SCHEMA, vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA, }), @@ -110,7 +112,7 @@ class Cloud: def __init__(self, hass, mode, alexa, google_actions, cognito_client_id=None, user_pool_id=None, region=None, - relayer=None): + relayer=None, google_actions_sync_url=None): """Create an instance of Cloud.""" self.hass = hass self.mode = mode @@ -128,6 +130,7 @@ class Cloud: self.user_pool_id = user_pool_id self.region = region self.relayer = relayer + self.google_actions_sync_url = google_actions_sync_url else: info = SERVERS[mode] @@ -136,6 +139,7 @@ class Cloud: self.user_pool_id = info['user_pool_id'] self.region = info['region'] self.relayer = info['relayer'] + self.google_actions_sync_url = info['google_actions_sync_url'] @property def is_logged_in(self): diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 99075d3d02d..82128206d47 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -8,7 +8,9 @@ SERVERS = { 'cognito_client_id': '60i2uvhvbiref2mftj7rgcrt9u', 'user_pool_id': 'us-east-1_87ll5WOP8', 'region': 'us-east-1', - 'relayer': 'wss://cloud.hass.io:8000/websocket' + 'relayer': 'wss://cloud.hass.io:8000/websocket', + 'google_actions_sync_url': ('https://24ab3v80xd.execute-api.us-east-1.' + 'amazonaws.com/prod/smart_home_sync'), } } diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 3065de24180..a4b3b59f333 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -16,9 +16,9 @@ from .const import DOMAIN, REQUEST_TIMEOUT _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass): +async def async_setup(hass): """Initialize the HTTP API.""" + hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) hass.http.register_view(CloudLogoutView) hass.http.register_view(CloudAccountView) @@ -38,12 +38,11 @@ _CLOUD_ERRORS = { def _handle_cloud_errors(handler): """Handle auth errors.""" - @asyncio.coroutine @wraps(handler) - def error_handler(view, request, *args, **kwargs): + async def error_handler(view, request, *args, **kwargs): """Handle exceptions that raise from the wrapped request handler.""" try: - result = yield from handler(view, request, *args, **kwargs) + result = await handler(view, request, *args, **kwargs) return result except (auth_api.CloudError, asyncio.TimeoutError) as err: @@ -57,6 +56,31 @@ def _handle_cloud_errors(handler): return error_handler +class GoogleActionsSyncView(HomeAssistantView): + """Trigger a Google Actions Smart Home Sync.""" + + url = '/api/cloud/google_actions/sync' + name = 'api:cloud:google_actions/sync' + + @_handle_cloud_errors + async def post(self, request): + """Trigger a Google Actions sync.""" + hass = request.app['hass'] + cloud = hass.data[DOMAIN] + websession = hass.helpers.aiohttp_client.async_get_clientsession() + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + await hass.async_add_job(auth_api.check_token, cloud) + + with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): + req = await websession.post( + cloud.google_actions_sync_url, headers={ + 'authorization': cloud.id_token + }) + + return self.json({}, status_code=req.status) + + class CloudLoginView(HomeAssistantView): """Login to Home Assistant cloud.""" @@ -68,19 +92,18 @@ class CloudLoginView(HomeAssistantView): vol.Required('email'): str, vol.Required('password'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle login request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job(auth_api.login, cloud, data['email'], - data['password']) + await hass.async_add_job(auth_api.login, cloud, data['email'], + data['password']) hass.async_add_job(cloud.iot.connect) # Allow cloud to start connecting. - yield from asyncio.sleep(0, loop=hass.loop) + await asyncio.sleep(0, loop=hass.loop) return self.json(_account_data(cloud)) @@ -91,14 +114,13 @@ class CloudLogoutView(HomeAssistantView): name = 'api:cloud:logout' @_handle_cloud_errors - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle logout request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from cloud.logout() + await cloud.logout() return self.json_message('ok') @@ -109,8 +131,7 @@ class CloudAccountView(HomeAssistantView): url = '/api/cloud/account' name = 'api:cloud:account' - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get account info.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] @@ -132,14 +153,13 @@ class CloudRegisterView(HomeAssistantView): vol.Required('email'): str, vol.Required('password'): vol.All(str, vol.Length(min=6)), })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle registration request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.register, cloud, data['email'], data['password']) return self.json_message('ok') @@ -155,14 +175,13 @@ class CloudResendConfirmView(HomeAssistantView): @RequestDataValidator(vol.Schema({ vol.Required('email'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle resending confirm email code request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.resend_email_confirm, cloud, data['email']) return self.json_message('ok') @@ -178,14 +197,13 @@ class CloudForgotPasswordView(HomeAssistantView): @RequestDataValidator(vol.Schema({ vol.Required('email'): str, })) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Handle forgot password request.""" hass = request.app['hass'] cloud = hass.data[DOMAIN] with async_timeout.timeout(REQUEST_TIMEOUT, loop=hass.loop): - yield from hass.async_add_job( + await hass.async_add_job( auth_api.forgot_password, cloud, data['email']) return self.json_message('ok') diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 1ed3d1b4744..55c6290c158 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -11,6 +11,9 @@ from homeassistant.components.cloud import DOMAIN, auth_api, iot from tests.common import mock_coro +GOOGLE_ACTIONS_SYNC_URL = 'https://api-test.hass.io/google_actions_sync' + + @pytest.fixture def cloud_client(hass, aiohttp_client): """Fixture that can fetch from the cloud client.""" @@ -23,6 +26,7 @@ def cloud_client(hass, aiohttp_client): 'user_pool_id': 'user_pool_id', 'region': 'region', 'relayer': 'relayer', + 'google_actions_sync_url': GOOGLE_ACTIONS_SYNC_URL, } })) hass.data['cloud']._decode_claims = \ @@ -38,6 +42,21 @@ def mock_cognito(): yield mock_cog() +async def test_google_actions_sync(mock_cognito, cloud_client, aioclient_mock): + """Test syncing Google Actions.""" + aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL) + req = await cloud_client.post('/api/cloud/google_actions/sync') + assert req.status == 200 + + +async def test_google_actions_sync_fails(mock_cognito, cloud_client, + aioclient_mock): + """Test syncing Google Actions gone bad.""" + aioclient_mock.post(GOOGLE_ACTIONS_SYNC_URL, status=403) + req = await cloud_client.post('/api/cloud/google_actions/sync') + assert req.status == 403 + + @asyncio.coroutine def test_account_view_no_account(cloud_client): """Test fetching account if no account available.""" diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 70990519a0b..91f8ab8316d 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -29,6 +29,7 @@ def test_constructor_loads_info_from_constant(): 'user_pool_id': 'test-user_pool_id', 'region': 'test-region', 'relayer': 'test-relayer', + 'google_actions_sync_url': 'test-google_actions_sync_url', } }), patch('homeassistant.components.cloud.Cloud._fetch_jwt_keyset', return_value=mock_coro(True)): @@ -43,6 +44,7 @@ def test_constructor_loads_info_from_constant(): assert cl.user_pool_id == 'test-user_pool_id' assert cl.region == 'test-region' assert cl.relayer == 'test-relayer' + assert cl.google_actions_sync_url == 'test-google_actions_sync_url' @asyncio.coroutine From 7fd687f59c7aa027f6d905fc9bd92504d4689430 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 23 Mar 2018 21:54:19 +0100 Subject: [PATCH 128/924] Fix current_cover_position (#13135) --- homeassistant/components/cover/template.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cover/template.py b/homeassistant/components/cover/template.py index f4728a12a3b..4e197365a70 100644 --- a/homeassistant/components/cover/template.py +++ b/homeassistant/components/cover/template.py @@ -234,7 +234,9 @@ class CoverTemplate(CoverDevice): None is unknown, 0 is closed, 100 is fully open. """ - return self._position + if self._position_template or self._position_script: + return self._position + return None @property def current_cover_tilt_position(self): From 630734ca152a04ff1fc6c9830626618e8a2397a8 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 23 Mar 2018 13:54:36 -0700 Subject: [PATCH 129/924] Switched values to downcase. (#13406) --- homeassistant/components/media_player/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index beaea8a8ad0..95072f0270c 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -131,8 +131,8 @@ play_media: description: The ID of the content to play. Platform dependent. example: 'https://home-assistant.io/images/cast/splash.png' media_content_type: - description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST - example: 'MUSIC' + description: The type of the content to play. Must be one of music, tvshow, video, episode, channel or playlist + example: 'music' select_source: description: Send the media player the command to change input source. From 6a625bdb37f4ccf85e89597518ca51ddc852e59c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 23 Mar 2018 22:02:52 +0100 Subject: [PATCH 130/924] Cast Integration Cleanup (#13275) * Cast Integration Cleanup * Fix long line * Fixes and logging * Fix tests * Lint * Report unknown state with None * Lint * Switch to async_add_job Gets rid of those pesky "Setup of platform cast is taking over 10 seconds." messages. * Re-introduce PlatformNotReady * Add tests * Remove unnecessary checks * Test PlatformNotReady * Fix async in sync context * Blocking update It's not using async anyway * Upgrade pychromecast to 2.1.0 * Make reviewing easier I like "protected" access, but I like reviewing more :) * Make reviewing even easier :) * Comment tests --- homeassistant/components/media_player/cast.py | 576 +++++++++++------- requirements_all.txt | 2 +- tests/components/media_player/test_cast.py | 354 +++++++---- 3 files changed, 601 insertions(+), 331 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 579f9b62864..91b8d362c43 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -7,8 +7,10 @@ https://home-assistant.io/components/media_player.cast/ # pylint: disable=import-error import logging import threading +from typing import Optional, Tuple import voluptuous as vol +import attr from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.typing import HomeAssistantType, ConfigType @@ -22,11 +24,11 @@ from homeassistant.components.media_player import ( SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_HOST, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, - STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) + EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['pychromecast==2.0.0'] +REQUIREMENTS = ['pychromecast==2.1.0'] _LOGGER = logging.getLogger(__name__) @@ -39,23 +41,103 @@ SUPPORT_CAST = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY +# Stores a threading.Lock that is held by the internal pychromecast discovery. INTERNAL_DISCOVERY_RUNNING_KEY = 'cast_discovery_running' -# UUID -> CastDevice mapping; cast devices without UUID are not stored +# Stores all ChromecastInfo we encountered through discovery or config as a set +# If we find a chromecast with a new host, the old one will be removed again. +KNOWN_CHROMECAST_INFO_KEY = 'cast_known_chromecasts' +# Stores UUIDs of cast devices that were added as entities. Doesn't store +# None UUIDs. ADDED_CAST_DEVICES_KEY = 'cast_added_cast_devices' -# Stores every discovered (host, port, uuid) -KNOWN_CHROMECASTS_KEY = 'cast_all_chromecasts' +# Dispatcher signal fired with a ChromecastInfo every time we discover a new +# Chromecast or receive it through configuration SIGNAL_CAST_DISCOVERED = 'cast_discovered' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC): [cv.string], + vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, + [cv.string]) }) +@attr.s(slots=True, frozen=True) +class ChromecastInfo(object): + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + host = attr.ib(type=str) + port = attr.ib(type=int) + uuid = attr.ib(type=Optional[str], converter=attr.converters.optional(str), + default=None) # always convert UUID to string if not None + model_name = attr.ib(type=str, default='') # needed for cast type + friendly_name = attr.ib(type=Optional[str], default=None) + + @property + def is_audio_group(self) -> bool: + """Return if this is an audio group.""" + return self.port != DEFAULT_PORT + + @property + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + return all(attr.astuple(self)) + + @property + def host_port(self) -> Tuple[str, int]: + """Return the host+port tuple.""" + return self.host, self.port + + +def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: + """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" + if info.is_information_complete or info.is_audio_group: + # We have all information, no need to check HTTP API. Or this is an + # audio group, so checking via HTTP won't give us any new information. + return info + + # Fill out missing information via HTTP dial. + from pychromecast import dial + + http_device_status = dial.get_device_status(info.host) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return info + + return ChromecastInfo( + host=info.host, port=info.port, + uuid=(info.uuid or http_device_status.uuid), + friendly_name=(info.friendly_name or http_device_status.friendly_name), + model_name=(info.model_name or http_device_status.model_name) + ) + + +def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): + if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", info) + return + + # Either discovered completely new chromecast or a "moved" one. + info = _fill_out_missing_chromecast_info(info) + _LOGGER.debug("Discovered chromecast %s", info) + + if info.uuid is not None: + # Remove previous cast infos with same uuid from known chromecasts. + same_uuid = set(x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] + if info.uuid == x.uuid) + hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid + + hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) + + def _setup_internal_discovery(hass: HomeAssistantType) -> None: """Set up the pychromecast internal discovery.""" - hass.data.setdefault(INTERNAL_DISCOVERY_RUNNING_KEY, threading.Lock()) + if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): # Internal discovery is already running return @@ -65,30 +147,14 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: def internal_callback(name): """Called when zeroconf has discovered a new chromecast.""" mdns = listener.services[name] - ip_address, port, uuid, _, _ = mdns - key = (ip_address, port, uuid) - - if key in hass.data[KNOWN_CHROMECASTS_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", mdns) - return - - _LOGGER.debug("Discovered new chromecast %s", mdns) - try: - # pylint: disable=protected-access - chromecast = pychromecast._get_chromecast_from_host( - mdns, blocking=True) - except pychromecast.ChromecastConnectionError: - _LOGGER.debug("Can't set up cast with mDNS info %s. " - "Assuming it's not a Chromecast", mdns) - return - hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast - dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, chromecast) + _discover_chromecast(hass, ChromecastInfo(*mdns)) _LOGGER.debug("Starting internal pychromecast discovery.") listener, browser = pychromecast.start_discovery(internal_callback) def stop_discovery(event): """Stop discovery of new chromecasts.""" + _LOGGER.debug("Stopping internal pychromecast discovery.") pychromecast.stop_discovery(browser) hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() @@ -96,40 +162,26 @@ def _setup_internal_discovery(hass: HomeAssistantType) -> None: @callback -def _async_create_cast_device(hass, chromecast): +def _async_create_cast_device(hass: HomeAssistantType, + info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. - Returns None if the cast device has already been added. Additionally, - automatically updates existing chromecast entities. + Returns None if the cast device has already been added. """ - if chromecast.uuid is None: + if info.uuid is None: # Found a cast without UUID, we don't store it because we won't be able # to update it anyway. - return CastDevice(chromecast) + return CastDevice(info) # Found a cast with UUID added_casts = hass.data[ADDED_CAST_DEVICES_KEY] - old_cast_device = added_casts.get(chromecast.uuid) - if old_cast_device is None: - # -> New cast device - cast_device = CastDevice(chromecast) - added_casts[chromecast.uuid] = cast_device - return cast_device - - old_key = (old_cast_device.cast.host, - old_cast_device.cast.port, - old_cast_device.cast.uuid) - new_key = (chromecast.host, chromecast.port, chromecast.uuid) - - if old_key == new_key: - # Re-discovered with same data, ignore + if info.uuid in added_casts: + # Already added this one, the entity will take care of moved hosts + # itself return None - - # -> Cast device changed host - # Remove old pychromecast.Chromecast from global list, because it isn't - # valid anymore - old_cast_device.async_set_chromecast(chromecast) - return None + # -> New cast device + added_casts.add(info.uuid) + return CastDevice(info) async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, @@ -139,98 +191,308 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) - hass.data.setdefault(ADDED_CAST_DEVICES_KEY, {}) - hass.data.setdefault(KNOWN_CHROMECASTS_KEY, {}) + hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) + hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) - # None -> use discovery; (host, port) -> manually specify chromecast. - want_host = None - if discovery_info: - want_host = (discovery_info.get('host'), discovery_info.get('port')) + info = None + if discovery_info is not None: + info = ChromecastInfo(host=discovery_info['host'], + port=discovery_info['port']) elif CONF_HOST in config: - want_host = (config.get(CONF_HOST), DEFAULT_PORT) + info = ChromecastInfo(host=config[CONF_HOST], + port=DEFAULT_PORT) - enable_discovery = False - if want_host is None: - # We were explicitly told to enable pychromecast discovery. - enable_discovery = True - elif want_host[1] != DEFAULT_PORT: - # We're trying to add a group, so we have to use pychromecast's - # discovery to get the correct friendly name. - enable_discovery = True + @callback + def async_cast_discovered(discover: ChromecastInfo) -> None: + """Callback for when a new chromecast is discovered.""" + if info is not None and info.host_port != discover.host_port: + # Not our requested cast device. + return - if enable_discovery: - @callback - def async_cast_discovered(chromecast): - """Callback for when a new chromecast is discovered.""" - if want_host is not None and \ - (chromecast.host, chromecast.port) != want_host: - return # for groups, only add requested device - cast_device = _async_create_cast_device(hass, chromecast) + cast_device = _async_create_cast_device(hass, discover) + if cast_device is not None: + async_add_devices([cast_device]) - if cast_device is not None: - async_add_devices([cast_device]) - - async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, - async_cast_discovered) - # Re-play the callback for all past chromecasts, store the objects in - # a list to avoid concurrent modification resulting in exception. - for chromecast in list(hass.data[KNOWN_CHROMECASTS_KEY].values()): - async_cast_discovered(chromecast) + async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + # Re-play the callback for all past chromecasts, store the objects in + # a list to avoid concurrent modification resulting in exception. + for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): + async_cast_discovered(chromecast) + if info is None or info.is_audio_group: + # If we were a) explicitly told to enable discovery or + # b) have an audio group cast device, we need internal discovery. hass.async_add_job(_setup_internal_discovery, hass) else: - # Manually add a "normal" Chromecast, we can do that without discovery. - try: - chromecast = await hass.async_add_job( - pychromecast.Chromecast, *want_host) - except pychromecast.ChromecastConnectionError as err: - _LOGGER.warning("Can't set up chromecast on %s: %s", - want_host[0], err) + info = await hass.async_add_job(_fill_out_missing_chromecast_info, + info) + if info.friendly_name is None: + # HTTP dial failed, so we won't be able to connect. raise PlatformNotReady - key = (chromecast.host, chromecast.port, chromecast.uuid) - cast_device = _async_create_cast_device(hass, chromecast) - if cast_device is not None: - hass.data[KNOWN_CHROMECASTS_KEY][key] = chromecast - async_add_devices([cast_device]) + hass.async_add_job(_discover_chromecast, hass, info) + + +class CastStatusListener(object): + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast): + """Initialize the status listener.""" + self._cast_device = cast_device + self._valid = True + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener( + self) + chromecast.register_connection_listener(self) + + def new_cast_status(self, cast_status): + """Called when a new CastStatus is received.""" + if self._valid: + self._cast_device.new_cast_status(cast_status) + + def new_media_status(self, media_status): + """Called when a new MediaStatus is received.""" + if self._valid: + self._cast_device.new_media_status(media_status) + + def new_connection_status(self, connection_status): + """Called when a new ConnectionStatus is received.""" + if self._valid: + self._cast_device.new_connection_status(connection_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + self._valid = False class CastDevice(MediaPlayerDevice): - """Representation of a Cast device on the network.""" + """Representation of a Cast device on the network. - def __init__(self, chromecast): - """Initialize the Cast device.""" - self.cast = None # type: pychromecast.Chromecast + This class is the holder of the pychromecast.Chromecast object and its + socket client. It therefore handles all reconnects and audio group changing + "elected leader" itself. + """ + + def __init__(self, cast_info): + """Initialize the cast device.""" + self._cast_info = cast_info # type: ChromecastInfo + self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None self.media_status_received = None + self._available = False # type: bool + self._status_listener = None # type: Optional[CastStatusListener] - self.async_set_chromecast(chromecast) + async def async_added_to_hass(self): + """Create chromecast object when added to hass.""" + @callback + def async_cast_discovered(discover: ChromecastInfo): + """Callback for changing elected leaders / IP.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) + self.hass.async_add_job(self.async_set_cast_info(discover)) + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, + async_cast_discovered) + self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect Chromecast object when removed.""" + self._async_disconnect() + if self._cast_info.uuid is not None: + # Remove the entity from the added casts so that it can dynamically + # be re-added again. + self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) + + async def async_set_cast_info(self, cast_info): + """Set the cast information and set up the chromecast object.""" + import pychromecast + old_cast_info = self._cast_info + self._cast_info = cast_info + + if self._chromecast is not None: + if old_cast_info.host_port == cast_info.host_port: + # Nothing connection-related updated + return + self._async_disconnect() + + # Failed connection will unfortunately never raise an exception, it + # will instead just try connecting indefinitely. + # pylint: disable=protected-access + _LOGGER.debug("Connecting to cast device %s", cast_info) + chromecast = await self.hass.async_add_job( + pychromecast._get_chromecast_from_host, attr.astuple(cast_info)) + self._chromecast = chromecast + self._status_listener = CastStatusListener(self, chromecast) + # Initialise connection status as connected because we can only + # register the connection listener *after* the initial connection + # attempt. If the initial connection failed, we would never reach + # this code anyway. + self._available = True + self.cast_status = chromecast.status + self.media_status = chromecast.media_controller.status + _LOGGER.debug("Connection successful!") + self.async_schedule_update_ha_state() + + @callback + def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + if self._chromecast is None: + # Can't disconnect if not connected. + return + _LOGGER.debug("Disconnecting from previous chromecast socket.") + self._available = False + self._chromecast.disconnect(blocking=False) + # Invalidate some attributes + self._chromecast = None + self.cast_status = None + self.media_status = None + self.media_status_received = None + self._status_listener.invalidate() + self._status_listener = None + + def update(self): + """Periodically update the properties. + + Even though we receive callbacks for most state changes, some 3rd party + apps don't always send them. Better poll every now and then if the + chromecast is active (i.e. an app is running). + """ + if not self._available: + # Not connected or not available. + return + + if self._chromecast.media_controller.is_active: + # We can only update status if the media namespace is active + self._chromecast.media_controller.update_status() + + # ========== Callbacks ========== + def new_cast_status(self, cast_status): + """Handle updates of the cast status.""" + self.cast_status = cast_status + self.schedule_update_ha_state() + + def new_media_status(self, media_status): + """Handle updates of the media status.""" + self.media_status = media_status + self.media_status_received = dt_util.utcnow() + self.schedule_update_ha_state() + + def new_connection_status(self, connection_status): + """Handle updates of connection status.""" + from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED + + new_available = connection_status.status == CONNECTION_STATUS_CONNECTED + if new_available != self._available: + # Connection status callbacks happen often when disconnected. + # Only update state when availability changed to put less pressure + # on state machine. + _LOGGER.debug("Cast device availability changed: %s", + connection_status.status) + self._available = new_available + self.schedule_update_ha_state() + + # ========== Service Calls ========== + def turn_on(self): + """Turn on the cast device.""" + import pychromecast + + if not self._chromecast.is_idle: + # Already turned on + return + + if self._chromecast.app_id is not None: + # Quit the previous app before starting splash screen + self._chromecast.quit_app() + + # The only way we can turn the Chromecast is on is by launching an app + self._chromecast.play_media(CAST_SPLASH, + pychromecast.STREAM_TYPE_BUFFERED) + + def turn_off(self): + """Turn off the cast device.""" + self._chromecast.quit_app() + + def mute_volume(self, mute): + """Mute the volume.""" + self._chromecast.set_volume_muted(mute) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._chromecast.set_volume(volume) + + def media_play(self): + """Send play command.""" + self._chromecast.media_controller.play() + + def media_pause(self): + """Send pause command.""" + self._chromecast.media_controller.pause() + + def media_stop(self): + """Send stop command.""" + self._chromecast.media_controller.stop() + + def media_previous_track(self): + """Send previous track command.""" + self._chromecast.media_controller.rewind() + + def media_next_track(self): + """Send next track command.""" + self._chromecast.media_controller.skip() + + def media_seek(self, position): + """Seek the media to a specific location.""" + self._chromecast.media_controller.seek(position) + + def play_media(self, media_type, media_id, **kwargs): + """Play media from a URL.""" + self._chromecast.media_controller.play_media(media_id, media_type) + + # ========== Properties ========== @property def should_poll(self): - """No polling needed.""" - return False + """Polling needed for cast integration, see async_update.""" + return True @property def name(self): """Return the name of the device.""" - return self.cast.device.friendly_name + return self._cast_info.friendly_name - # MediaPlayerDevice properties and methods @property def state(self): """Return the state of the player.""" if self.media_status is None: - return STATE_UNKNOWN + return None elif self.media_status.player_is_playing: return STATE_PLAYING elif self.media_status.player_is_paused: return STATE_PAUSED elif self.media_status.player_is_idle: return STATE_IDLE - elif self.cast.is_idle: + elif self._chromecast is not None and self._chromecast.is_idle: return STATE_OFF - return STATE_UNKNOWN + return None + + @property + def available(self): + """Return True if the cast device is connected.""" + return self._available @property def volume_level(self): @@ -318,12 +580,12 @@ class CastDevice(MediaPlayerDevice): @property def app_id(self): """Return the ID of the current running app.""" - return self.cast.app_id + return self._chromecast.app_id if self._chromecast else None @property def app_name(self): """Name of the current running app.""" - return self.cast.app_display_name + return self._chromecast.app_display_name if self._chromecast else None @property def supported_features(self): @@ -349,101 +611,7 @@ class CastDevice(MediaPlayerDevice): """ return self.media_status_received - def turn_on(self): - """Turn on the ChromeCast.""" - # The only way we can turn the Chromecast is on is by launching an app - if not self.cast.status or not self.cast.status.is_active_input: - import pychromecast - - if self.cast.app_id: - self.cast.quit_app() - - self.cast.play_media( - CAST_SPLASH, pychromecast.STREAM_TYPE_BUFFERED) - - def turn_off(self): - """Turn Chromecast off.""" - self.cast.quit_app() - - def mute_volume(self, mute): - """Mute the volume.""" - self.cast.set_volume_muted(mute) - - def set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self.cast.set_volume(volume) - - def media_play(self): - """Send play command.""" - self.cast.media_controller.play() - - def media_pause(self): - """Send pause command.""" - self.cast.media_controller.pause() - - def media_stop(self): - """Send stop command.""" - self.cast.media_controller.stop() - - def media_previous_track(self): - """Send previous track command.""" - self.cast.media_controller.rewind() - - def media_next_track(self): - """Send next track command.""" - self.cast.media_controller.skip() - - def media_seek(self, position): - """Seek the media to a specific location.""" - self.cast.media_controller.seek(position) - - def play_media(self, media_type, media_id, **kwargs): - """Play media from a URL.""" - self.cast.media_controller.play_media(media_id, media_type) - - # Implementation of chromecast status_listener methods - def new_cast_status(self, status): - """Handle updates of the cast status.""" - self.cast_status = status - self.schedule_update_ha_state() - - def new_media_status(self, status): - """Handle updates of the media status.""" - self.media_status = status - self.media_status_received = dt_util.utcnow() - self.schedule_update_ha_state() - @property - def unique_id(self) -> str: + def unique_id(self) -> Optional[str]: """Return a unique ID.""" - if self.cast.uuid is not None: - return str(self.cast.uuid) - return None - - @callback - def async_set_chromecast(self, chromecast): - """Set the internal Chromecast object and disconnect the previous.""" - self._async_disconnect() - - self.cast = chromecast - - self.cast.socket_client.receiver_controller.register_status_listener( - self) - self.cast.socket_client.media_controller.register_status_listener(self) - - self.cast_status = self.cast.status - self.media_status = self.cast.media_controller.status - - async def async_will_remove_from_hass(self) -> None: - """Disconnect Chromecast object when removed.""" - self._async_disconnect() - - @callback - def _async_disconnect(self): - """Disconnect Chromecast object if it is set.""" - if self.cast is None: - return - _LOGGER.debug("Disconnecting existing chromecast object") - old_key = (self.cast.host, self.cast.port, self.cast.uuid) - self.hass.data[KNOWN_CHROMECASTS_KEY].pop(old_key) - self.cast.disconnect(blocking=False) + return self._cast_info.uuid diff --git a/requirements_all.txt b/requirements_all.txt index 75fd6de8f46..52833969872 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -685,7 +685,7 @@ pybbox==0.0.5-alpha pychannels==1.0.0 # homeassistant.components.media_player.cast -pychromecast==2.0.0 +pychromecast==2.1.0 # homeassistant.components.media_player.cmus pycmus==0.1.0 diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 2075b4cf6e6..ee69ec1c85d 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -5,12 +5,17 @@ from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID +import attr import pytest from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ + async_dispatcher_send from homeassistant.components.media_player import cast +from homeassistant.setup import async_setup_component @pytest.fixture(autouse=True) @@ -26,57 +31,74 @@ def cast_mock(): FakeUUID = UUID('57355bce-9364-4aa6-ac1e-eb849dccf9e2') -def get_fake_chromecast(host='192.168.178.42', port=8009, - uuid: Optional[UUID] = FakeUUID): +def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" - return MagicMock(host=host, port=port, uuid=uuid) + mock = MagicMock(host=info.host, port=info.port, uuid=info.uuid) + mock.media_controller.status = None + return mock -@asyncio.coroutine -def async_setup_cast(hass, config=None, discovery_info=None): +def get_fake_chromecast_info(host='192.168.178.42', port=8009, + uuid: Optional[UUID] = FakeUUID): + """Generate a Fake ChromecastInfo with the specified arguments.""" + return ChromecastInfo(host=host, port=port, uuid=uuid, + friendly_name="Speaker") + + +async def async_setup_cast(hass, config=None, discovery_info=None): """Helper to setup the cast platform.""" if config is None: config = {} add_devices = Mock() - yield from cast.async_setup_platform(hass, config, add_devices, - discovery_info=discovery_info) - yield from hass.async_block_till_done() + await cast.async_setup_platform(hass, config, add_devices, + discovery_info=discovery_info) + await hass.async_block_till_done() return add_devices -@asyncio.coroutine -def async_setup_cast_internal_discovery(hass, config=None, - discovery_info=None, - no_from_host_patch=False): +async def async_setup_cast_internal_discovery(hass, config=None, + discovery_info=None): """Setup the cast platform and the discovery.""" listener = MagicMock(services={}) with patch('pychromecast.start_discovery', return_value=(listener, None)) as start_discovery: - add_devices = yield from async_setup_cast(hass, config, discovery_info) - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() + add_devices = await async_setup_cast(hass, config, discovery_info) + await hass.async_block_till_done() + await hass.async_block_till_done() assert start_discovery.call_count == 1 discovery_callback = start_discovery.call_args[0][0] - def discover_chromecast(service_name, chromecast): + def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" - listener.services[service_name] = ( - chromecast.host, chromecast.port, chromecast.uuid, None, None) - if no_from_host_patch: - discovery_callback(service_name) - else: - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast): - discovery_callback(service_name) + listener.services[service_name] = attr.astuple(info) + discovery_callback(service_name) return discover_chromecast, add_devices +async def async_setup_media_player_cast(hass: HomeAssistantType, + info: ChromecastInfo): + """Setup the cast platform with async_setup_component.""" + chromecast = get_fake_chromecast(info) + + cast.CastStatusListener = MagicMock() + + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast) as get_chromecast: + await async_setup_component(hass, 'media_player', { + 'media_player': {'platform': 'cast', 'host': info.host}}) + await hass.async_block_till_done() + assert get_chromecast.call_count == 1 + assert cast.CastStatusListener.call_count == 1 + entity = cast.CastStatusListener.call_args[0][0] + return chromecast, entity + + @asyncio.coroutine def test_start_discovery_called_once(hass): """Test pychromecast.start_discovery called exactly once.""" @@ -95,11 +117,13 @@ def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" with patch('pychromecast.start_discovery', return_value=(None, 'the-browser')) as start_discovery: - yield from async_setup_cast(hass) + # start_discovery should be called with empty config + yield from async_setup_cast(hass, {}) assert start_discovery.call_count == 1 with patch('pychromecast.stop_discovery') as stop_discovery: + # stop discovery should be called on shutdown hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) yield from hass.async_block_till_done() @@ -107,145 +131,223 @@ def test_stop_discovery_called_on_stop(hass): with patch('pychromecast.start_discovery', return_value=(None, 'the-browser')) as start_discovery: + # start_discovery should be called again on re-startup yield from async_setup_cast(hass) assert start_discovery.call_count == 1 -@asyncio.coroutine -def test_internal_discovery_callback_only_generates_once(hass): - """Test _get_chromecast_from_host only called once per device.""" - discover_cast, _ = yield from async_setup_cast_internal_discovery( - hass, no_from_host_patch=True) - chromecast = get_fake_chromecast() +async def test_internal_discovery_callback_only_generates_once(hass): + """Test discovery only called once per device.""" + discover_cast, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info() - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast) as gen_chromecast: - discover_cast('the-service', chromecast) - mdns = (chromecast.host, chromecast.port, chromecast.uuid, None, None) - gen_chromecast.assert_called_once_with(mdns, blocking=True) + signal = MagicMock() + async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - gen_chromecast.reset_mock() - assert gen_chromecast.call_count == 0 - - -@asyncio.coroutine -def test_internal_discovery_callback_calls_dispatcher(hass): - """Test internal discovery calls dispatcher.""" - discover_cast, _ = yield from async_setup_cast_internal_discovery(hass) - chromecast = get_fake_chromecast() - - with patch('pychromecast._get_chromecast_from_host', - return_value=chromecast): - signal = MagicMock() - - async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - yield from hass.async_block_till_done() - - signal.assert_called_once_with(chromecast) - - -@asyncio.coroutine -def test_internal_discovery_callback_with_connection_error(hass): - """Test internal discovery not calling dispatcher on ConnectionError.""" - import pychromecast # imports mock pychromecast - - pychromecast.ChromecastConnectionError = IOError - - discover_cast, _ = yield from async_setup_cast_internal_discovery( - hass, no_from_host_patch=True) - chromecast = get_fake_chromecast() - - with patch('pychromecast._get_chromecast_from_host', - side_effect=pychromecast.ChromecastConnectionError): - signal = MagicMock() - - async_dispatcher_connect(hass, 'cast_discovered', signal) - discover_cast('the-service', chromecast) - yield from hass.async_block_till_done() + with patch('pychromecast.dial.get_device_status', return_value=None): + # discovering a cast device should call the dispatcher + discover_cast('the-service', info) + await hass.async_block_till_done() + discover = signal.mock_calls[0][1][0] + # attr's __eq__ somehow breaks here, use tuples instead + assert attr.astuple(discover) == attr.astuple(info) + signal.reset_mock() + # discovering it a second time shouldn't + discover_cast('the-service', info) + await hass.async_block_till_done() assert signal.call_count == 0 -def test_create_cast_device_without_uuid(hass): - """Test create a cast device without a UUID.""" - chromecast = get_fake_chromecast(uuid=None) - cast_device = cast._async_create_cast_device(hass, chromecast) - assert cast_device is not None - - -def test_create_cast_device_with_uuid(hass): - """Test create cast devices with UUID.""" - added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - chromecast = get_fake_chromecast() - cast_device = cast._async_create_cast_device(hass, chromecast) - assert cast_device is not None - assert chromecast.uuid in added_casts - - with patch.object(cast_device, 'async_set_chromecast') as mock_set: - assert cast._async_create_cast_device(hass, chromecast) is None - assert mock_set.call_count == 0 - - chromecast = get_fake_chromecast(host='192.168.178.1') - assert cast._async_create_cast_device(hass, chromecast) is None - assert mock_set.call_count == 1 - mock_set.assert_called_once_with(chromecast) - - -@asyncio.coroutine -def test_normal_chromecast_not_starting_discovery(hass): - """Test cast platform not starting discovery when not required.""" +async def test_internal_discovery_callback_fill_out(hass): + """Test internal discovery automatically filling out information.""" import pychromecast # imports mock pychromecast pychromecast.ChromecastConnectionError = IOError - chromecast = get_fake_chromecast() + discover_cast, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(uuid=None) + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) - with patch('pychromecast.Chromecast', return_value=chromecast): - add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + signal = MagicMock() + + async_dispatcher_connect(hass, 'cast_discovered', signal) + discover_cast('the-service', info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + # attr's __eq__ somehow breaks here, use tuples instead + assert attr.astuple(discover) == attr.astuple(full_info) + + +async def test_create_cast_device_without_uuid(hass): + """Test create a cast device with no UUId should still create an entity.""" + info = get_fake_chromecast_info(uuid=None) + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is not None + + +async def test_create_cast_device_with_uuid(hass): + """Test create cast devices with UUID creates entities.""" + added_casts = hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + info = get_fake_chromecast_info() + + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is not None + assert info.uuid in added_casts + + # Sending second time should not create new entity + cast_device = cast._async_create_cast_device(hass, info) + assert cast_device is None + + +async def test_normal_chromecast_not_starting_discovery(hass): + """Test cast platform not starting discovery when not required.""" + # pylint: disable=no-member + with patch('homeassistant.components.media_player.cast.' + '_setup_internal_discovery') as setup_discovery: + # normal (non-group) chromecast shouldn't start discovery. + add_devices = await async_setup_cast(hass, {'host': 'host1'}) + await hass.async_block_till_done() assert add_devices.call_count == 1 + assert setup_discovery.call_count == 0 # Same entity twice - add_devices = yield from async_setup_cast(hass, {'host': 'host1'}) + add_devices = await async_setup_cast(hass, {'host': 'host1'}) + await hass.async_block_till_done() assert add_devices.call_count == 0 + assert setup_discovery.call_count == 0 - hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - add_devices = yield from async_setup_cast( + hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + add_devices = await async_setup_cast( hass, discovery_info={'host': 'host1', 'port': 8009}) + await hass.async_block_till_done() assert add_devices.call_count == 1 + assert setup_discovery.call_count == 0 - hass.data[cast.ADDED_CAST_DEVICES_KEY] = {} - add_devices = yield from async_setup_cast( + # group should start discovery. + hass.data[cast.ADDED_CAST_DEVICES_KEY] = set() + add_devices = await async_setup_cast( hass, discovery_info={'host': 'host1', 'port': 42}) + await hass.async_block_till_done() assert add_devices.call_count == 0 + assert setup_discovery.call_count == 1 - with patch('pychromecast.Chromecast', - side_effect=pychromecast.ChromecastConnectionError): + +async def test_normal_raises_platform_not_ready(hass): + """Test cast platform raises PlatformNotReady if HTTP dial fails.""" + with patch('pychromecast.dial.get_device_status', return_value=None): with pytest.raises(PlatformNotReady): - yield from async_setup_cast(hass, {'host': 'host3'}) + await async_setup_cast(hass, {'host': 'host1'}) -@asyncio.coroutine -def test_replay_past_chromecasts(hass): +async def test_replay_past_chromecasts(hass): """Test cast platform re-playing past chromecasts when adding new one.""" - cast_group1 = get_fake_chromecast(host='host1', port=42) - cast_group2 = get_fake_chromecast(host='host2', port=42, uuid=UUID( + cast_group1 = get_fake_chromecast_info(host='host1', port=42) + cast_group2 = get_fake_chromecast_info(host='host2', port=42, uuid=UUID( '9462202c-e747-4af5-a66b-7dce0e1ebc09')) - discover_cast, add_dev1 = yield from async_setup_cast_internal_discovery( + discover_cast, add_dev1 = await async_setup_cast_internal_discovery( hass, discovery_info={'host': 'host1', 'port': 42}) discover_cast('service2', cast_group2) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert add_dev1.call_count == 0 discover_cast('service1', cast_group1) - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() # having jobs that add jobs + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs assert add_dev1.call_count == 1 - add_dev2 = yield from async_setup_cast( + add_dev2 = await async_setup_cast( hass, discovery_info={'host': 'host2', 'port': 42}) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert add_dev2.call_count == 1 + + +async def test_entity_media_states(hass: HomeAssistantType): + """Test various entity media states.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, entity = await async_setup_media_player_cast(hass, info) + + state = hass.states.get('media_player.speaker') + assert state is not None + assert state.name == 'Speaker' + assert state.state == 'unknown' + assert entity.unique_id == full_info.uuid + + media_status = MagicMock(images=None) + media_status.player_is_playing = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'playing' + + entity.new_media_status(media_status) + media_status.player_is_playing = False + media_status.player_is_paused = True + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'paused' + + entity.new_media_status(media_status) + media_status.player_is_paused = False + media_status.player_is_idle = True + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'idle' + + media_status.player_is_idle = False + chromecast.is_idle = True + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'off' + + chromecast.is_idle = False + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.state == 'unknown' + + +async def test_switched_host(hass: HomeAssistantType): + """Test cast device listens for changed hosts and disconnects old cast.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, _ = await async_setup_media_player_cast(hass, full_info) + + chromecast2 = get_fake_chromecast(info) + with patch('pychromecast._get_chromecast_from_host', + return_value=chromecast2) as get_chromecast: + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, full_info) + await hass.async_block_till_done() + assert get_chromecast.call_count == 0 + + changed = attr.evolve(full_info, friendly_name='Speaker 2') + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) + await hass.async_block_till_done() + assert get_chromecast.call_count == 0 + + changed = attr.evolve(changed, host='host2') + async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) + await hass.async_block_till_done() + assert get_chromecast.call_count == 1 + chromecast.disconnect.assert_called_once_with(blocking=False) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + chromecast.disconnect.assert_called_once_with(blocking=False) From a17e60208dc9ae7b3915a5b93629489a87e31806 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:15:31 -0700 Subject: [PATCH 131/924] Update translations --- .../.translations/nl.json | 3 ++- .../.translations/vi.json | 24 +++++++++++++++++++ .../components/hue/.translations/de.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../sensor/.translations/season.vi.json | 8 +++++++ 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/config_entry_example/.translations/vi.json create mode 100644 homeassistant/components/sensor/.translations/season.vi.json diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json index 10469dd0804..7b52ac88cf0 100644 --- a/homeassistant/components/config_entry_example/.translations/nl.json +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -18,6 +18,7 @@ "description": "Voer een naam in voor het testen van de entiteit.", "title": "Naam van de entiteit" } - } + }, + "title": "Voorbeeld van de config vermelding" } } \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json new file mode 100644 index 00000000000..e40c4d38e9f --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/vi.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" + }, + "step": { + "init": { + "data": { + "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "name": { + "data": { + "name": "T\u00ean" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" + } + }, + "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index b7094d91528..f11af7756c7 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -6,7 +6,7 @@ "no_bridges": "Philips Hue Bridges entdeckt" }, "error": { - "linking": "Unbekannte Link-Fehler aufgetreten.", + "linking": "Unbekannter Link-Fehler aufgetreten.", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" }, "step": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index cdd26a5b4b2..e364b7033a1 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -17,7 +17,7 @@ "title": "Wybierz mostek Hue" }, "link": { - "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant. ", + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", "title": "Hub Link" } }, diff --git a/homeassistant/components/sensor/.translations/season.vi.json b/homeassistant/components/sensor/.translations/season.vi.json new file mode 100644 index 00000000000..a3bb21dee27 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.vi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "M\u00f9a thu", + "spring": "M\u00f9a xu\u00e2n", + "summer": "M\u00f9a h\u00e8", + "winter": "M\u00f9a \u0111\u00f4ng" + } +} \ No newline at end of file From 725e1ddfc116ff6f06e5d7574321ffc96d30faf8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:15:31 -0700 Subject: [PATCH 132/924] Update translations --- .../.translations/nl.json | 3 ++- .../.translations/vi.json | 24 +++++++++++++++++++ .../components/hue/.translations/de.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../sensor/.translations/season.vi.json | 8 +++++++ 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/config_entry_example/.translations/vi.json create mode 100644 homeassistant/components/sensor/.translations/season.vi.json diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json index 10469dd0804..7b52ac88cf0 100644 --- a/homeassistant/components/config_entry_example/.translations/nl.json +++ b/homeassistant/components/config_entry_example/.translations/nl.json @@ -18,6 +18,7 @@ "description": "Voer een naam in voor het testen van de entiteit.", "title": "Naam van de entiteit" } - } + }, + "title": "Voorbeeld van de config vermelding" } } \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json new file mode 100644 index 00000000000..e40c4d38e9f --- /dev/null +++ b/homeassistant/components/config_entry_example/.translations/vi.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" + }, + "step": { + "init": { + "data": { + "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" + }, + "name": { + "data": { + "name": "T\u00ean" + }, + "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", + "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" + } + }, + "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index b7094d91528..f11af7756c7 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -6,7 +6,7 @@ "no_bridges": "Philips Hue Bridges entdeckt" }, "error": { - "linking": "Unbekannte Link-Fehler aufgetreten.", + "linking": "Unbekannter Link-Fehler aufgetreten.", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" }, "step": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index cdd26a5b4b2..e364b7033a1 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -17,7 +17,7 @@ "title": "Wybierz mostek Hue" }, "link": { - "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant. ", + "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue z Home Assistant.", "title": "Hub Link" } }, diff --git a/homeassistant/components/sensor/.translations/season.vi.json b/homeassistant/components/sensor/.translations/season.vi.json new file mode 100644 index 00000000000..a3bb21dee27 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.vi.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "M\u00f9a thu", + "spring": "M\u00f9a xu\u00e2n", + "summer": "M\u00f9a h\u00e8", + "winter": "M\u00f9a \u0111\u00f4ng" + } +} \ No newline at end of file From b159484a79b769b280884ed1ceb5f595d149c671 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:16:17 -0700 Subject: [PATCH 133/924] Version bump to 0.67.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4ce2f503ad6..d286aa85458 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 66 +MINOR_VERSION = 67 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 101b39300b9fcbb01fdc9f237caf37a3a386189a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:17:36 -0700 Subject: [PATCH 134/924] Version bump to 0.66.0.beta0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4ce2f503ad6..b0be1933ffe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0.beta0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 8e14e803cb985943b83339091b8ecb2965cf309a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Mar 2018 14:27:05 -0700 Subject: [PATCH 135/924] Fix release script --- script/release | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/release b/script/release index 65a6339cedc..17d5ad9370d 100755 --- a/script/release +++ b/script/release @@ -21,9 +21,9 @@ fi CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD` -if [ "$CURRENT_BRANCH" != "master" ] +if [ "$CURRENT_BRANCH" != "master" ] && [ "$CURRENT_BRANCH" != "rc" ] then - echo "You have to be on the master branch to release." + echo "You have to be on the master or rc branch to release." exit 1 fi From 872b6cf16b458a8b69abc32fc45bcbbd6392846e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Mar 2018 23:22:33 +0100 Subject: [PATCH 136/924] Updates default Pilight port number (#13419) --- homeassistant/components/pilight.py | 2 +- tests/components/test_pilight.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pilight.py b/homeassistant/components/pilight.py index 71e8232e8c2..344c750c0ec 100644 --- a/homeassistant/components/pilight.py +++ b/homeassistant/components/pilight.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) CONF_SEND_DELAY = 'send_delay' DEFAULT_HOST = '127.0.0.1' -DEFAULT_PORT = 5000 +DEFAULT_PORT = 5001 DEFAULT_SEND_DELAY = 0.0 DOMAIN = 'pilight' diff --git a/tests/components/test_pilight.py b/tests/components/test_pilight.py index 06ad84e7a34..24052a56839 100644 --- a/tests/components/test_pilight.py +++ b/tests/components/test_pilight.py @@ -81,7 +81,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_connection_failed_error(self, mock_error): - """Try to connect at 127.0.0.1:5000 with socket error.""" + """Try to connect at 127.0.0.1:5001 with socket error.""" with assert_setup_component(4): with patch('pilight.pilight.Client', side_effect=socket.error) as mock_client: @@ -93,7 +93,7 @@ class TestPilight(unittest.TestCase): @patch('homeassistant.components.pilight._LOGGER.error') def test_connection_timeout_error(self, mock_error): - """Try to connect at 127.0.0.1:5000 with socket timeout.""" + """Try to connect at 127.0.0.1:5001 with socket timeout.""" with assert_setup_component(4): with patch('pilight.pilight.Client', side_effect=socket.timeout) as mock_client: From 8bd5f66c5731e5ab1fa42886ea5435cf88cf0e20 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 23 Mar 2018 23:50:32 +0100 Subject: [PATCH 137/924] Upgrade mypy to 0.580 (#13420) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index fc9e113e97c..afcdec23a00 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.570 +mypy==0.580 pydocstyle==1.1.1 pylint==1.8.2 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2c1df2d3bf..02505138343 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.570 +mypy==0.580 pydocstyle==1.1.1 pylint==1.8.2 pytest-aiohttp==0.3.0 From 4d52875229b1709f52470354e9fbd06d38c8d17c Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Sat, 24 Mar 2018 12:16:49 +0100 Subject: [PATCH 138/924] Update to new "b2vapi" of BMW ConnectedDrive (#13305) * updated to new "b2vapi" of bimmer_connected * updated requirements_all.txt * updated 2 more vehicle names after rebase * cleanup of import statements * found one more broken name... * removed unused constant * cleanup of import statements 2 --- .../binary_sensor/bmw_connected_drive.py | 13 +++++---- .../components/bmw_connected_drive.py | 28 ++++++++++--------- .../device_tracker/bmw_connected_drive.py | 6 ++-- .../components/lock/bmw_connected_drive.py | 20 +++++++------ .../components/sensor/bmw_connected_drive.py | 12 ++++---- requirements_all.txt | 2 +- 6 files changed, 44 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0f3edd86dcd..e7af5af988b 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -7,8 +7,8 @@ https://home-assistant.io/components/binary_sensor.bmw_connected_drive/ import asyncio import logging -from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN DEPENDENCIES = ['bmw_connected_drive'] @@ -45,7 +45,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._sensor_name = sensor_name self._device_class = device_class self._state = None @@ -75,7 +75,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state result = { - 'car': self._vehicle.modelName + 'car': self._vehicle.name } if self._attribute == 'lids': @@ -91,6 +91,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def update(self): """Read new state data from the library.""" + from bimmer_connected.state import LockState vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed @@ -101,9 +102,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._state = not vehicle_state.all_windows_closed # device class safety: On means unsafe, Off means safe if self._attribute == 'door_lock_state': - # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED - self._state = bool(vehicle_state.door_lock_state.value - in ('SELECTIVELOCKED', 'UNLOCKED')) + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._state = vehicle_state.door_lock_state not in \ + [LockState.LOCKED, LockState.SECURED] def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive.py index 9e9e2bafac5..48452b6d79b 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive.py @@ -4,30 +4,29 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/bmw_connected_drive/ """ -import logging import datetime +import logging import voluptuous as vol + +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change - import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD -) -REQUIREMENTS = ['bimmer_connected==0.4.1'] +REQUIREMENTS = ['bimmer_connected==0.5.0'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'bmw_connected_drive' -CONF_VALUES = 'values' -CONF_COUNTRY = 'country' +CONF_REGION = 'region' + ACCOUNT_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_COUNTRY): cv.string, + vol.Required(CONF_REGION): vol.Any('north_america', 'china', + 'rest_of_world'), }) CONFIG_SCHEMA = vol.Schema({ @@ -47,9 +46,9 @@ def setup(hass, config): for name, account_config in config[DOMAIN].items(): username = account_config[CONF_USERNAME] password = account_config[CONF_PASSWORD] - country = account_config[CONF_COUNTRY] + region = account_config[CONF_REGION] _LOGGER.debug('Adding new account %s', name) - bimmer = BMWConnectedDriveAccount(username, password, country, name) + bimmer = BMWConnectedDriveAccount(username, password, region, name) accounts.append(bimmer) # update every UPDATE_INTERVAL minutes, starting now @@ -75,12 +74,15 @@ def setup(hass, config): class BMWConnectedDriveAccount(object): """Representation of a BMW vehicle.""" - def __init__(self, username: str, password: str, country: str, + def __init__(self, username: str, password: str, region_str: str, name: str) -> None: """Constructor.""" from bimmer_connected.account import ConnectedDriveAccount + from bimmer_connected.country_selector import get_region_from_name - self.account = ConnectedDriveAccount(username, password, country) + region = get_region_from_name(region_str) + + self.account = ConnectedDriveAccount(username, password, region) self.name = name self._update_listeners = [] diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py index 6ba2681e4cd..1e501c0e199 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/device_tracker/bmw_connected_drive.py @@ -37,15 +37,15 @@ class BMWDeviceTracker(object): def update(self) -> None: """Update the device info.""" - dev_id = slugify(self.vehicle.modelName) + dev_id = slugify(self.vehicle.name) _LOGGER.debug('Updating %s', dev_id) attrs = { 'trackr_id': dev_id, 'id': dev_id, - 'name': self.vehicle.modelName + 'name': self.vehicle.name } self._see( - dev_id=dev_id, host_name=self.vehicle.modelName, + dev_id=dev_id, host_name=self.vehicle.name, gps=self.vehicle.state.gps_position, attributes=attrs, icon='mdi:car' ) diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index c500e02b2f7..c992bf1225a 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -37,7 +37,7 @@ class BMWLock(LockDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._sensor_name = sensor_name self._state = None @@ -59,7 +59,7 @@ class BMWLock(LockDevice): """Return the state attributes of the lock.""" vehicle_state = self._vehicle.state return { - 'car': self._vehicle.modelName, + 'car': self._vehicle.name, 'door_lock_state': vehicle_state.door_lock_state.value } @@ -70,7 +70,7 @@ class BMWLock(LockDevice): def lock(self, **kwargs): """Lock the car.""" - _LOGGER.debug("%s: locking doors", self._vehicle.modelName) + _LOGGER.debug("%s: locking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the # update callback response self._state = STATE_LOCKED @@ -79,7 +79,7 @@ class BMWLock(LockDevice): def unlock(self, **kwargs): """Unlock the car.""" - _LOGGER.debug("%s: unlocking doors", self._vehicle.modelName) + _LOGGER.debug("%s: unlocking doors", self._vehicle.name) # Optimistic state set here because it takes some time before the # update callback response self._state = STATE_UNLOCKED @@ -88,13 +88,17 @@ class BMWLock(LockDevice): def update(self): """Update state of the lock.""" - _LOGGER.debug("%s: updating data for %s", self._vehicle.modelName, + from bimmer_connected.state import LockState + + _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) vehicle_state = self._vehicle.state - # Possible values: LOCKED, SECURED, SELECTIVELOCKED, UNLOCKED - self._state = (STATE_LOCKED if vehicle_state.door_lock_state.value - in ('LOCKED', 'SECURED') else STATE_UNLOCKED) + # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED + self._state = STATE_LOCKED \ + if vehicle_state.door_lock_state \ + in [LockState.LOCKED, LockState.SECURED] \ + else STATE_UNLOCKED def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 3208c7377df..bd582da1ef4 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -4,8 +4,8 @@ Reads vehicle status from BMW connected drive portal. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.bmw_connected_drive/ """ -import logging import asyncio +import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.helpers.entity import Entity @@ -51,7 +51,7 @@ class BMWConnectedDriveSensor(Entity): self._attribute = attribute self._state = None self._unit_of_measurement = None - self._name = '{} {}'.format(self._vehicle.modelName, self._attribute) + self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._sensor_name = sensor_name self._icon = icon @@ -88,19 +88,19 @@ class BMWConnectedDriveSensor(Entity): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" return { - 'car': self._vehicle.modelName + 'car': self._vehicle.name } def update(self) -> None: """Read new state data from the library.""" - _LOGGER.debug('Updating %s', self._vehicle.modelName) + _LOGGER.debug('Updating %s', self._vehicle.name) vehicle_state = self._vehicle.state self._state = getattr(vehicle_state, self._attribute) if self._attribute in LENGTH_ATTRIBUTES: - self._unit_of_measurement = vehicle_state.unit_of_length + self._unit_of_measurement = 'km' elif self._attribute == 'remaining_fuel': - self._unit_of_measurement = vehicle_state.unit_of_volume + self._unit_of_measurement = 'l' else: self._unit_of_measurement = None diff --git a/requirements_all.txt b/requirements_all.txt index 52833969872..8e284b3d2a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -137,7 +137,7 @@ beautifulsoup4==4.6.0 bellows==0.5.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.4.1 +bimmer_connected==0.5.0 # homeassistant.components.blink blinkpy==0.6.0 From df35159cb4a80adafee05821077d5e908f23587c Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 24 Mar 2018 16:33:49 -0400 Subject: [PATCH 139/924] Add code owner for Manual Alarm with MQTT Control (#13438) --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index d8ebc3cff56..b7f84cf02f5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -43,6 +43,7 @@ homeassistant/components/hassio.py @home-assistant/hassio # Individual components homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt +homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell homeassistant/components/binary_sensor/hikvision.py @mezz64 homeassistant/components/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/camera/yi.py @bachya From 11930d5f202a3ab409489a216da8e2690363448c Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 24 Mar 2018 17:13:12 -0400 Subject: [PATCH 140/924] QNAP updates (#13435) * Add @colinodell to CODEOWNERS for qnap sensor * Bump qnapstats library to 0.2.5 This release adds better error handling for sharenames with no folder --- CODEOWNERS | 1 + homeassistant/components/sensor/qnap.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b7f84cf02f5..9528e7a09e9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -70,6 +70,7 @@ homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/pollen.py @bachya +homeassistant/components/sensor/qnap.py @colinodell homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/tibber.py @danielhiversen diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 09c9938f1c1..629a5f6a0ee 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['qnapstats==0.2.4'] +REQUIREMENTS = ['qnapstats==0.2.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8e284b3d2a9..fac18e23667 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1054,7 +1054,7 @@ pyxeoma==1.4.0 pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.4 +qnapstats==0.2.5 # homeassistant.components.switch.rachio rachiopy==0.1.2 From e36f27d6fd5b0655375bb90c66cbe108fe967269 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 24 Mar 2018 23:04:43 +0100 Subject: [PATCH 141/924] Xiaomi MiIO Fan: Xiaomi Air Humidifier integration (#12627) * Device support for the Xiaomi Air Humidifier. * Requirements updated. * "continuation line under-indented for visual indent" fixed. * Make hound happy. * Inadvertently added light.xiaomi_miio component removed from PR. * Service descriptions added. * One of the pylint errors fixed. * Redundancy removed. * pylint: disable=no-self-use added. The method signature is important here. * Pylint fixed. * Use a unique data key per domain. * Review incorporated. * Map of available attributes added. * Pylint fixed. Attribute "volume" added. * Don't use the support flag bit mask as model identifier. Determine support features and attributes at the constructor. Use starred expressions at dicts instead of copies. * Blank line removed. * Use Async / await syntax. * Make hound happy. * Xiaomi Air Humidifier CA support added. * Duplicate method removed. * Air Purifier V3 support added. * Don't abuse the system property supported_features anymore. * python-miio version bumped. * Clean-up. * Additional supported features refactoring completed. * Additional supported features renamed properly. * Unique id added. * Device unavailable handling improved. * Refactoring. * Missed const updated. * Incomplete Air Humidifier CA support fixed. * Review incorporated * The Air Humidifier CA supports the operation mode "auto" - the standard version doesn't * Attributes are part of the common set already * Revert "Attributes are part of the common set already" This reverts commit 40b443eba0e2fc55075479fd540f977fbf4b704a. * Comment added * Service description of the set_dry_{on,off} service added * Typo fixed --- homeassistant/components/fan/services.yaml | 111 ++- homeassistant/components/fan/xiaomi_miio.py | 741 ++++++++++++++++---- 2 files changed, 691 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index a306cf7767c..a74f67b83fb 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -68,50 +68,50 @@ xiaomi_miio_set_buzzer_on: description: Turn the buzzer on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_buzzer_off: description: Turn the buzzer off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_led_on: description: Turn the led on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_led_off: description: Turn the led off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_child_lock_on: description: Turn the child lock on. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_child_lock_off: description: Turn the child lock off. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' xiaomi_miio_set_favorite_level: description: Set the favorite level. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' level: description: Level, between 0 and 16. example: 1 @@ -120,8 +120,87 @@ xiaomi_miio_set_led_brightness: description: Set the led brightness. fields: entity_id: - description: Name of the air purifier entity. - example: 'fan.xiaomi_air_purifier' + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' brightness: description: Brightness (0 = Bright, 1 = Dim, 2 = Off) example: 1 + +xiaomi_miio_set_auto_detect_on: + description: Turn the auto detect on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_auto_detect_off: + description: Turn the auto detect off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_learn_mode_on: + description: Turn the learn mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_learn_mode_off: + description: Turn the learn mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_volume: + description: Set the sound volume. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + volume: + description: Volume, between 0 and 100. + example: 50 + +xiaomi_miio_reset_filter: + description: Reset the filter lifetime and usage. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_extra_features: + description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + features: + description: Integer, known values are 0 (default) and 1 (turbo mode). + example: 1 + +xiaomi_miio_set_target_humidity: + description: Set the target humidity. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + humidity: + description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. + example: 50 + +xiaomi_miio_set_dry_on: + description: Turn the dry mode on. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' + +xiaomi_miio_set_dry_off: + description: Turn the dry mode off. + fields: + entity_id: + description: Name of the xiaomi miio entity. + example: 'fan.xiaomi_miio_device' diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 09df55200a2..a1cb0431381 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -1,16 +1,16 @@ """ -Support for Xiaomi Mi Air Purifier 2. +Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier. For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.xiaomi_miio/ """ import asyncio +from enum import Enum from functools import partial import logging import voluptuous as vol -from homeassistant.helpers.entity import ToggleEntity from homeassistant.components.fan import (FanEntity, PLATFORM_SCHEMA, SUPPORT_SET_SPEED, DOMAIN, ) from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_TOKEN, @@ -20,17 +20,40 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'Xiaomi Air Purifier' -PLATFORM = 'xiaomi_miio' +DEFAULT_NAME = 'Xiaomi Miio Device' +DATA_KEY = 'fan.xiaomi_miio' + +CONF_MODEL = 'model' +MODEL_AIRPURIFIER_PRO = 'zhimi.airpurifier.v6' +MODEL_AIRPURIFIER_V3 = 'zhimi.airpurifier.v3' +MODEL_AIRHUMIDIFIER_V1 = 'zhimi.humidifier.v1' +MODEL_AIRHUMIDIFIER_CA = 'zhimi.humidifier.ca1' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODEL): vol.In( + ['zhimi.airpurifier.m1', + 'zhimi.airpurifier.m2', + 'zhimi.airpurifier.ma1', + 'zhimi.airpurifier.ma2', + 'zhimi.airpurifier.sa1', + 'zhimi.airpurifier.sa2', + 'zhimi.airpurifier.v1', + 'zhimi.airpurifier.v2', + 'zhimi.airpurifier.v3', + 'zhimi.airpurifier.v5', + 'zhimi.airpurifier.v6', + 'zhimi.humidifier.v1', + 'zhimi.humidifier.ca1']), }) REQUIREMENTS = ['python-miio==0.3.8'] +ATTR_MODEL = 'model' + +# Air Purifier ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' ATTR_AIR_QUALITY_INDEX = 'aqi' @@ -45,20 +68,190 @@ ATTR_LED_BRIGHTNESS = 'led_brightness' ATTR_MOTOR_SPEED = 'motor_speed' ATTR_AVERAGE_AIR_QUALITY_INDEX = 'average_aqi' ATTR_PURIFY_VOLUME = 'purify_volume' - ATTR_BRIGHTNESS = 'brightness' ATTR_LEVEL = 'level' +ATTR_MOTOR2_SPEED = 'motor2_speed' +ATTR_ILLUMINANCE = 'illuminance' +ATTR_FILTER_RFID_PRODUCT_ID = 'filter_rfid_product_id' +ATTR_FILTER_RFID_TAG = 'filter_rfid_tag' +ATTR_FILTER_TYPE = 'filter_type' +ATTR_LEARN_MODE = 'learn_mode' +ATTR_SLEEP_TIME = 'sleep_time' +ATTR_SLEEP_LEARN_COUNT = 'sleep_mode_learn_count' +ATTR_EXTRA_FEATURES = 'extra_features' +ATTR_FEATURES = 'features' +ATTR_TURBO_MODE_SUPPORTED = 'turbo_mode_supported' +ATTR_AUTO_DETECT = 'auto_detect' +ATTR_SLEEP_MODE = 'sleep_mode' +ATTR_VOLUME = 'volume' +ATTR_USE_TIME = 'use_time' +ATTR_BUTTON_PRESSED = 'button_pressed' + +# Air Humidifier +ATTR_TARGET_HUMIDITY = 'target_humidity' +ATTR_TRANS_LEVEL = 'trans_level' +ATTR_HARDWARE_VERSION = 'hardware_version' + +# Air Humidifier CA +ATTR_SPEED = 'speed' +ATTR_DEPTH = 'depth' +ATTR_DRY = 'dry' + +# Map attributes to properties of the state object +AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON = { + ATTR_TEMPERATURE: 'temperature', + ATTR_HUMIDITY: 'humidity', + ATTR_AIR_QUALITY_INDEX: 'aqi', + ATTR_MODE: 'mode', + ATTR_FILTER_HOURS_USED: 'filter_hours_used', + ATTR_FILTER_LIFE: 'filter_life_remaining', + ATTR_FAVORITE_LEVEL: 'favorite_level', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_LED: 'led', + ATTR_MOTOR_SPEED: 'motor_speed', + ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_LEARN_MODE: 'learn_mode', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_EXTRA_FEATURES: 'extra_features', + ATTR_TURBO_MODE_SUPPORTED: 'turbo_mode_supported', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', + ATTR_BUTTON_PRESSED: 'button_pressed', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_BUZZER: 'buzzer', + ATTR_LED_BRIGHTNESS: 'led_brightness', + ATTR_SLEEP_MODE: 'sleep_mode', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO = { + **AVAILABLE_ATTRIBUTES_AIRPURIFIER_COMMON, + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_VOLUME: 'volume', +} + +AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 = { + # Common set isn't used here. It's a very basic version of the device. + ATTR_AIR_QUALITY_INDEX: 'aqi', + ATTR_MODE: 'mode', + ATTR_LED: 'led', + ATTR_BUZZER: 'buzzer', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_ILLUMINANCE: 'illuminance', + ATTR_FILTER_HOURS_USED: 'filter_hours_used', + ATTR_FILTER_LIFE: 'filter_life_remaining', + ATTR_MOTOR_SPEED: 'motor_speed', + # perhaps supported but unconfirmed + ATTR_AVERAGE_AIR_QUALITY_INDEX: 'average_aqi', + ATTR_VOLUME: 'volume', + ATTR_MOTOR2_SPEED: 'motor2_speed', + ATTR_FILTER_RFID_PRODUCT_ID: 'filter_rfid_product_id', + ATTR_FILTER_RFID_TAG: 'filter_rfid_tag', + ATTR_FILTER_TYPE: 'filter_type', + ATTR_PURIFY_VOLUME: 'purify_volume', + ATTR_LEARN_MODE: 'learn_mode', + ATTR_SLEEP_TIME: 'sleep_time', + ATTR_SLEEP_LEARN_COUNT: 'sleep_mode_learn_count', + ATTR_EXTRA_FEATURES: 'extra_features', + ATTR_AUTO_DETECT: 'auto_detect', + ATTR_USE_TIME: 'use_time', + ATTR_BUTTON_PRESSED: 'button_pressed', +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { + ATTR_TEMPERATURE: 'temperature', + ATTR_HUMIDITY: 'humidity', + ATTR_MODE: 'mode', + ATTR_BUZZER: 'buzzer', + ATTR_CHILD_LOCK: 'child_lock', + ATTR_TRANS_LEVEL: 'trans_level', + ATTR_TARGET_HUMIDITY: 'target_humidity', + ATTR_LED_BRIGHTNESS: 'led_brightness', + ATTR_BUTTON_PRESSED: 'button_pressed', + ATTR_USE_TIME: 'use_time', + ATTR_HARDWARE_VERSION: 'hardware_version', +} + +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { + **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER, + ATTR_SPEED: 'speed', + ATTR_DEPTH: 'depth', + ATTR_DRY: 'dry', +} + +OPERATION_MODES_AIRPURIFIER = ['Auto', 'Silent', 'Favorite', 'Idle'] +OPERATION_MODES_AIRPURIFIER_PRO = ['Auto', 'Silent', 'Favorite'] +OPERATION_MODES_AIRPURIFIER_V3 = ['Auto', 'Silent', 'Favorite', 'Idle', + 'Medium', 'High', 'Strong'] SUCCESS = ['ok'] +FEATURE_SET_BUZZER = 1 +FEATURE_SET_LED = 2 +FEATURE_SET_CHILD_LOCK = 4 +FEATURE_SET_LED_BRIGHTNESS = 8 +FEATURE_SET_FAVORITE_LEVEL = 16 +FEATURE_SET_AUTO_DETECT = 32 +FEATURE_SET_LEARN_MODE = 64 +FEATURE_SET_VOLUME = 128 +FEATURE_RESET_FILTER = 256 +FEATURE_SET_EXTRA_FEATURES = 512 +FEATURE_SET_TARGET_HUMIDITY = 1024 +FEATURE_SET_DRY = 2048 + +FEATURE_FLAGS_GENERIC = (FEATURE_SET_BUZZER | + FEATURE_SET_CHILD_LOCK) + +FEATURE_FLAGS_AIRPURIFIER = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED | + FEATURE_SET_LED_BRIGHTNESS | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_LEARN_MODE | + FEATURE_RESET_FILTER | + FEATURE_SET_EXTRA_FEATURES) + +FEATURE_FLAGS_AIRPURIFIER_PRO = (FEATURE_SET_CHILD_LOCK | + FEATURE_SET_LED | + FEATURE_SET_FAVORITE_LEVEL | + FEATURE_SET_AUTO_DETECT | + FEATURE_SET_VOLUME) + +FEATURE_FLAGS_AIRPURIFIER_V3 = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED) + +FEATURE_FLAGS_AIRHUMIDIFIER = (FEATURE_FLAGS_GENERIC | + FEATURE_SET_LED_BRIGHTNESS | + FEATURE_SET_TARGET_HUMIDITY) + +FEATURE_FLAGS_AIRHUMIDIFIER_CA = (FEATURE_FLAGS_AIRHUMIDIFIER | + FEATURE_SET_DRY) + SERVICE_SET_BUZZER_ON = 'xiaomi_miio_set_buzzer_on' SERVICE_SET_BUZZER_OFF = 'xiaomi_miio_set_buzzer_off' SERVICE_SET_LED_ON = 'xiaomi_miio_set_led_on' SERVICE_SET_LED_OFF = 'xiaomi_miio_set_led_off' SERVICE_SET_CHILD_LOCK_ON = 'xiaomi_miio_set_child_lock_on' SERVICE_SET_CHILD_LOCK_OFF = 'xiaomi_miio_set_child_lock_off' -SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' SERVICE_SET_LED_BRIGHTNESS = 'xiaomi_miio_set_led_brightness' +SERVICE_SET_FAVORITE_LEVEL = 'xiaomi_miio_set_favorite_level' +SERVICE_SET_AUTO_DETECT_ON = 'xiaomi_miio_set_auto_detect_on' +SERVICE_SET_AUTO_DETECT_OFF = 'xiaomi_miio_set_auto_detect_off' +SERVICE_SET_LEARN_MODE_ON = 'xiaomi_miio_set_learn_mode_on' +SERVICE_SET_LEARN_MODE_OFF = 'xiaomi_miio_set_learn_mode_off' +SERVICE_SET_VOLUME = 'xiaomi_miio_set_volume' +SERVICE_RESET_FILTER = 'xiaomi_miio_reset_filter' +SERVICE_SET_EXTRA_FEATURES = 'xiaomi_miio_set_extra_features' +SERVICE_SET_TARGET_HUMIDITY = 'xiaomi_miio_set_target_humidity' +SERVICE_SET_DRY_ON = 'xiaomi_miio_set_dry_on' +SERVICE_SET_DRY_OFF = 'xiaomi_miio_set_dry_off' AIRPURIFIER_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, @@ -74,6 +267,21 @@ SERVICE_SCHEMA_FAVORITE_LEVEL = AIRPURIFIER_SERVICE_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Clamp(min=0, max=16)) }) +SERVICE_SCHEMA_VOLUME = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_VOLUME): + vol.All(vol.Coerce(int), vol.Clamp(min=0, max=100)) +}) + +SERVICE_SCHEMA_EXTRA_FEATURES = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_FEATURES): + vol.All(vol.Coerce(int), vol.Range(min=0)) +}) + +SERVICE_SCHEMA_TARGET_HUMIDITY = AIRPURIFIER_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_HUMIDITY): + vol.All(vol.Coerce(int), vol.In([30, 40, 50, 60, 70, 80])) +}) + SERVICE_TO_METHOD = { SERVICE_SET_BUZZER_ON: {'method': 'async_set_buzzer_on'}, SERVICE_SET_BUZZER_OFF: {'method': 'async_set_buzzer_off'}, @@ -81,59 +289,99 @@ SERVICE_TO_METHOD = { SERVICE_SET_LED_OFF: {'method': 'async_set_led_off'}, SERVICE_SET_CHILD_LOCK_ON: {'method': 'async_set_child_lock_on'}, SERVICE_SET_CHILD_LOCK_OFF: {'method': 'async_set_child_lock_off'}, - SERVICE_SET_FAVORITE_LEVEL: { - 'method': 'async_set_favorite_level', - 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, + SERVICE_SET_AUTO_DETECT_ON: {'method': 'async_set_auto_detect_on'}, + SERVICE_SET_AUTO_DETECT_OFF: {'method': 'async_set_auto_detect_off'}, + SERVICE_SET_LEARN_MODE_ON: {'method': 'async_set_learn_mode_on'}, + SERVICE_SET_LEARN_MODE_OFF: {'method': 'async_set_learn_mode_off'}, + SERVICE_RESET_FILTER: {'method': 'async_reset_filter'}, SERVICE_SET_LED_BRIGHTNESS: { 'method': 'async_set_led_brightness', 'schema': SERVICE_SCHEMA_LED_BRIGHTNESS}, + SERVICE_SET_FAVORITE_LEVEL: { + 'method': 'async_set_favorite_level', + 'schema': SERVICE_SCHEMA_FAVORITE_LEVEL}, + SERVICE_SET_VOLUME: { + 'method': 'async_set_volume', + 'schema': SERVICE_SCHEMA_VOLUME}, + SERVICE_SET_EXTRA_FEATURES: { + 'method': 'async_set_extra_features', + 'schema': SERVICE_SCHEMA_EXTRA_FEATURES}, + SERVICE_SET_TARGET_HUMIDITY: { + 'method': 'async_set_target_humidity', + 'schema': SERVICE_SCHEMA_TARGET_HUMIDITY}, + SERVICE_SET_DRY_ON: {'method': 'async_set_dry_on'}, + SERVICE_SET_DRY_OFF: {'method': 'async_set_dry_off'}, } # pylint: disable=unused-argument -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the air purifier from config.""" - from miio import AirPurifier, DeviceException - if PLATFORM not in hass.data: - hass.data[PLATFORM] = {} +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the miio fan device from config.""" + from miio import Device, DeviceException + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} host = config.get(CONF_HOST) name = config.get(CONF_NAME) token = config.get(CONF_TOKEN) + model = config.get(CONF_MODEL) _LOGGER.info("Initializing with host %s (token %s...)", host, token[:5]) + unique_id = None - try: + if model is None: + try: + miio_device = Device(host, token) + device_info = miio_device.info() + model = device_info.model + unique_id = "{}-{}".format(model, device_info.mac_address) + _LOGGER.info("%s %s %s detected", + model, + device_info.firmware_version, + device_info.hardware_version) + except DeviceException: + raise PlatformNotReady + + if model.startswith('zhimi.airpurifier.'): + from miio import AirPurifier air_purifier = AirPurifier(host, token) + device = XiaomiAirPurifier(name, air_purifier, model, unique_id) + elif model.startswith('zhimi.humidifier.'): + from miio import AirHumidifier + air_humidifier = AirHumidifier(host, token) + device = XiaomiAirHumidifier(name, air_humidifier, model, unique_id) + else: + _LOGGER.error( + 'Unsupported device found! Please create an issue at ' + 'https://github.com/syssi/xiaomi_airpurifier/issues ' + 'and provide the following data: %s', model) + return False - xiaomi_air_purifier = XiaomiAirPurifier(name, air_purifier) - hass.data[PLATFORM][host] = xiaomi_air_purifier - except DeviceException: - raise PlatformNotReady + hass.data[DATA_KEY][host] = device + async_add_devices([device], update_before_add=True) - async_add_devices([xiaomi_air_purifier], update_before_add=True) - - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on XiaomiAirPurifier.""" method = SERVICE_TO_METHOD.get(service.service) params = {key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID} entity_ids = service.data.get(ATTR_ENTITY_ID) if entity_ids: - devices = [device for device in hass.data[PLATFORM].values() if + devices = [device for device in hass.data[DATA_KEY].values() if device.entity_id in entity_ids] else: - devices = hass.data[PLATFORM].values() + devices = hass.data[DATA_KEY].values() update_tasks = [] for device in devices: - yield from getattr(device, method['method'])(**params) + if not hasattr(device, method['method']): + continue + await getattr(device, method['method'])(**params) update_tasks.append(device.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for air_purifier_service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[air_purifier_service].get( @@ -142,31 +390,22 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): DOMAIN, air_purifier_service, async_service_handler, schema=schema) -class XiaomiAirPurifier(FanEntity): - """Representation of a Xiaomi Air Purifier.""" +class XiaomiGenericDevice(FanEntity): + """Representation of a generic Xiaomi device.""" - def __init__(self, name, air_purifier): - """Initialize the air purifier.""" + def __init__(self, name, device, model, unique_id): + """Initialize the generic Xiaomi device.""" self._name = name + self._device = device + self._model = model + self._unique_id = unique_id - self._air_purifier = air_purifier + self._available = False self._state = None self._state_attrs = { - ATTR_AIR_QUALITY_INDEX: None, - ATTR_TEMPERATURE: None, - ATTR_HUMIDITY: None, - ATTR_MODE: None, - ATTR_FILTER_HOURS_USED: None, - ATTR_FILTER_LIFE: None, - ATTR_FAVORITE_LEVEL: None, - ATTR_BUZZER: None, - ATTR_CHILD_LOCK: None, - ATTR_LED: None, - ATTR_LED_BRIGHTNESS: None, - ATTR_MOTOR_SPEED: None, - ATTR_AVERAGE_AIR_QUALITY_INDEX: None, - ATTR_PURIFY_VOLUME: None, + ATTR_MODEL: self._model, } + self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @property @@ -176,9 +415,14 @@ class XiaomiAirPurifier(FanEntity): @property def should_poll(self): - """Poll the fan.""" + """Poll the device.""" return True + @property + def unique_id(self): + """Return an unique ID.""" + return self._unique_id + @property def name(self): """Return the name of the device if any.""" @@ -187,7 +431,7 @@ class XiaomiAirPurifier(FanEntity): @property def available(self): """Return true when state is known.""" - return self._state is not None + return self._available @property def device_state_attributes(self): @@ -196,50 +440,116 @@ class XiaomiAirPurifier(FanEntity): @property def is_on(self): - """Return true if fan is on.""" + """Return true if device is on.""" return self._state - @asyncio.coroutine - def _try_command(self, mask_error, func, *args, **kwargs): - """Call an air purifier command handling error messages.""" + @staticmethod + def _extract_value_from_attribute(state, attribute): + value = getattr(state, attribute) + if isinstance(value, Enum): + return value.value + + return value + + async def _try_command(self, mask_error, func, *args, **kwargs): + """Call a miio device command handling error messages.""" from miio import DeviceException try: - result = yield from self.hass.async_add_job( + result = await self.hass.async_add_job( partial(func, *args, **kwargs)) - _LOGGER.debug("Response received from air purifier: %s", result) + _LOGGER.debug("Response received from miio device: %s", result) return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) + self._available = False return False - @asyncio.coroutine - def async_turn_on(self: ToggleEntity, speed: str = None, **kwargs) -> None: - """Turn the fan on.""" + async def async_turn_on(self, speed: str = None, + **kwargs) -> None: + """Turn the device on.""" if speed: # If operation mode was set the device must not be turned on. - result = yield from self.async_set_speed(speed) + result = await self.async_set_speed(speed) else: - result = yield from self._try_command( - "Turning the air purifier on failed.", self._air_purifier.on) + result = await self._try_command( + "Turning the miio device on failed.", self._device.on) if result: self._state = True self._skip_update = True - @asyncio.coroutine - def async_turn_off(self: ToggleEntity, **kwargs) -> None: - """Turn the fan off.""" - result = yield from self._try_command( - "Turning the air purifier off failed.", self._air_purifier.off) + async def async_turn_off(self, **kwargs) -> None: + """Turn the device off.""" + result = await self._try_command( + "Turning the miio device off failed.", self._device.off) if result: self._state = False self._skip_update = True - @asyncio.coroutine - def async_update(self): + async def async_set_buzzer_on(self): + """Turn the buzzer on.""" + if self._device_features & FEATURE_SET_BUZZER == 0: + return + + await self._try_command( + "Turning the buzzer of the miio device on failed.", + self._device.set_buzzer, True) + + async def async_set_buzzer_off(self): + """Turn the buzzer off.""" + if self._device_features & FEATURE_SET_BUZZER == 0: + return + + await self._try_command( + "Turning the buzzer of the miio device off failed.", + self._device.set_buzzer, False) + + async def async_set_child_lock_on(self): + """Turn the child lock on.""" + if self._device_features & FEATURE_SET_CHILD_LOCK == 0: + return + + await self._try_command( + "Turning the child lock of the miio device on failed.", + self._device.set_child_lock, True) + + async def async_set_child_lock_off(self): + """Turn the child lock off.""" + if self._device_features & FEATURE_SET_CHILD_LOCK == 0: + return + + await self._try_command( + "Turning the child lock of the miio device off failed.", + self._device.set_child_lock, False) + + +class XiaomiAirPurifier(XiaomiGenericDevice): + """Representation of a Xiaomi Air Purifier.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the plug switch.""" + super().__init__(name, device, model, unique_id) + + if self._model == MODEL_AIRPURIFIER_PRO: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO + self._speed_list = OPERATION_MODES_AIRPURIFIER_PRO + elif self._model == MODEL_AIRPURIFIER_V3: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_V3 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_V3 + self._speed_list = OPERATION_MODES_AIRPURIFIER_V3 + else: + self._device_features = FEATURE_FLAGS_AIRPURIFIER + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER + self._speed_list = OPERATION_MODES_AIRPURIFIER + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes}) + + async def async_update(self): """Fetch state from the device.""" from miio import DeviceException @@ -249,40 +559,24 @@ class XiaomiAirPurifier(FanEntity): return try: - state = yield from self.hass.async_add_job( - self._air_purifier.status) + state = await self.hass.async_add_job( + self._device.status) _LOGGER.debug("Got new state: %s", state) + self._available = True self._state = state.is_on - self._state_attrs = { - ATTR_TEMPERATURE: state.temperature, - ATTR_HUMIDITY: state.humidity, - ATTR_AIR_QUALITY_INDEX: state.aqi, - ATTR_MODE: state.mode.value, - ATTR_FILTER_HOURS_USED: state.filter_hours_used, - ATTR_FILTER_LIFE: state.filter_life_remaining, - ATTR_FAVORITE_LEVEL: state.favorite_level, - ATTR_BUZZER: state.buzzer, - ATTR_CHILD_LOCK: state.child_lock, - ATTR_LED: state.led, - ATTR_MOTOR_SPEED: state.motor_speed, - ATTR_AVERAGE_AIR_QUALITY_INDEX: state.average_aqi, - ATTR_PURIFY_VOLUME: state.purify_volume, - } - - if state.led_brightness: - self._state_attrs[ - ATTR_LED_BRIGHTNESS] = state.led_brightness.value + self._state_attrs.update( + {key: self._extract_value_from_attribute(state, value) for + key, value in self._available_attributes.items()}) except DeviceException as ex: - self._state = None + self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) @property - def speed_list(self: ToggleEntity) -> list: + def speed_list(self) -> list: """Get the list of available speeds.""" - from miio.airpurifier import OperationMode - return [mode.name for mode in OperationMode] + return self._speed_list @property def speed(self): @@ -294,70 +588,227 @@ class XiaomiAirPurifier(FanEntity): return None - @asyncio.coroutine - def async_set_speed(self: ToggleEntity, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - _LOGGER.debug("Setting the operation mode to: %s", speed) + if self.supported_features & SUPPORT_SET_SPEED == 0: + return + from miio.airpurifier import OperationMode - yield from self._try_command( - "Setting operation mode of the air purifier failed.", - self._air_purifier.set_mode, OperationMode[speed.title()]) + _LOGGER.debug("Setting the operation mode to: %s", speed) - @asyncio.coroutine - def async_set_buzzer_on(self): - """Turn the buzzer on.""" - yield from self._try_command( - "Turning the buzzer of the air purifier on failed.", - self._air_purifier.set_buzzer, True) + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, OperationMode[speed.title()]) - @asyncio.coroutine - def async_set_buzzer_off(self): - """Turn the buzzer off.""" - yield from self._try_command( - "Turning the buzzer of the air purifier off failed.", - self._air_purifier.set_buzzer, False) - - @asyncio.coroutine - def async_set_led_on(self): + async def async_set_led_on(self): """Turn the led on.""" - yield from self._try_command( - "Turning the led of the air purifier off failed.", - self._air_purifier.set_led, True) + if self._device_features & FEATURE_SET_LED == 0: + return - @asyncio.coroutine - def async_set_led_off(self): + await self._try_command( + "Turning the led of the miio device off failed.", + self._device.set_led, True) + + async def async_set_led_off(self): """Turn the led off.""" - yield from self._try_command( - "Turning the led of the air purifier off failed.", - self._air_purifier.set_led, False) + if self._device_features & FEATURE_SET_LED == 0: + return - @asyncio.coroutine - def async_set_child_lock_on(self): - """Turn the child lock on.""" - yield from self._try_command( - "Turning the child lock of the air purifier on failed.", - self._air_purifier.set_child_lock, True) + await self._try_command( + "Turning the led of the miio device off failed.", + self._device.set_led, False) - @asyncio.coroutine - def async_set_child_lock_off(self): - """Turn the child lock off.""" - yield from self._try_command( - "Turning the child lock of the air purifier off failed.", - self._air_purifier.set_child_lock, False) - - @asyncio.coroutine - def async_set_led_brightness(self, brightness: int = 2): + async def async_set_led_brightness(self, brightness: int = 2): """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + from miio.airpurifier import LedBrightness - yield from self._try_command( - "Setting the led brightness of the air purifier failed.", - self._air_purifier.set_led_brightness, LedBrightness(brightness)) + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, LedBrightness(brightness)) - @asyncio.coroutine - def async_set_favorite_level(self, level: int = 1): + async def async_set_favorite_level(self, level: int = 1): """Set the favorite level.""" - yield from self._try_command( - "Setting the favorite level of the air purifier failed.", - self._air_purifier.set_favorite_level, level) + if self._device_features & FEATURE_SET_FAVORITE_LEVEL == 0: + return + + await self._try_command( + "Setting the favorite level of the miio device failed.", + self._device.set_favorite_level, level) + + async def async_set_auto_detect_on(self): + """Turn the auto detect on.""" + if self._device_features & FEATURE_SET_AUTO_DETECT == 0: + return + + await self._try_command( + "Turning the auto detect of the miio device on failed.", + self._device.set_auto_detect, True) + + async def async_set_auto_detect_off(self): + """Turn the auto detect off.""" + if self._device_features & FEATURE_SET_AUTO_DETECT == 0: + return + + await self._try_command( + "Turning the auto detect of the miio device off failed.", + self._device.set_auto_detect, False) + + async def async_set_learn_mode_on(self): + """Turn the learn mode on.""" + if self._device_features & FEATURE_SET_LEARN_MODE == 0: + return + + await self._try_command( + "Turning the learn mode of the miio device on failed.", + self._device.set_learn_mode, True) + + async def async_set_learn_mode_off(self): + """Turn the learn mode off.""" + if self._device_features & FEATURE_SET_LEARN_MODE == 0: + return + + await self._try_command( + "Turning the learn mode of the miio device off failed.", + self._device.set_learn_mode, False) + + async def async_set_volume(self, volume: int = 50): + """Set the sound volume.""" + if self._device_features & FEATURE_SET_VOLUME == 0: + return + + await self._try_command( + "Setting the sound volume of the miio device failed.", + self._device.set_volume, volume) + + async def async_set_extra_features(self, features: int = 1): + """Set the extra features.""" + if self._device_features & FEATURE_SET_EXTRA_FEATURES == 0: + return + + await self._try_command( + "Setting the extra features of the miio device failed.", + self._device.set_extra_features, features) + + async def async_reset_filter(self): + """Reset the filter lifetime and usage.""" + if self._device_features & FEATURE_RESET_FILTER == 0: + return + + await self._try_command( + "Resetting the filter lifetime of the miio device failed.", + self._device.reset_filter) + + +class XiaomiAirHumidifier(XiaomiGenericDevice): + """Representation of a Xiaomi Air Humidifier.""" + + def __init__(self, name, device, model, unique_id): + """Initialize the plug switch.""" + from miio.airpurifier import OperationMode + + super().__init__(name, device, model, unique_id) + + if self._model == MODEL_AIRHUMIDIFIER_CA: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA + self._speed_list = [mode.name for mode in OperationMode] + else: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER + self._speed_list = [mode.name for mode in OperationMode if + mode.name != 'Auto'] + + self._state_attrs.update( + {attribute: None for attribute in self._available_attributes}) + + async def async_update(self): + """Fetch state from the device.""" + from miio import DeviceException + + # On state change the device doesn't provide the new state immediately. + if self._skip_update: + self._skip_update = False + return + + try: + state = await self.hass.async_add_job(self._device.status) + _LOGGER.debug("Got new state: %s", state) + + self._available = True + self._state = state.is_on + self._state_attrs.update( + {key: self._extract_value_from_attribute(state, value) for + key, value in self._available_attributes.items()}) + + except DeviceException as ex: + self._available = False + _LOGGER.error("Got exception while fetching the state: %s", ex) + + def speed_list(self) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def speed(self): + """Return the current speed.""" + if self._state: + from miio.airhumidifier import OperationMode + + return OperationMode(self._state_attrs[ATTR_MODE]).name + + return None + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self.supported_features & SUPPORT_SET_SPEED == 0: + return + + from miio.airhumidifier import OperationMode + + _LOGGER.debug("Setting the operation mode to: %s", speed) + + await self._try_command( + "Setting operation mode of the miio device failed.", + self._device.set_mode, OperationMode[speed.title()]) + + async def async_set_led_brightness(self, brightness: int = 2): + """Set the led brightness.""" + if self._device_features & FEATURE_SET_LED_BRIGHTNESS == 0: + return + + from miio.airhumidifier import LedBrightness + + await self._try_command( + "Setting the led brightness of the miio device failed.", + self._device.set_led_brightness, LedBrightness(brightness)) + + async def async_set_target_humidity(self, humidity: int = 40): + """Set the target humidity.""" + if self._device_features & FEATURE_SET_TARGET_HUMIDITY == 0: + return + + await self._try_command( + "Setting the target humidity of the miio device failed.", + self._device.set_target_humidity, humidity) + + async def async_set_dry_on(self): + """Turn the dry mode on.""" + if self._device_features & FEATURE_SET_DRY == 0: + return + + await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, True) + + async def async_set_dry_off(self): + """Turn the dry mode off.""" + if self._device_features & FEATURE_SET_DRY == 0: + return + + await self._try_command( + "Turning the dry mode of the miio device off failed.", + self._device.set_dry, False) From 7166d53e2b0516ce2263562a56de51eb0420b430 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 01:12:26 +0100 Subject: [PATCH 142/924] Log invalid templates in script delays (#13423) * Log invalid templates in script delays * Abort on error * Remove unused import --- homeassistant/helpers/script.py | 15 ++++++++++----- tests/helpers/test_script.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index abfdde8c8e1..f2ae36e7fd0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -97,11 +97,16 @@ class Script(): delay = action[CONF_DELAY] - if isinstance(delay, template.Template): - delay = vol.All( - cv.time_period, - cv.positive_timedelta)( - delay.async_render(variables)) + try: + if isinstance(delay, template.Template): + delay = vol.All( + cv.time_period, + cv.positive_timedelta)( + delay.async_render(variables)) + except (TemplateError, vol.Invalid) as ex: + _LOGGER.error("Error rendering '%s' delay template: %s", + self.name, ex) + break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a8ae20ad69b..4297ca26e7d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -218,6 +218,32 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 2 + def test_delay_invalid_template(self): + """Test the delay as a template that fails.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '{{ invalid_delay }}'}, + {'delay': {'seconds': 5}}, + {'event': event}])) + + with mock.patch.object(script, '_LOGGER') as mock_logger: + script_obj.run() + self.hass.block_till_done() + assert mock_logger.error.called + + assert not script_obj.is_running + assert len(events) == 1 + def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' From f3ccbda05435d1e0e4ae873b03077f7d47b00e09 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 25 Mar 2018 08:24:03 +0200 Subject: [PATCH 143/924] Bump songpal version, fixes lots of issues mentioned in #13022 (#13440) --- homeassistant/components/media_player/songpal.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index e43f5951db7..955456f2465 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-songpal==0.0.6'] +REQUIREMENTS = ['python-songpal==0.0.7'] SUPPORT_SONGPAL = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE | \ @@ -101,7 +101,7 @@ class SongpalDevice(MediaPlayerDevice): import songpal self._name = name self.endpoint = endpoint - self.dev = songpal.Protocol(self.endpoint) + self.dev = songpal.Device(self.endpoint) self._sysinfo = None self._state = False diff --git a/requirements_all.txt b/requirements_all.txt index fac18e23667..af3fd68ec64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -976,7 +976,7 @@ python-roku==3.1.5 python-sochain-api==0.0.2 # homeassistant.components.media_player.songpal -python-songpal==0.0.6 +python-songpal==0.0.7 # homeassistant.components.sensor.synologydsm python-synology==0.1.0 From 2d2e8034d68f5e932921c8bf22080f87e0afbf8c Mon Sep 17 00:00:00 2001 From: Marc Forth Date: Sun, 25 Mar 2018 08:45:25 +0100 Subject: [PATCH 144/924] Removed the google home warning from emulated_hue (#13436) * Removed the google home warning from emulated_hue * Update test_init.py * Update test_init.py --- .../components/emulated_hue/__init__.py | 4 ---- tests/components/emulated_hue/test_init.py | 16 +--------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 09ce1a57060..fa558cf299f 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -158,10 +158,6 @@ class Config(object): "Listen port not specified, defaulting to %s", self.listen_port) - if self.type == TYPE_GOOGLE and self.listen_port != 80: - _LOGGER.warning("When targeting Google Home, listening port has " - "to be port 80") - # Get whether or not UPNP binds to multicast address (239.255.255.250) # or to the unicast address (host_ip_addr) self.upnp_bind_multicast = conf.get( diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 06613f1336a..2f443eb5d6e 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -3,7 +3,7 @@ import json from unittest.mock import patch, Mock, mock_open -from homeassistant.components.emulated_hue import Config, _LOGGER +from homeassistant.components.emulated_hue import Config def test_config_google_home_entity_id_to_number(): @@ -112,17 +112,3 @@ def test_config_alexa_entity_id_to_number(): entity_id = conf.number_to_entity_id('light.test') assert entity_id == 'light.test' - - -def test_warning_config_google_home_listen_port(): - """Test we warn when non-default port is used for Google Home.""" - with patch.object(_LOGGER, 'warning') as mock_warn: - Config(None, { - 'type': 'google_home', - 'host_ip': '123.123.123.123', - 'listen_port': 8300 - }) - - assert mock_warn.called - assert mock_warn.mock_calls[0][1][0] == \ - "When targeting Google Home, listening port has to be port 80" From 3a765692e71f3a383c30e3e3bde54cee5b2bbd37 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 25 Mar 2018 00:46:47 -0700 Subject: [PATCH 145/924] Fixing odometer to display km (#13427) --- homeassistant/components/sensor/tesla.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sensor/tesla.py b/homeassistant/components/sensor/tesla.py index 1ffc97bb137..3233ebb1780 100644 --- a/homeassistant/components/sensor/tesla.py +++ b/homeassistant/components/sensor/tesla.py @@ -86,6 +86,8 @@ class TeslaSensor(TeslaDevice, Entity): self._unit = LENGTH_MILES else: self._unit = LENGTH_KILOMETERS + self.current_value /= 0.621371 + self.current_value = round(self.current_value, 2) else: self.current_value = self.tesla_device.get_value() if self.tesla_device.bin_type == 0x5: @@ -95,3 +97,5 @@ class TeslaSensor(TeslaDevice, Entity): self._unit = LENGTH_MILES else: self._unit = LENGTH_KILOMETERS + self.current_value /= 0.621371 + self.current_value = round(self.current_value, 2) From 594a5b7d29b1a08b3e246f13a17907d0c7746872 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 09:47:10 +0200 Subject: [PATCH 146/924] LimitlessLED hs_color fixes (#13425) --- homeassistant/components/light/limitlessled.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 5a6a0a34959..bb84b3a6fed 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -197,7 +197,7 @@ class LimitlessLEDGroup(Light): self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') self._temperature = last_state.attributes.get('color_temp') - self._color = last_state.attributes.get('rgb_color') + self._color = last_state.attributes.get('hs_color') @property def should_poll(self): @@ -336,4 +336,4 @@ class LimitlessLEDGroup(Light): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(color_hs_to_RGB(*tuple(self._color))) + return Color(*color_hs_to_RGB(*tuple(self._color))) From 55daea5169b19cb20f727444a523011bd3fc7f10 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 12:51:11 +0200 Subject: [PATCH 147/924] Improve detection of entity names in templates (#13432) * Improve detection of entity names in templates * Only test variables --- homeassistant/helpers/template.py | 5 +++-- tests/helpers/test_template.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3dd65aa362c..28ab4e9bfa0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,7 +13,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, MATCH_ALL, STATE_UNKNOWN) -from homeassistant.core import State +from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.loader import bind_hass, get_component @@ -73,7 +73,8 @@ def extract_entities(template, variables=None): extraction_final.append(result[0]) if variables and result[1] in variables and \ - isinstance(variables[result[1]], str): + isinstance(variables[result[1]], str) and \ + valid_entity_id(variables[result[1]]): extraction_final.append(variables[result[1]]) if extraction_final: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 47e46bae3c7..def06ea9284 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -836,6 +836,12 @@ is_state_attr('device_tracker.phone_2', 'battery', 40) "{{ is_state(trigger.entity_id, 'off') }}", {'trigger': {'entity_id': 'input_boolean.switch'}})) + self.assertEqual( + MATCH_ALL, + template.extract_entities( + "{{ is_state('media_player.' ~ where , 'playing') }}", + {'where': 'livingroom'})) + @asyncio.coroutine def test_state_with_unit(hass): From 7db37a38344c3ee57f16c97c402eaacfe8441417 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Mar 2018 12:53:15 +0200 Subject: [PATCH 148/924] HomeKit: Bugfix & improved logging (#13431) * Bugfix & improved logging * Removed logging statements * Removed logging test --- homeassistant/components/homekit/__init__.py | 4 ---- homeassistant/components/homekit/type_covers.py | 1 + homeassistant/components/homekit/type_lights.py | 4 ++++ .../components/homekit/type_security_systems.py | 1 + homeassistant/components/homekit/type_switches.py | 1 + homeassistant/components/homekit/type_thermostats.py | 4 ++++ tests/components/homekit/test_get_accessories.py | 11 ----------- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 02449607bf2..4854a828e41 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,8 +73,6 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('', - state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -129,8 +127,6 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) - _LOGGER.warning('The entity "%s" is not supported yet', - state.entity_id) return None diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 36cfa4d635a..7616ef05fdf 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -46,6 +46,7 @@ class WindowCovering(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" + self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index c723fcc08a6..2415bb1a4df 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -71,6 +71,7 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self._entity_id, value) self._flag[CHAR_ON] = True + self.char_on.set_value(value, should_callback=False) if value == 1: self._hass.components.light.turn_on(self._entity_id) @@ -81,6 +82,7 @@ class Light(HomeAccessory): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True + self.char_brightness.set_value(value, should_callback=False) self._hass.components.light.turn_on( self._entity_id, brightness_pct=value) @@ -88,6 +90,7 @@ class Light(HomeAccessory): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) self._flag[CHAR_SATURATION] = True + self.char_saturation.set_value(value, should_callback=False) self._saturation = value self.set_color() @@ -95,6 +98,7 @@ class Light(HomeAccessory): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) self._flag[CHAR_HUE] = True + self.char_hue.set_value(value, should_callback=False) self._hue = value self.set_color() diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 1d47160f9d2..146fca95b53 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -54,6 +54,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True + self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index fd3291ffe23..1f19893d0be 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,6 +36,7 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True + self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index b73b492ba74..3f545e90eb3 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -97,6 +97,7 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" + self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True @@ -109,6 +110,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', self._entity_id, value) self.coolingthresh_flag_target_state = True + self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, @@ -119,6 +121,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f', self._entity_id, value) self.heatingthresh_flag_target_state = True + self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value self._hass.components.climate.set_temperature( @@ -130,6 +133,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f', self._entity_id, value) self.temperature_flag_target_state = True + self.char_target_temp.set_value(value, should_callback=False) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e6dbe1ff729..ee7baae2755 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,17 +16,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} -def test_get_accessory_invalid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ - is None - assert caplog.records[1].levelname == 'WARNING' - - assert get_accessory(None, State('test.test', 'on'), None, None) \ - is None - assert caplog.records[3].levelname == 'WARNING' - - class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" From eaf81150eac9b10d43e89ca35dbb5972aae26526 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 25 Mar 2018 14:23:53 +0200 Subject: [PATCH 149/924] Upgrade keyring to 12.0.0 and keyrings.alt to 3.0 (#13452) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 64ad09bcd70..82a57c90263 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==11.0.0', 'keyrings.alt==2.3'] +REQUIREMENTS = ['keyring==12.0.0', 'keyrings.alt==3.0'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index af3fd68ec64..3eb4367d7de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,10 +430,10 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==11.0.0 +keyring==12.0.0 # homeassistant.scripts.keyring -keyrings.alt==2.3 +keyrings.alt==3.0 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From b99663296592f6c8c480b02bdd4d08d7088cc291 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 25 Mar 2018 14:25:00 +0200 Subject: [PATCH 150/924] Upgrade aiohttp to 3.1.0 (#13451) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e43e1f3dafe..317c1c8bc6c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.9 +aiohttp==3.1.0 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index 3eb4367d7de..79e371e0b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.0.9 +aiohttp==3.1.0 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index a317aeb18f1..9324713e71e 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.0.9', + 'aiohttp==3.1.0', 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', From 6d20a84f0e1e0c6114129f36ade817c8b28b0262 Mon Sep 17 00:00:00 2001 From: Patrick Hofmann Date: Sun, 25 Mar 2018 23:25:28 +0200 Subject: [PATCH 151/924] Security fix & lock for HomeMatic (#11980) * HomeMatic KeyMatic device become a real lock component * Adds supported features to lock component. Locks may are capable to open the door latch. If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property. * hound improvements. * Travis improvements. * Improvements from review process * Simplifies is_locked method * Adds an openable lock in the lock demo component * removes blank line * Adds test for openable demo lock and lint and reviewer improvements. * adds new line... * Comment end with a period. * Additional blank line. * Mock service based testing, lint fixes * Update description --- .../components/homematic/__init__.py | 9 ++- homeassistant/components/lock/__init__.py | 33 ++++++++++- homeassistant/components/lock/demo.py | 19 +++++- homeassistant/components/lock/homematic.py | 58 +++++++++++++++++++ tests/components/lock/test_demo.py | 12 +++- 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/lock/homematic.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1accf038575..c542cd9e88e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' +DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' @@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], + 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_SENSORS: [ 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', @@ -78,7 +79,8 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP'], - DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] + DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], + DISCOVER_LOCKS: ['KeyMatic'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -464,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args): ('cover', DISCOVER_COVER), ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), - ('climate', DISCOVER_CLIMATE)): + ('climate', DISCOVER_CLIMATE), + ('lock', DISCOVER_LOCKS)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index d03bbebd696..b3e4ac8f0ff 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) + STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) from homeassistant.components import group ATTR_CHANGED_BY = 'changed_by' @@ -39,6 +39,9 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_CODE): cv.string, }) +# Bitfield of features supported by the lock entity +SUPPORT_OPEN = 1 + _LOGGER = logging.getLogger(__name__) PROP_TO_ATTR = { @@ -78,6 +81,18 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" @@ -97,6 +112,8 @@ def async_setup(hass, config): for entity in target_locks: if service.service == SERVICE_LOCK: yield from entity.async_lock(code=code) + elif service.service == SERVICE_OPEN: + yield from entity.async_open(code=code) else: yield from entity.async_unlock(code=code) @@ -113,6 +130,9 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_LOCK, async_handle_lock_service, schema=LOCK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_OPEN, async_handle_lock_service, + schema=LOCK_SERVICE_SCHEMA) return True @@ -158,6 +178,17 @@ class LockDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) + def open(self, **kwargs): + """Open the door latch.""" + raise NotImplementedError() + + def async_open(self, **kwargs): + """Open the door latch. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(ft.partial(self.open, **kwargs)) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index aca25e7e16d..d561dd333ab 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -4,7 +4,7 @@ Demo lock platform that has two fake locks. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) @@ -13,17 +13,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo lock platform.""" add_devices([ DemoLock('Front Door', STATE_LOCKED), - DemoLock('Kitchen Door', STATE_UNLOCKED) + DemoLock('Kitchen Door', STATE_UNLOCKED), + DemoLock('Openable Lock', STATE_LOCKED, True) ]) class DemoLock(LockDevice): """Representation of a Demo lock.""" - def __init__(self, name, state): + def __init__(self, name, state, openable=False): """Initialize the lock.""" self._name = name self._state = state + self._openable = openable @property def should_poll(self): @@ -49,3 +51,14 @@ class DemoLock(LockDevice): """Unlock the device.""" self._state = STATE_UNLOCKED self.schedule_update_ha_state() + + def open(self, **kwargs): + """Open the door latch.""" + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + if self._openable: + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/homematic.py b/homeassistant/components/lock/homematic.py new file mode 100644 index 00000000000..0d70849e37e --- /dev/null +++ b/homeassistant/components/lock/homematic.py @@ -0,0 +1,58 @@ +""" +Support for Homematic lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.homematic/ +""" +import logging +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES +from homeassistant.const import STATE_UNKNOWN + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Homematic lock platform.""" + if discovery_info is None: + return + + devices = [] + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + devices.append(HMLock(conf)) + + add_devices(devices) + + +class HMLock(HMDevice, LockDevice): + """Representation of a Homematic lock aka KeyMatic.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return not bool(self._hm_get_state()) + + def lock(self, **kwargs): + """Lock the lock.""" + self._hmdevice.lock() + + def unlock(self, **kwargs): + """Unlock the lock.""" + self._hmdevice.unlock() + + def open(self, **kwargs): + """Open the door latch.""" + self._hmdevice.open() + + def _init_data_struct(self): + """Generate the data dictionary (self._data) from metadata.""" + self._state = "STATE" + self._data.update({self._state: STATE_UNKNOWN}) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 12007d2b8ad..1d774248f35 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -4,11 +4,10 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import lock -from tests.common import get_test_home_assistant - - +from tests.common import get_test_home_assistant, mock_service FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' +OPENABLE_LOCK = 'lock.openable_lock' class TestLockDemo(unittest.TestCase): @@ -48,3 +47,10 @@ class TestLockDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) + + def test_opening(self): + """Test the opening of a lock.""" + calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) + lock.open_lock(self.hass, OPENABLE_LOCK) + self.hass.block_till_done() + self.assertEqual(1, len(calls)) From 3e3f710b1269c4045095c899665f6002f3710695 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 25 Mar 2018 23:32:13 +0200 Subject: [PATCH 152/924] Qwikswitch async & updates (#12641) --- homeassistant/components/light/qwikswitch.py | 11 +- homeassistant/components/qwikswitch.py | 150 +++++++++--------- homeassistant/components/switch/qwikswitch.py | 11 +- requirements_all.txt | 2 +- 4 files changed, 87 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index 63051d2ea8c..c4faf0f9ca0 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -6,19 +6,16 @@ https://home-assistant.io/components/light.qwikswitch/ """ import logging -import homeassistant.components.qwikswitch as qwikswitch - -_LOGGER = logging.getLogger(__name__) - DEPENDENCIES = ['qwikswitch'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the lights from the main Qwikswitch component.""" + """Add lights from the main Qwikswitch component.""" if discovery_info is None: - _LOGGER.error("Configure Qwikswitch component failed") + logging.getLogger(__name__).error( + "Configure Qwikswitch Light component failed") return False - add_devices(qwikswitch.QSUSB['light']) + add_devices(hass.data['qwikswitch']['light']) return True diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 4d5f27082de..c4901805e3e 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -4,18 +4,21 @@ Support for Qwikswitch devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ +import asyncio import logging import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL) +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import load_platform from homeassistant.components.light import ( ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) from homeassistant.components.switch import SwitchDevice -REQUIREMENTS = ['pyqwikswitch==0.4'] +REQUIREMENTS = ['pyqwikswitch==0.5'] _LOGGER = logging.getLogger(__name__) @@ -33,10 +36,6 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str) })}, extra=vol.ALLOW_EXTRA) -QSUSB = {} - -SUPPORT_QWIKSWITCH = SUPPORT_BRIGHTNESS - class QSToggleEntity(object): """Representation of a Qwikswitch Entity. @@ -53,22 +52,15 @@ class QSToggleEntity(object): [3] /components/switch/__init__.py """ - def __init__(self, qsitem, qsusb): + def __init__(self, qsid, qsusb): """Initialize the ToggleEntity.""" - from pyqwikswitch import (QS_ID, QS_NAME, QSType, PQS_VALUE, PQS_TYPE) - self._id = qsitem[QS_ID] - self._name = qsitem[QS_NAME] - self._value = qsitem[PQS_VALUE] - self._qsusb = qsusb - self._dim = qsitem[PQS_TYPE] == QSType.dimmer - QSUSB[self._id] = self + from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) + self._id = qsid + self._qsusb = qsusb.devices + dev = qsusb.devices[qsid] + self._dim = dev[QS_TYPE] == QSType.dimmer + self._name = dev[QSDATA][QS_NAME] - @property - def brightness(self): - """Return the brightness of this light between 0..100.""" - return self._value if self._dim else None - - # pylint: disable=no-self-use @property def should_poll(self): """No polling needed.""" @@ -82,29 +74,27 @@ class QSToggleEntity(object): @property def is_on(self): """Check if device is on (non-zero).""" - return self._value > 0 - - def update_value(self, value): - """Decode the QSUSB value and update the Home assistant state.""" - if value != self._value: - self._value = value - # pylint: disable=no-member - super().schedule_update_ha_state() # Part of Entity/ToggleEntity - return self._value + return self._qsusb[self._id, 1] > 0 def turn_on(self, **kwargs): """Turn the device on.""" - newvalue = 255 - if ATTR_BRIGHTNESS in kwargs: - newvalue = kwargs[ATTR_BRIGHTNESS] - if self._qsusb.set(self._id, round(min(newvalue, 255)/2.55)) >= 0: - self.update_value(newvalue) + new = kwargs.get(ATTR_BRIGHTNESS, 255) + self._qsusb.set_value(self._id, new) - # pylint: disable=unused-argument - def turn_off(self, **kwargs): + @asyncio.coroutine + def async_turn_on(self, **kwargs): + """Turn the device on.""" + new = kwargs.get(ATTR_BRIGHTNESS, 255) + self._qsusb.set_value(self._id, new) + + def turn_off(self, **kwargs): # pylint: disable=unused-argument """Turn the device off.""" - if self._qsusb.set(self._id, 0) >= 0: - self.update_value(0) + self._qsusb.set_value(self._id, 0) + + @asyncio.coroutine + def async_turn_off(self, **kwargs): # pylint: disable=unused-argument + """Turn the device off.""" + self._qsusb.set_value(self._id, 0) class QSSwitch(QSToggleEntity, SwitchDevice): @@ -116,17 +106,25 @@ class QSSwitch(QSToggleEntity, SwitchDevice): class QSLight(QSToggleEntity, Light): """Light based on a Qwikswitch relay/dimmer module.""" + @property + def brightness(self): + """Return the brightness of this light (0-255).""" + return self._qsusb[self._id, 1] if self._dim else None + @property def supported_features(self): """Flag supported features.""" - return SUPPORT_QWIKSWITCH + return SUPPORT_BRIGHTNESS if self._dim else None -def setup(hass, config): - """Set up the QSUSB component.""" +@asyncio.coroutine +def async_setup(hass, config): + """Setup qwiskswitch component.""" + from pyqwikswitch.async import QSUsb from pyqwikswitch import ( - QSUsb, CMD_BUTTONS, QS_NAME, QS_ID, QS_CMD, PQS_VALUE, PQS_TYPE, - QSType) + CMD_BUTTONS, QS_CMD, QSDATA, QS_ID, QS_NAME, QS_TYPE, QSType) + + hass.data[DOMAIN] = {} # Override which cmd's in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] @@ -136,61 +134,69 @@ def setup(hass, config): url = config[DOMAIN][CONF_URL] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] - qsusb = QSUsb(url, _LOGGER, dimmer_adjust) + def callback_value_changed(qsdevices, key, new): \ + # pylint: disable=unused-argument + """Update entiry values based on device change.""" + entity = hass.data[DOMAIN].get(key) + if entity is not None: + entity.schedule_update_ha_state() # Part of Entity/ToggleEntity - def _stop(event): + session = async_get_clientsession(hass) + qsusb = QSUsb(url=url, dim_adj=dimmer_adjust, session=session, + callback_value_changed=callback_value_changed) + + @callback + def async_stop(event): # pylint: disable=unused-argument """Stop the listener queue and clean up.""" nonlocal qsusb qsusb.stop() qsusb = None - global QSUSB - QSUSB = {} + hass.data[DOMAIN] = {} _LOGGER.info("Waiting for long poll to QSUSB to time out") - hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) # Discover all devices in QSUSB - devices = qsusb.devices() - QSUSB['switch'] = [] - QSUSB['light'] = [] - for item in devices: - if item[PQS_TYPE] == QSType.relay and (item[QS_NAME].lower() - .endswith(' switch')): - item[QS_NAME] = item[QS_NAME][:-7] # Remove ' switch' postfix - QSUSB['switch'].append(QSSwitch(item, qsusb)) - elif item[PQS_TYPE] in [QSType.relay, QSType.dimmer]: - QSUSB['light'].append(QSLight(item, qsusb)) + yield from qsusb.update_from_devices() + hass.data[DOMAIN]['switch'] = [] + hass.data[DOMAIN]['light'] = [] + for _id, item in qsusb.devices: + if (item[QS_TYPE] == QSType.relay and + item[QSDATA][QS_NAME].lower().endswith(' switch')): + item[QSDATA][QS_NAME] = item[QSDATA][QS_NAME][:-7] # Remove switch + new_dev = QSSwitch(_id, qsusb) + hass.data[DOMAIN]['switch'].append(new_dev) + elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: + new_dev = QSLight(_id, qsusb) + hass.data[DOMAIN]['light'].append(new_dev) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", item) + continue + hass.data[DOMAIN][_id] = new_dev # Load platforms for comp_name in ('switch', 'light'): - if QSUSB[comp_name]: + if hass.data[DOMAIN][comp_name]: load_platform(hass, comp_name, 'qwikswitch', {}, config) - def qs_callback(item): + def callback_qs_listen(item): """Typically a button press or update signal.""" if qsusb is None: # Shutting down - _LOGGER.info("Button press or updating signal done") return # If button pressed, fire a hass event - if item.get(QS_CMD, '') in cmd_buttons: - hass.bus.fire('qwikswitch.button.' + item.get(QS_ID, '@no_id')) + if item.get(QS_CMD, '') in cmd_buttons and QS_ID in item: + hass.bus.async_fire('qwikswitch.button.{}'.format(item[QS_ID])) return # Update all ha_objects - qsreply = qsusb.devices() - if qsreply is False: - return - for itm in qsreply: - if itm[QS_ID] in QSUSB: - QSUSB[itm[QS_ID]].update_value( - round(min(itm[PQS_VALUE], 100) * 2.55)) + hass.async_add_job(qsusb.update_from_devices) - def _start(event): + @callback + def async_start(event): # pylint: disable=unused-argument """Start listening.""" - qsusb.listen(callback=qs_callback, timeout=30) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start) + hass.async_add_job(qsusb.listen, callback_qs_listen) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) return True diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index 7aea1dea1e1..258e1141052 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -6,19 +6,16 @@ https://home-assistant.io/components/switch.qwikswitch/ """ import logging -import homeassistant.components.qwikswitch as qwikswitch - -_LOGGER = logging.getLogger(__name__) - DEPENDENCIES = ['qwikswitch'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Add switched from the main Qwikswitch component.""" + """Add switches from the main Qwikswitch component.""" if discovery_info is None: - _LOGGER.error("Configure Qwikswitch component") + logging.getLogger(__name__).error( + "Configure Qwikswitch Switch component failed") return False - add_devices(qwikswitch.QSUSB['switch']) + add_devices(hass.data['qwikswitch']['switch']) return True diff --git a/requirements_all.txt b/requirements_all.txt index 79e371e0b48..e51bbc98823 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -860,7 +860,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.4 +pyqwikswitch==0.5 # homeassistant.components.rainbird pyrainbird==0.1.3 From 8ab5978db3b6d241546bd6f2d87c85a88ebec48c Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Mon, 26 Mar 2018 03:02:21 +0200 Subject: [PATCH 153/924] Fix encoding errors in mikrotik device tracker (#13464) --- homeassistant/components/device_tracker/mikrotik.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 1d9161c0d45..154fc3d2a63 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner): self.host, self.username, self.password, - port=int(self.port) + port=int(self.port), + encoding='utf-8' ) try: From a5ae77ab93811948c95e579667719971ec47ad3b Mon Sep 17 00:00:00 2001 From: Cedric Van Goethem Date: Mon, 26 Mar 2018 02:03:23 +0100 Subject: [PATCH 154/924] Add extra check for ESSID field in case there's a wired connection (#13459) --- homeassistant/components/device_tracker/unifi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 8663930c4e6..d8a52aaaeb4 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner): # Filter clients to provided SSID list if self._ssid_filter: clients = [client for client in clients - if client['essid'] in self._ssid_filter] + if 'essid' in client and + client['essid'] in self._ssid_filter] self._clients = { client['mac']: client From d6af26b589a504317c8b9cfbfc3fac24e8775087 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:04:20 -0700 Subject: [PATCH 155/924] Add version bump script (#13447) * Add version bump script * Lint --- script/version_bump.py | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100755 script/version_bump.py diff --git a/script/version_bump.py b/script/version_bump.py new file mode 100755 index 00000000000..0cd02ddbfcb --- /dev/null +++ b/script/version_bump.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Helper script to bump the current version.""" +import argparse +import re + +from homeassistant import const + + +PARSE_PATCH = r'(?P\d+)(\.(?P\D+)(?P\d+))?' + + +def format_patch(patch_parts): + """Format the patch parts back into a patch string.""" + return '{patch}.{prerel}{prerelversion}'.format(**patch_parts) + + +def bump_version(cur_major, cur_minor, cur_patch, bump_type): + """Return a new version given a current version and action.""" + patch_parts = re.match(PARSE_PATCH, cur_patch).groupdict() + patch_parts['patch'] = int(patch_parts['patch']) + if patch_parts['prerelversion'] is not None: + patch_parts['prerelversion'] = int(patch_parts['prerelversion']) + + if bump_type == 'release_patch': + # Convert 0.67.3 to 0.67.4 + # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.dev0 to 0.67.3 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + new_patch = str(patch_parts['patch'] + 1) + else: + new_patch = str(patch_parts['patch']) + + elif bump_type == 'dev': + # Convert 0.67.3 to 0.67.4.dev0 + # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.dev0 to 0.67.3.dev1 + new_major = cur_major + + if patch_parts['prerel'] == 'dev': + new_minor = cur_minor + patch_parts['prerelversion'] += 1 + new_patch = format_patch(patch_parts) + else: + new_minor = cur_minor + 1 + new_patch = '0.dev0' + + elif bump_type == 'beta': + # Convert 0.67.5 to 0.67.8.beta0 + # Convert 0.67.0.dev0 to 0.67.0.beta0 + # Convert 0.67.5.beta4 to 0.67.5.beta5 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + patch_parts['patch'] += 1 + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + elif patch_parts['prerel'] == 'beta': + patch_parts['prerelversion'] += 1 + + elif patch_parts['prerel'] == 'dev': + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + else: + raise Exception('Can only bump from beta or no prerel version') + + new_patch = format_patch(patch_parts) + + return new_major, new_minor, new_patch + + +def write_version(major, minor, patch): + """Update Home Assistant constant file with new version.""" + with open('homeassistant/const.py') as fil: + content = fil.read() + + content = re.sub('MAJOR_VERSION = .*\n', + 'MAJOR_VERSION = {}\n'.format(major), + content) + content = re.sub('MINOR_VERSION = .*\n', + 'MINOR_VERSION = {}\n'.format(minor), + content) + content = re.sub('PATCH_VERSION = .*\n', + "PATCH_VERSION = '{}'\n".format(patch), + content) + + with open('homeassistant/const.py', 'wt') as fil: + content = fil.write(content) + + +def main(): + """Execute script.""" + parser = argparse.ArgumentParser( + description="Bump version of Home Assistant") + parser.add_argument( + 'type', + help="The type of the bump the version to.", + choices=['beta', 'dev', 'release_patch'], + ) + arguments = parser.parse_args() + write_version(*bump_version(const.MAJOR_VERSION, const.MINOR_VERSION, + const.PATCH_VERSION, arguments.type)) + + +def test_bump_version(): + """Make sure it all works.""" + assert bump_version(0, 56, '0', 'beta') == \ + (0, 56, '1.beta0') + assert bump_version(0, 56, '0.beta3', 'beta') == \ + (0, 56, '0.beta4') + assert bump_version(0, 56, '0.dev0', 'beta') == \ + (0, 56, '0.beta0') + + assert bump_version(0, 56, '3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.beta3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.dev0', 'dev') == \ + (0, 56, '0.dev1') + + assert bump_version(0, 56, '3', 'release_patch') == \ + (0, 56, '4') + assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + (0, 56, '3') + assert bump_version(0, 56, '0.dev0', 'release_patch') == \ + (0, 56, '0') + + +if __name__ == '__main__': + main() From 1887bac37e6858796c7683ae207457baf49ffeab Mon Sep 17 00:00:00 2001 From: a-andre Date: Mon, 26 Mar 2018 03:07:26 +0200 Subject: [PATCH 156/924] Hyperion: fix typo (#13429) --- homeassistant/components/light/hyperion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index e5a4bd18115..8ba2329af7e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -215,7 +215,7 @@ class Hyperion(Light): pass led_color = response['info']['activeLedColor'] - if not led_color or led_color[0]['RGB value'] == [0, 0, 0]: + if not led_color or led_color[0]['RGB Value'] == [0, 0, 0]: # Get the active effect if response['info'].get('activeEffects'): self._rgb_color = [175, 0, 255] @@ -234,8 +234,7 @@ class Hyperion(Light): self._effect = None else: # Get the RGB color - self._rgb_color =\ - response['info']['activeLedColor'][0]['RGB Value'] + self._rgb_color = led_color[0]['RGB Value'] self._brightness = max(self._rgb_color) self._rgb_mem = [int(round(float(x)*255/self._brightness)) for x in self._rgb_color] From 8a204fd15bc9217974b6656af93c1eceab8ab499 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:10:59 -0700 Subject: [PATCH 157/924] Bump frontend to 20180326.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 9107e64a040..dad07c87cb6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180322.0'] +REQUIREMENTS = ['home-assistant-frontend==20180326.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 52833969872..017449bfeca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180322.0 +home-assistant-frontend==20180326.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2c1df2d3bf..fda514af007 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180322.0 +home-assistant-frontend==20180326.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 0d48a8eec68bcb226df3e8abfafafa40e04ea786 Mon Sep 17 00:00:00 2001 From: Patrick Hofmann Date: Sun, 25 Mar 2018 23:25:28 +0200 Subject: [PATCH 158/924] Security fix & lock for HomeMatic (#11980) * HomeMatic KeyMatic device become a real lock component * Adds supported features to lock component. Locks may are capable to open the door latch. If component is support it, the SUPPORT_OPENING bitmask can be supplied in the supported_features property. * hound improvements. * Travis improvements. * Improvements from review process * Simplifies is_locked method * Adds an openable lock in the lock demo component * removes blank line * Adds test for openable demo lock and lint and reviewer improvements. * adds new line... * Comment end with a period. * Additional blank line. * Mock service based testing, lint fixes * Update description --- .../components/homematic/__init__.py | 9 ++- homeassistant/components/lock/__init__.py | 33 ++++++++++- homeassistant/components/lock/demo.py | 19 +++++- homeassistant/components/lock/homematic.py | 58 +++++++++++++++++++ tests/components/lock/test_demo.py | 12 +++- 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/lock/homematic.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1accf038575..c542cd9e88e 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -33,6 +33,7 @@ DISCOVER_SENSORS = 'homematic.sensor' DISCOVER_BINARY_SENSORS = 'homematic.binary_sensor' DISCOVER_COVER = 'homematic.cover' DISCOVER_CLIMATE = 'homematic.climate' +DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' @@ -59,7 +60,7 @@ SERVICE_SET_INSTALL_MODE = 'set_install_mode' HM_DEVICE_TYPES = { DISCOVER_SWITCHES: [ 'Switch', 'SwitchPowermeter', 'IOSwitch', 'IPSwitch', 'RFSiren', - 'IPSwitchPowermeter', 'KeyMatic', 'HMWIOSwitch', 'Rain', 'EcoLogic'], + 'IPSwitchPowermeter', 'HMWIOSwitch', 'Rain', 'EcoLogic'], DISCOVER_LIGHTS: ['Dimmer', 'KeyDimmer', 'IPKeyDimmer'], DISCOVER_SENSORS: [ 'SwitchPowermeter', 'Motion', 'MotionV2', 'RemoteMotion', 'MotionIP', @@ -78,7 +79,8 @@ HM_DEVICE_TYPES = { 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', 'WiredSensor', 'PresenceIP'], - DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'] + DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], + DISCOVER_LOCKS: ['KeyMatic'] } HM_IGNORE_DISCOVERY_NODE = [ @@ -464,7 +466,8 @@ def _system_callback_handler(hass, config, src, *args): ('cover', DISCOVER_COVER), ('binary_sensor', DISCOVER_BINARY_SENSORS), ('sensor', DISCOVER_SENSORS), - ('climate', DISCOVER_CLIMATE)): + ('climate', DISCOVER_CLIMATE), + ('lock', DISCOVER_LOCKS)): # Get all devices of a specific type found_devices = _get_devices( hass, discovery_type, addresses, interface) diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index d03bbebd696..b3e4ac8f0ff 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, - STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK) + STATE_UNKNOWN, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) from homeassistant.components import group ATTR_CHANGED_BY = 'changed_by' @@ -39,6 +39,9 @@ LOCK_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_CODE): cv.string, }) +# Bitfield of features supported by the lock entity +SUPPORT_OPEN = 1 + _LOGGER = logging.getLogger(__name__) PROP_TO_ATTR = { @@ -78,6 +81,18 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) +@bind_hass +def open_lock(hass, entity_id=None, code=None): + """Open all or specified locks.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_OPEN, data) + + @asyncio.coroutine def async_setup(hass, config): """Track states and offer events for locks.""" @@ -97,6 +112,8 @@ def async_setup(hass, config): for entity in target_locks: if service.service == SERVICE_LOCK: yield from entity.async_lock(code=code) + elif service.service == SERVICE_OPEN: + yield from entity.async_open(code=code) else: yield from entity.async_unlock(code=code) @@ -113,6 +130,9 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_LOCK, async_handle_lock_service, schema=LOCK_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_OPEN, async_handle_lock_service, + schema=LOCK_SERVICE_SCHEMA) return True @@ -158,6 +178,17 @@ class LockDevice(Entity): """ return self.hass.async_add_job(ft.partial(self.unlock, **kwargs)) + def open(self, **kwargs): + """Open the door latch.""" + raise NotImplementedError() + + def async_open(self, **kwargs): + """Open the door latch. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(ft.partial(self.open, **kwargs)) + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lock/demo.py b/homeassistant/components/lock/demo.py index aca25e7e16d..d561dd333ab 100644 --- a/homeassistant/components/lock/demo.py +++ b/homeassistant/components/lock/demo.py @@ -4,7 +4,7 @@ Demo lock platform that has two fake locks. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.components.lock import LockDevice +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) @@ -13,17 +13,19 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo lock platform.""" add_devices([ DemoLock('Front Door', STATE_LOCKED), - DemoLock('Kitchen Door', STATE_UNLOCKED) + DemoLock('Kitchen Door', STATE_UNLOCKED), + DemoLock('Openable Lock', STATE_LOCKED, True) ]) class DemoLock(LockDevice): """Representation of a Demo lock.""" - def __init__(self, name, state): + def __init__(self, name, state, openable=False): """Initialize the lock.""" self._name = name self._state = state + self._openable = openable @property def should_poll(self): @@ -49,3 +51,14 @@ class DemoLock(LockDevice): """Unlock the device.""" self._state = STATE_UNLOCKED self.schedule_update_ha_state() + + def open(self, **kwargs): + """Open the door latch.""" + self._state = STATE_UNLOCKED + self.schedule_update_ha_state() + + @property + def supported_features(self): + """Flag supported features.""" + if self._openable: + return SUPPORT_OPEN diff --git a/homeassistant/components/lock/homematic.py b/homeassistant/components/lock/homematic.py new file mode 100644 index 00000000000..0d70849e37e --- /dev/null +++ b/homeassistant/components/lock/homematic.py @@ -0,0 +1,58 @@ +""" +Support for Homematic lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.homematic/ +""" +import logging +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from homeassistant.components.homematic import HMDevice, ATTR_DISCOVER_DEVICES +from homeassistant.const import STATE_UNKNOWN + + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['homematic'] + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Homematic lock platform.""" + if discovery_info is None: + return + + devices = [] + for conf in discovery_info[ATTR_DISCOVER_DEVICES]: + devices.append(HMLock(conf)) + + add_devices(devices) + + +class HMLock(HMDevice, LockDevice): + """Representation of a Homematic lock aka KeyMatic.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return not bool(self._hm_get_state()) + + def lock(self, **kwargs): + """Lock the lock.""" + self._hmdevice.lock() + + def unlock(self, **kwargs): + """Unlock the lock.""" + self._hmdevice.unlock() + + def open(self, **kwargs): + """Open the door latch.""" + self._hmdevice.open() + + def _init_data_struct(self): + """Generate the data dictionary (self._data) from metadata.""" + self._state = "STATE" + self._data.update({self._state: STATE_UNKNOWN}) + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN diff --git a/tests/components/lock/test_demo.py b/tests/components/lock/test_demo.py index 12007d2b8ad..1d774248f35 100644 --- a/tests/components/lock/test_demo.py +++ b/tests/components/lock/test_demo.py @@ -4,11 +4,10 @@ import unittest from homeassistant.setup import setup_component from homeassistant.components import lock -from tests.common import get_test_home_assistant - - +from tests.common import get_test_home_assistant, mock_service FRONT = 'lock.front_door' KITCHEN = 'lock.kitchen_door' +OPENABLE_LOCK = 'lock.openable_lock' class TestLockDemo(unittest.TestCase): @@ -48,3 +47,10 @@ class TestLockDemo(unittest.TestCase): self.hass.block_till_done() self.assertFalse(lock.is_locked(self.hass, FRONT)) + + def test_opening(self): + """Test the opening of a lock.""" + calls = mock_service(self.hass, lock.DOMAIN, lock.SERVICE_OPEN) + lock.open_lock(self.hass, OPENABLE_LOCK) + self.hass.block_till_done() + self.assertEqual(1, len(calls)) From a08293cff7facd2a204f5fd8e84aff0db9d43cd4 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 01:12:26 +0100 Subject: [PATCH 159/924] Log invalid templates in script delays (#13423) * Log invalid templates in script delays * Abort on error * Remove unused import --- homeassistant/helpers/script.py | 15 ++++++++++----- tests/helpers/test_script.py | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index abfdde8c8e1..f2ae36e7fd0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -97,11 +97,16 @@ class Script(): delay = action[CONF_DELAY] - if isinstance(delay, template.Template): - delay = vol.All( - cv.time_period, - cv.positive_timedelta)( - delay.async_render(variables)) + try: + if isinstance(delay, template.Template): + delay = vol.All( + cv.time_period, + cv.positive_timedelta)( + delay.async_render(variables)) + except (TemplateError, vol.Invalid) as ex: + _LOGGER.error("Error rendering '%s' delay template: %s", + self.name, ex) + break unsub = async_track_point_in_utc_time( self.hass, async_script_delay, diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a8ae20ad69b..4297ca26e7d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -218,6 +218,32 @@ class TestScriptHelper(unittest.TestCase): assert not script_obj.is_running assert len(events) == 2 + def test_delay_invalid_template(self): + """Test the delay as a template that fails.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(event, record_event) + + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '{{ invalid_delay }}'}, + {'delay': {'seconds': 5}}, + {'event': event}])) + + with mock.patch.object(script, '_LOGGER') as mock_logger: + script_obj.run() + self.hass.block_till_done() + assert mock_logger.error.called + + assert not script_obj.is_running + assert len(events) == 1 + def test_cancel_while_delay(self): """Test the cancelling while the delay is present.""" event = 'test_event' From 444805df103619f9aa52591a11ff1d4fb6e224b0 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 09:47:10 +0200 Subject: [PATCH 160/924] LimitlessLED hs_color fixes (#13425) --- homeassistant/components/light/limitlessled.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index 5a6a0a34959..bb84b3a6fed 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -197,7 +197,7 @@ class LimitlessLEDGroup(Light): self._is_on = (last_state.state == STATE_ON) self._brightness = last_state.attributes.get('brightness') self._temperature = last_state.attributes.get('color_temp') - self._color = last_state.attributes.get('rgb_color') + self._color = last_state.attributes.get('hs_color') @property def should_poll(self): @@ -336,4 +336,4 @@ class LimitlessLEDGroup(Light): def limitlessled_color(self): """Convert Home Assistant HS list to RGB Color tuple.""" from limitlessled import Color - return Color(color_hs_to_RGB(*tuple(self._color))) + return Color(*color_hs_to_RGB(*tuple(self._color))) From 24d299e266833a2d22369500bb41d91c058f333d Mon Sep 17 00:00:00 2001 From: a-andre Date: Mon, 26 Mar 2018 03:07:26 +0200 Subject: [PATCH 161/924] Hyperion: fix typo (#13429) --- homeassistant/components/light/hyperion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/hyperion.py b/homeassistant/components/light/hyperion.py index e5a4bd18115..8ba2329af7e 100644 --- a/homeassistant/components/light/hyperion.py +++ b/homeassistant/components/light/hyperion.py @@ -215,7 +215,7 @@ class Hyperion(Light): pass led_color = response['info']['activeLedColor'] - if not led_color or led_color[0]['RGB value'] == [0, 0, 0]: + if not led_color or led_color[0]['RGB Value'] == [0, 0, 0]: # Get the active effect if response['info'].get('activeEffects'): self._rgb_color = [175, 0, 255] @@ -234,8 +234,7 @@ class Hyperion(Light): self._effect = None else: # Get the RGB color - self._rgb_color =\ - response['info']['activeLedColor'][0]['RGB Value'] + self._rgb_color = led_color[0]['RGB Value'] self._brightness = max(self._rgb_color) self._rgb_mem = [int(round(float(x)*255/self._brightness)) for x in self._rgb_color] From 60f6109cbfdfb180a853b8dd6cadc6c7503d8c9e Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 25 Mar 2018 12:53:15 +0200 Subject: [PATCH 162/924] HomeKit: Bugfix & improved logging (#13431) * Bugfix & improved logging * Removed logging statements * Removed logging test --- homeassistant/components/homekit/__init__.py | 4 ---- homeassistant/components/homekit/type_covers.py | 1 + homeassistant/components/homekit/type_lights.py | 4 ++++ .../components/homekit/type_security_systems.py | 1 + homeassistant/components/homekit/type_switches.py | 1 + homeassistant/components/homekit/type_thermostats.py | 4 ++++ tests/components/homekit/test_get_accessories.py | 11 ----------- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 02449607bf2..4854a828e41 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -73,8 +73,6 @@ async def async_setup(hass, config): def get_accessory(hass, state, aid, config): """Take state and return an accessory object if supported.""" - _LOGGER.debug('', - state.entity_id, aid, config) if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' 'generates an invalid aid, please change it.', @@ -129,8 +127,6 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) - _LOGGER.warning('The entity "%s" is not supported yet', - state.entity_id) return None diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 36cfa4d635a..7616ef05fdf 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -46,6 +46,7 @@ class WindowCovering(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" + self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: _LOGGER.debug('%s: Set position to %d', self._entity_id, value) self.homekit_target = value diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index c723fcc08a6..2415bb1a4df 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -71,6 +71,7 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self._entity_id, value) self._flag[CHAR_ON] = True + self.char_on.set_value(value, should_callback=False) if value == 1: self._hass.components.light.turn_on(self._entity_id) @@ -81,6 +82,7 @@ class Light(HomeAccessory): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True + self.char_brightness.set_value(value, should_callback=False) self._hass.components.light.turn_on( self._entity_id, brightness_pct=value) @@ -88,6 +90,7 @@ class Light(HomeAccessory): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) self._flag[CHAR_SATURATION] = True + self.char_saturation.set_value(value, should_callback=False) self._saturation = value self.set_color() @@ -95,6 +98,7 @@ class Light(HomeAccessory): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) self._flag[CHAR_HUE] = True + self.char_hue.set_value(value, should_callback=False) self._hue = value self.set_color() diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 1d47160f9d2..146fca95b53 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -54,6 +54,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Set security state to %d', self._entity_id, value) self.flag_target_state = True + self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index fd3291ffe23..1f19893d0be 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,6 +36,7 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self._entity_id, value) self.flag_target_state = True + self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self._hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self._entity_id}) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index b73b492ba74..3f545e90eb3 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -97,6 +97,7 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" + self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) self.heat_cool_flag_target_state = True @@ -109,6 +110,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', self._entity_id, value) self.coolingthresh_flag_target_state = True + self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, @@ -119,6 +121,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f', self._entity_id, value) self.heatingthresh_flag_target_state = True + self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value self._hass.components.climate.set_temperature( @@ -130,6 +133,7 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f', self._entity_id, value) self.temperature_flag_target_state = True + self.char_target_temp.set_value(value, should_callback=False) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e6dbe1ff729..ee7baae2755 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,17 +16,6 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} -def test_get_accessory_invalid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('test.unsupported', 'on'), 2, None) \ - is None - assert caplog.records[1].levelname == 'WARNING' - - assert get_accessory(None, State('test.test', 'on'), None, None) \ - is None - assert caplog.records[3].levelname == 'WARNING' - - class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" From 22cefc7e640bee867296a98c9098470f52aa052b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 25 Mar 2018 12:51:11 +0200 Subject: [PATCH 163/924] Improve detection of entity names in templates (#13432) * Improve detection of entity names in templates * Only test variables --- homeassistant/helpers/template.py | 5 +++-- tests/helpers/test_template.py | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3dd65aa362c..28ab4e9bfa0 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,7 +13,7 @@ from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, MATCH_ALL, STATE_UNKNOWN) -from homeassistant.core import State +from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.loader import bind_hass, get_component @@ -73,7 +73,8 @@ def extract_entities(template, variables=None): extraction_final.append(result[0]) if variables and result[1] in variables and \ - isinstance(variables[result[1]], str): + isinstance(variables[result[1]], str) and \ + valid_entity_id(variables[result[1]]): extraction_final.append(variables[result[1]]) if extraction_final: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 47e46bae3c7..def06ea9284 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -836,6 +836,12 @@ is_state_attr('device_tracker.phone_2', 'battery', 40) "{{ is_state(trigger.entity_id, 'off') }}", {'trigger': {'entity_id': 'input_boolean.switch'}})) + self.assertEqual( + MATCH_ALL, + template.extract_entities( + "{{ is_state('media_player.' ~ where , 'playing') }}", + {'where': 'livingroom'})) + @asyncio.coroutine def test_state_with_unit(hass): From 93b9ec0b0f904b0f20b5c1b8b922e68806196de9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:04:20 -0700 Subject: [PATCH 164/924] Add version bump script (#13447) * Add version bump script * Lint --- script/version_bump.py | 135 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100755 script/version_bump.py diff --git a/script/version_bump.py b/script/version_bump.py new file mode 100755 index 00000000000..0cd02ddbfcb --- /dev/null +++ b/script/version_bump.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Helper script to bump the current version.""" +import argparse +import re + +from homeassistant import const + + +PARSE_PATCH = r'(?P\d+)(\.(?P\D+)(?P\d+))?' + + +def format_patch(patch_parts): + """Format the patch parts back into a patch string.""" + return '{patch}.{prerel}{prerelversion}'.format(**patch_parts) + + +def bump_version(cur_major, cur_minor, cur_patch, bump_type): + """Return a new version given a current version and action.""" + patch_parts = re.match(PARSE_PATCH, cur_patch).groupdict() + patch_parts['patch'] = int(patch_parts['patch']) + if patch_parts['prerelversion'] is not None: + patch_parts['prerelversion'] = int(patch_parts['prerelversion']) + + if bump_type == 'release_patch': + # Convert 0.67.3 to 0.67.4 + # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.dev0 to 0.67.3 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + new_patch = str(patch_parts['patch'] + 1) + else: + new_patch = str(patch_parts['patch']) + + elif bump_type == 'dev': + # Convert 0.67.3 to 0.67.4.dev0 + # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.dev0 to 0.67.3.dev1 + new_major = cur_major + + if patch_parts['prerel'] == 'dev': + new_minor = cur_minor + patch_parts['prerelversion'] += 1 + new_patch = format_patch(patch_parts) + else: + new_minor = cur_minor + 1 + new_patch = '0.dev0' + + elif bump_type == 'beta': + # Convert 0.67.5 to 0.67.8.beta0 + # Convert 0.67.0.dev0 to 0.67.0.beta0 + # Convert 0.67.5.beta4 to 0.67.5.beta5 + new_major = cur_major + new_minor = cur_minor + + if patch_parts['prerel'] is None: + patch_parts['patch'] += 1 + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + elif patch_parts['prerel'] == 'beta': + patch_parts['prerelversion'] += 1 + + elif patch_parts['prerel'] == 'dev': + patch_parts['prerel'] = 'beta' + patch_parts['prerelversion'] = 0 + + else: + raise Exception('Can only bump from beta or no prerel version') + + new_patch = format_patch(patch_parts) + + return new_major, new_minor, new_patch + + +def write_version(major, minor, patch): + """Update Home Assistant constant file with new version.""" + with open('homeassistant/const.py') as fil: + content = fil.read() + + content = re.sub('MAJOR_VERSION = .*\n', + 'MAJOR_VERSION = {}\n'.format(major), + content) + content = re.sub('MINOR_VERSION = .*\n', + 'MINOR_VERSION = {}\n'.format(minor), + content) + content = re.sub('PATCH_VERSION = .*\n', + "PATCH_VERSION = '{}'\n".format(patch), + content) + + with open('homeassistant/const.py', 'wt') as fil: + content = fil.write(content) + + +def main(): + """Execute script.""" + parser = argparse.ArgumentParser( + description="Bump version of Home Assistant") + parser.add_argument( + 'type', + help="The type of the bump the version to.", + choices=['beta', 'dev', 'release_patch'], + ) + arguments = parser.parse_args() + write_version(*bump_version(const.MAJOR_VERSION, const.MINOR_VERSION, + const.PATCH_VERSION, arguments.type)) + + +def test_bump_version(): + """Make sure it all works.""" + assert bump_version(0, 56, '0', 'beta') == \ + (0, 56, '1.beta0') + assert bump_version(0, 56, '0.beta3', 'beta') == \ + (0, 56, '0.beta4') + assert bump_version(0, 56, '0.dev0', 'beta') == \ + (0, 56, '0.beta0') + + assert bump_version(0, 56, '3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.beta3', 'dev') == \ + (0, 57, '0.dev0') + assert bump_version(0, 56, '0.dev0', 'dev') == \ + (0, 56, '0.dev1') + + assert bump_version(0, 56, '3', 'release_patch') == \ + (0, 56, '4') + assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + (0, 56, '3') + assert bump_version(0, 56, '0.dev0', 'release_patch') == \ + (0, 56, '0') + + +if __name__ == '__main__': + main() From 38d2702e3c05d7df42f87c998b8b819e6a1e3c40 Mon Sep 17 00:00:00 2001 From: Cedric Van Goethem Date: Mon, 26 Mar 2018 02:03:23 +0100 Subject: [PATCH 165/924] Add extra check for ESSID field in case there's a wired connection (#13459) --- homeassistant/components/device_tracker/unifi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index 8663930c4e6..d8a52aaaeb4 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -98,7 +98,8 @@ class UnifiScanner(DeviceScanner): # Filter clients to provided SSID list if self._ssid_filter: clients = [client for client in clients - if client['essid'] in self._ssid_filter] + if 'essid' in client and + client['essid'] in self._ssid_filter] self._clients = { client['mac']: client From 068b037944ab3ee918430e0607294cadaf521b1f Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Mon, 26 Mar 2018 03:02:21 +0200 Subject: [PATCH 166/924] Fix encoding errors in mikrotik device tracker (#13464) --- homeassistant/components/device_tracker/mikrotik.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 1d9161c0d45..154fc3d2a63 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -73,7 +73,8 @@ class MikrotikScanner(DeviceScanner): self.host, self.username, self.password, - port=int(self.port) + port=int(self.port), + encoding='utf-8' ) try: From a507ed0af8aaa4b6a464cdb9b138f4a8173c404b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 25 Mar 2018 18:24:16 -0700 Subject: [PATCH 167/924] Version bump to 0.66.0.beta1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b0be1933ffe..382323ed534 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.beta0' +PATCH_VERSION = '0.beta1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a6e455a07054204b74e224913f95eeac0d5bc5d5 Mon Sep 17 00:00:00 2001 From: Dan Nixon Date: Mon, 26 Mar 2018 17:22:21 +0100 Subject: [PATCH 168/924] Make Telnet Switch value template optional (#13433) When no statis command is defined a value template does nothing so should not have to be provided. --- homeassistant/components/switch/telnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/telnet.py b/homeassistant/components/switch/telnet.py index 7c69b31aa00..c3a608b9692 100644 --- a/homeassistant/components/switch/telnet.py +++ b/homeassistant/components/switch/telnet.py @@ -25,7 +25,7 @@ SWITCH_SCHEMA = vol.Schema({ vol.Required(CONF_COMMAND_OFF): cv.string, vol.Required(CONF_COMMAND_ON): cv.string, vol.Required(CONF_RESOURCE): cv.string, - vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_STATE): cv.string, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, From 2e3ec121d13062061305e8d9106035032f21795e Mon Sep 17 00:00:00 2001 From: Lindsay Ward Date: Tue, 27 Mar 2018 02:27:53 +1000 Subject: [PATCH 169/924] Update yeelightsunflower to 0.0.10 (#13448) * Update yeelightsunflower to 0.0.10 * Update yeelightsunflower platform to 0.0.10 --- homeassistant/components/light/yeelightsunflower.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/yeelightsunflower.py b/homeassistant/components/light/yeelightsunflower.py index 88f86063c13..96cce67b1bb 100644 --- a/homeassistant/components/light/yeelightsunflower.py +++ b/homeassistant/components/light/yeelightsunflower.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_HOST import homeassistant.util.color as color_util -REQUIREMENTS = ['yeelightsunflower==0.0.8'] +REQUIREMENTS = ['yeelightsunflower==0.0.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5b2be9dcbde..bd32ed12d9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1307,7 +1307,7 @@ yahooweather==0.10 yeelight==0.4.0 # homeassistant.components.light.yeelightsunflower -yeelightsunflower==0.0.8 +yeelightsunflower==0.0.10 # homeassistant.components.media_extractor youtube_dl==2018.03.10 From 181e68b0278a6afd81e1557b7c0b5bbc64749893 Mon Sep 17 00:00:00 2001 From: c727 Date: Mon, 26 Mar 2018 19:22:05 +0200 Subject: [PATCH 170/924] Add more info to issue template (#12955) * Update ISSUE_TEMPLATE.md * Minumum supported version is Python 3.5.3 * typo * Feedback * Feedback * Address comments --- .github/ISSUE_TEMPLATE.md | 57 ++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index c570b548360..e853ce2f1b4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,23 +1,50 @@ -Make sure you are running the latest version of Home Assistant before reporting an issue. + -You should only file an issue if you found a bug. Feature and enhancement requests should go in [the Feature Requests section](https://community.home-assistant.io/c/feature-requests) of our community forum: +**1. Home Assistant version:** + +``` -**Home Assistant release (`hass --version`):** +``` + +**2.a) I run Hass.io or the Docker image**: + +``` + +``` + +**2.b) ...No, I run an installation with this Python version:** + +``` + +``` + +**3. Component/platform:** + -**Python release (`python3 --version`):** +**4. Description of problem:** -**Component/platform:** +**5. Expected:** -**Description of problem:** - - -**Expected:** - - -**Problem-relevant `configuration.yaml` entries and steps to reproduce:** +**6. Problem-relevant `configuration.yaml` entries and steps to reproduce:** ```yaml ``` @@ -26,10 +53,10 @@ You should only file an issue if you found a bug. Feature and enhancement reques 2. 3. -**Traceback (if applicable):** -```bash +**7. Traceback (if applicable):** +``` ``` -**Additional info:** +**8. Additional info:** From 3e6f4d0e5acb3ea70e439501541c1f15d98b115e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 26 Mar 2018 21:21:18 +0200 Subject: [PATCH 171/924] [RFC] Update issue template (#12989) * Update issue template * Any release --- .github/ISSUE_TEMPLATE.md | 47 +++++++++++++-------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index e853ce2f1b4..84464220749 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,62 +1,45 @@ -**1. Home Assistant version:** +**Home Assistant release with the issue:** -``` -``` -**2.a) I run Hass.io or the Docker image**: +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** -``` -``` - -**2.b) ...No, I run an installation with this Python version:** +**Component/platform:** -``` - -``` - -**3. Component/platform:** - -**4. Description of problem:** +**Description of problem:** -**5. Expected:** - -**6. Problem-relevant `configuration.yaml` entries and steps to reproduce:** +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** ```yaml ``` -1. -2. -3. - -**7. Traceback (if applicable):** +**Traceback (if applicable):** ``` ``` -**8. Additional info:** +**Additional information:** From 263dbe5d81af3667227bee8183e9bfd679ab25ca Mon Sep 17 00:00:00 2001 From: phileaton Date: Mon, 26 Mar 2018 12:32:38 -0700 Subject: [PATCH 172/924] Update total_connect_client to 0.17 for Honeywell L5100-WiFi Support (#13473) * Update total_connect_client to 0.17 * Delete tqdm.1 --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 5c1323989d4..1f383e32f92 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.16'] +REQUIREMENTS = ['total_connect_client==0.17'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bd32ed12d9d..2dfd359c1f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ todoist-python==7.0.17 toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.16 +total_connect_client==0.17 # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission From 24e0bb198a8f809afce156708fc9345b60a3a56a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:00:56 -0700 Subject: [PATCH 173/924] Hue: Convert XY to HS color if HS not present (#13465) * Hue: Convert XY to HS color if HS not present * Revert change to test * Address comments * Lint --- homeassistant/components/light/hue.py | 30 ++++++++------ tests/components/light/test_hue.py | 57 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 71e3d4fa30b..4a54f0a337d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -18,6 +18,7 @@ from homeassistant.components.light import ( FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, Light) +from homeassistant.util import color DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -235,19 +236,26 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # Don't return hue/sat if in color temperature mode - if self._color_mode == "ct": + # pylint: disable=redefined-outer-name + mode = self._color_mode + + if mode not in ('hs', 'xy'): + return + + source = self.light.action if self.is_group else self.light.state + + hue = source.get('hue') + sat = source.get('sat') + + # Sometimes the state will not include valid hue/sat values. + # Reported as issue 13434 + if hue is not None and sat is not None: + return hue / 65535 * 360, sat / 255 * 100 + + if 'xy' not in source: return None - if self.is_group: - return ( - self.light.action.get('hue') / 65535 * 360, - self.light.action.get('sat') / 255 * 100, - ) - return ( - self.light.state.get('hue') / 65535 * 360, - self.light.state.get('sat') / 255 * 100, - ) + return color.color_xy_to_hs(*source['xy']) @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 54bb2184a64..d73531b1b9a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -11,6 +11,7 @@ import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light +from homeassistant.util import color _LOGGER = logging.getLogger(__name__) @@ -623,3 +624,59 @@ def test_available(): ) assert light.available is True + + +def test_hs_color(): + """Test hs_color property.""" + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'ct', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': None, + 'sat': 123, + 'xy': [0.4, 0.5] + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == color.color_xy_to_hs(0.4, 0.5) From 08bcf841709e1c2fbc632d99bc970337b22ec8fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:55:09 -0700 Subject: [PATCH 174/924] version should contain just 'b' not 'beta' (#13476) --- script/version_bump.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index 0cd02ddbfcb..0500fc45957 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -23,7 +23,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): if bump_type == 'release_patch': # Convert 0.67.3 to 0.67.4 - # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 new_major = cur_major new_minor = cur_minor @@ -35,7 +35,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): elif bump_type == 'dev': # Convert 0.67.3 to 0.67.4.dev0 - # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 new_major = cur_major @@ -48,22 +48,22 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): new_patch = '0.dev0' elif bump_type == 'beta': - # Convert 0.67.5 to 0.67.8.beta0 - # Convert 0.67.0.dev0 to 0.67.0.beta0 - # Convert 0.67.5.beta4 to 0.67.5.beta5 + # Convert 0.67.5 to 0.67.8.b0 + # Convert 0.67.0.dev0 to 0.67.0.b0 + # Convert 0.67.5.b4 to 0.67.5.b5 new_major = cur_major new_minor = cur_minor if patch_parts['prerel'] is None: patch_parts['patch'] += 1 - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 - elif patch_parts['prerel'] == 'beta': + elif patch_parts['prerel'] == 'b': patch_parts['prerelversion'] += 1 elif patch_parts['prerel'] == 'dev': - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 else: @@ -110,22 +110,22 @@ def main(): def test_bump_version(): """Make sure it all works.""" assert bump_version(0, 56, '0', 'beta') == \ - (0, 56, '1.beta0') - assert bump_version(0, 56, '0.beta3', 'beta') == \ - (0, 56, '0.beta4') + (0, 56, '1.b0') + assert bump_version(0, 56, '0.b3', 'beta') == \ + (0, 56, '0.b4') assert bump_version(0, 56, '0.dev0', 'beta') == \ - (0, 56, '0.beta0') + (0, 56, '0.b0') assert bump_version(0, 56, '3', 'dev') == \ (0, 57, '0.dev0') - assert bump_version(0, 56, '0.beta3', 'dev') == \ + assert bump_version(0, 56, '0.b3', 'dev') == \ (0, 57, '0.dev0') assert bump_version(0, 56, '0.dev0', 'dev') == \ (0, 56, '0.dev1') assert bump_version(0, 56, '3', 'release_patch') == \ (0, 56, '4') - assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + assert bump_version(0, 56, '3.b3', 'release_patch') == \ (0, 56, '3') assert bump_version(0, 56, '0.dev0', 'release_patch') == \ (0, 56, '0') From f1d37fc8494225515e4729affc1d58a5a9915f2d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:07:22 -0700 Subject: [PATCH 175/924] Upgrade aiohue and fix race condition (#13475) * Bump aiohue to 1.3 * Store bridge in hass.data before setting up platform * Fix tests --- homeassistant/components/hue/__init__.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_bridge.py | 1 + tests/components/hue/test_setup.py | 6 +----- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 2fb55f8f6e0..b70021e0304 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries from homeassistant.util.json import save_json -REQUIREMENTS = ['aiohue==1.2.0'] +REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) @@ -145,7 +145,6 @@ async def async_setup_bridge( bridge = HueBridge(host, hass, filename, username, allow_unreachable, allow_hue_groups) await bridge.async_setup() - hass.data[DOMAIN][host] = bridge def _find_username_from_config(hass, filename): @@ -209,6 +208,8 @@ class HueBridge(object): self.host) return + self.hass.data[DOMAIN][self.host] = self + # If we came here and configuring this host, mark as done if self.config_request_id: request_id = self.config_request_id diff --git a/requirements_all.txt b/requirements_all.txt index 2dfd359c1f1..e8baacf7d2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7610eb02d2..185f1fff81b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 88a7223d91e..39351699df5 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -66,6 +66,7 @@ async def test_only_create_no_username(hass): async def test_configurator_callback(hass, mock_request): """.""" + hass.data[hue.DOMAIN] = {} with patch('aiohue.Bridge.create_user', side_effect=aiohue.LinkButtonNotPressed): await MockBridge(hass).async_setup() diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py index 690419fcb7a..f90f58a50c3 100644 --- a/tests/components/hue/test_setup.py +++ b/tests/components/hue/test_setup.py @@ -18,7 +18,6 @@ async def test_setup_with_multiple_hosts(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 2 hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) assert hosts == ['127.0.0.1', '192.168.1.10'] - assert len(hass.data[hue.DOMAIN]) == 2 async def test_bridge_discovered(hass, mock_bridge): @@ -33,7 +32,6 @@ async def test_bridge_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 async def test_bridge_configure_and_discovered(hass, mock_bridge): @@ -48,7 +46,7 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 + hass.data[hue.DOMAIN] = {'192.168.1.10': {}} mock_bridge.reset_mock() @@ -59,7 +57,6 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): await hass.async_block_till_done() assert len(mock_bridge.mock_calls) == 0 - assert len(hass.data[hue.DOMAIN]) == 1 async def test_setup_no_host(hass, aioclient_mock): @@ -71,4 +68,3 @@ async def test_setup_no_host(hass, aioclient_mock): assert result assert len(aioclient_mock.mock_calls) == 1 - assert len(hass.data[hue.DOMAIN]) == 0 From 254256c08ff83586bc6dbf2d439192a5667cdce3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 27 Mar 2018 01:08:44 +0200 Subject: [PATCH 176/924] Fix ID (fixes #13444) (#13471) --- homeassistant/components/media_player/mpchc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index cc195db2590..a375a585ad4 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -155,8 +155,8 @@ class MpcHcDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._send_command(921) + self._send_command(920) def media_previous_track(self): """Send previous track command.""" - self._send_command(920) + self._send_command(919) From dfe3219f3f39254b30468dc273285ce936c952e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:00:56 -0700 Subject: [PATCH 177/924] Hue: Convert XY to HS color if HS not present (#13465) * Hue: Convert XY to HS color if HS not present * Revert change to test * Address comments * Lint --- homeassistant/components/light/hue.py | 30 ++++++++------ tests/components/light/test_hue.py | 57 +++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 71e3d4fa30b..4a54f0a337d 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -18,6 +18,7 @@ from homeassistant.components.light import ( FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_COLOR, SUPPORT_TRANSITION, Light) +from homeassistant.util import color DEPENDENCIES = ['hue'] SCAN_INTERVAL = timedelta(seconds=5) @@ -235,19 +236,26 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # Don't return hue/sat if in color temperature mode - if self._color_mode == "ct": + # pylint: disable=redefined-outer-name + mode = self._color_mode + + if mode not in ('hs', 'xy'): + return + + source = self.light.action if self.is_group else self.light.state + + hue = source.get('hue') + sat = source.get('sat') + + # Sometimes the state will not include valid hue/sat values. + # Reported as issue 13434 + if hue is not None and sat is not None: + return hue / 65535 * 360, sat / 255 * 100 + + if 'xy' not in source: return None - if self.is_group: - return ( - self.light.action.get('hue') / 65535 * 360, - self.light.action.get('sat') / 255 * 100, - ) - return ( - self.light.state.get('hue') / 65535 * 360, - self.light.state.get('sat') / 255 * 100, - ) + return color.color_xy_to_hs(*source['xy']) @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 54bb2184a64..d73531b1b9a 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -11,6 +11,7 @@ import pytest from homeassistant.components import hue import homeassistant.components.light.hue as hue_light +from homeassistant.util import color _LOGGER = logging.getLogger(__name__) @@ -623,3 +624,59 @@ def test_available(): ) assert light.available is True + + +def test_hs_color(): + """Test hs_color property.""" + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'ct', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) + + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'xy', + 'hue': None, + 'sat': 123, + 'xy': [0.4, 0.5] + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color == color.color_xy_to_hs(0.4, 0.5) From f48ce3d437c5bcfcbd4eb4d2ff6ca7756dd93966 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 27 Mar 2018 01:08:44 +0200 Subject: [PATCH 178/924] Fix ID (fixes #13444) (#13471) --- homeassistant/components/media_player/mpchc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpchc.py b/homeassistant/components/media_player/mpchc.py index cc195db2590..a375a585ad4 100644 --- a/homeassistant/components/media_player/mpchc.py +++ b/homeassistant/components/media_player/mpchc.py @@ -155,8 +155,8 @@ class MpcHcDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._send_command(921) + self._send_command(920) def media_previous_track(self): """Send previous track command.""" - self._send_command(920) + self._send_command(919) From ce3a5972c787b1cc854d0ae1deb3558b28a816f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:07:22 -0700 Subject: [PATCH 179/924] Upgrade aiohue and fix race condition (#13475) * Bump aiohue to 1.3 * Store bridge in hass.data before setting up platform * Fix tests --- homeassistant/components/hue/__init__.py | 5 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hue/test_bridge.py | 1 + tests/components/hue/test_setup.py | 6 +----- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 2fb55f8f6e0..b70021e0304 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers import discovery, aiohttp_client from homeassistant import config_entries from homeassistant.util.json import save_json -REQUIREMENTS = ['aiohue==1.2.0'] +REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) @@ -145,7 +145,6 @@ async def async_setup_bridge( bridge = HueBridge(host, hass, filename, username, allow_unreachable, allow_hue_groups) await bridge.async_setup() - hass.data[DOMAIN][host] = bridge def _find_username_from_config(hass, filename): @@ -209,6 +208,8 @@ class HueBridge(object): self.host) return + self.hass.data[DOMAIN][self.host] = self + # If we came here and configuring this host, mark as done if self.config_request_id: request_id = self.config_request_id diff --git a/requirements_all.txt b/requirements_all.txt index 017449bfeca..e30b70eb976 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -74,7 +74,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fda514af007..27d3bd21ad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.2.0 +aiohue==1.3.0 # homeassistant.components.notify.apns apns2==0.3.0 diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 88a7223d91e..39351699df5 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -66,6 +66,7 @@ async def test_only_create_no_username(hass): async def test_configurator_callback(hass, mock_request): """.""" + hass.data[hue.DOMAIN] = {} with patch('aiohue.Bridge.create_user', side_effect=aiohue.LinkButtonNotPressed): await MockBridge(hass).async_setup() diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py index 690419fcb7a..f90f58a50c3 100644 --- a/tests/components/hue/test_setup.py +++ b/tests/components/hue/test_setup.py @@ -18,7 +18,6 @@ async def test_setup_with_multiple_hosts(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 2 hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) assert hosts == ['127.0.0.1', '192.168.1.10'] - assert len(hass.data[hue.DOMAIN]) == 2 async def test_bridge_discovered(hass, mock_bridge): @@ -33,7 +32,6 @@ async def test_bridge_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 async def test_bridge_configure_and_discovered(hass, mock_bridge): @@ -48,7 +46,7 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): assert len(mock_bridge.mock_calls) == 1 assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - assert len(hass.data[hue.DOMAIN]) == 1 + hass.data[hue.DOMAIN] = {'192.168.1.10': {}} mock_bridge.reset_mock() @@ -59,7 +57,6 @@ async def test_bridge_configure_and_discovered(hass, mock_bridge): await hass.async_block_till_done() assert len(mock_bridge.mock_calls) == 0 - assert len(hass.data[hue.DOMAIN]) == 1 async def test_setup_no_host(hass, aioclient_mock): @@ -71,4 +68,3 @@ async def test_setup_no_host(hass, aioclient_mock): assert result assert len(aioclient_mock.mock_calls) == 1 - assert len(hass.data[hue.DOMAIN]) == 0 From a06eea444af977c8513db2fe7c1593da816bec0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 14:55:09 -0700 Subject: [PATCH 180/924] version should contain just 'b' not 'beta' (#13476) --- script/version_bump.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index 0cd02ddbfcb..0500fc45957 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -23,7 +23,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): if bump_type == 'release_patch': # Convert 0.67.3 to 0.67.4 - # Convert 0.67.3.beta5 to 0.67.3 + # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 new_major = cur_major new_minor = cur_minor @@ -35,7 +35,7 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): elif bump_type == 'dev': # Convert 0.67.3 to 0.67.4.dev0 - # Convert 0.67.3.beta5 to 0.67.4.dev0 + # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 new_major = cur_major @@ -48,22 +48,22 @@ def bump_version(cur_major, cur_minor, cur_patch, bump_type): new_patch = '0.dev0' elif bump_type == 'beta': - # Convert 0.67.5 to 0.67.8.beta0 - # Convert 0.67.0.dev0 to 0.67.0.beta0 - # Convert 0.67.5.beta4 to 0.67.5.beta5 + # Convert 0.67.5 to 0.67.8.b0 + # Convert 0.67.0.dev0 to 0.67.0.b0 + # Convert 0.67.5.b4 to 0.67.5.b5 new_major = cur_major new_minor = cur_minor if patch_parts['prerel'] is None: patch_parts['patch'] += 1 - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 - elif patch_parts['prerel'] == 'beta': + elif patch_parts['prerel'] == 'b': patch_parts['prerelversion'] += 1 elif patch_parts['prerel'] == 'dev': - patch_parts['prerel'] = 'beta' + patch_parts['prerel'] = 'b' patch_parts['prerelversion'] = 0 else: @@ -110,22 +110,22 @@ def main(): def test_bump_version(): """Make sure it all works.""" assert bump_version(0, 56, '0', 'beta') == \ - (0, 56, '1.beta0') - assert bump_version(0, 56, '0.beta3', 'beta') == \ - (0, 56, '0.beta4') + (0, 56, '1.b0') + assert bump_version(0, 56, '0.b3', 'beta') == \ + (0, 56, '0.b4') assert bump_version(0, 56, '0.dev0', 'beta') == \ - (0, 56, '0.beta0') + (0, 56, '0.b0') assert bump_version(0, 56, '3', 'dev') == \ (0, 57, '0.dev0') - assert bump_version(0, 56, '0.beta3', 'dev') == \ + assert bump_version(0, 56, '0.b3', 'dev') == \ (0, 57, '0.dev0') assert bump_version(0, 56, '0.dev0', 'dev') == \ (0, 56, '0.dev1') assert bump_version(0, 56, '3', 'release_patch') == \ (0, 56, '4') - assert bump_version(0, 56, '3.beta3', 'release_patch') == \ + assert bump_version(0, 56, '3.b3', 'release_patch') == \ (0, 56, '3') assert bump_version(0, 56, '0.dev0', 'release_patch') == \ (0, 56, '0') From 94d9aa0c5fe023725d74beb6a61402f3b1169a3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:10:03 -0700 Subject: [PATCH 181/924] Bump version to 0.66.0.b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 382323ed534..58c49289989 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.beta1' +PATCH_VERSION = '0.b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3639a4470c6c626414a8c4018f455e3f79b59170 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 26 Mar 2018 16:16:42 -0700 Subject: [PATCH 182/924] Use twine for release --- script/release | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/release b/script/release index 17d5ad9370d..dc3e208bc1a 100755 --- a/script/release +++ b/script/release @@ -27,4 +27,5 @@ then exit 1 fi -python3 setup.py sdist bdist_wheel upload +python3 setup.py sdist bdist_wheel +python3 -m twine upload dist/* --skip-existing From 81cf0dacfe27d0cb7105f539ff3ae01fd02fdcd3 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Mon, 26 Mar 2018 19:10:22 -0600 Subject: [PATCH 183/924] Fix Google Calendar caching when offline (#13375) * Fix Google Calendar caching when offline Events from Google Calendar were not firing under the following circumstances: 1. Start ha as normal with Google Calendar configured as per instructions. 2. ha loses network connectivity to Google 3. ha attempts update of Google Calendar 4. calendar/google component throws uncaught Exception causing update method to not return 5. (cached) Google Calendar event does not fire, remains "Off" Catching the Exception and returning False from the update() method causes the correct behavior (i.e., the calendar component firing the event as scheduled using cached data). * Add requirements * Revert code cleanup * Remove explicit return value from update() * Revert "Remove explicit return value from update()" This reverts commit 7cd77708af658ccea855de47a32ce4ac5262ac30. * Use MockDependency decorator No need to whitelist google-python-api-client for a single unit test at this point. --- homeassistant/components/calendar/google.py | 9 ++++++++- tests/components/calendar/test_google.py | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 098c7c70834..a8763e8ca9e 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -62,7 +62,14 @@ class GoogleCalendarData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - service = self.calendar_service.get() + from httplib2 import ServerNotFoundError + + try: + service = self.calendar_service.get() + except ServerNotFoundError: + _LOGGER.warning("Unable to connect to Google, using cached data") + return False + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 62c8ea8854f..9f94ea9f44c 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import logging import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest @@ -11,7 +11,7 @@ import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, MockDependency TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -421,3 +421,16 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'location': event['location'], 'description': event['description'] }) + + @MockDependency("httplib2") + def test_update_false(self, mock_httplib2): + """Test that the update returns False upon Error.""" + mock_service = Mock() + mock_service.get = Mock( + side_effect=mock_httplib2.ServerNotFoundError("unit test")) + + cal = calendar.GoogleCalendarEventDevice(self.hass, mock_service, None, + {'name': "test"}) + result = cal.data.update() + + self.assertFalse(result) From 8a0facb747e59867a135b6fcc429b1c1d55feebf Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 04:50:29 +0200 Subject: [PATCH 184/924] Validate basic customize entries (#13478) * Added schema to validate customize dictionary * Added test --- homeassistant/config.py | 13 ++++++++++--- tests/test_config.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 58cfe845e8f..53e611ac725 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -13,6 +13,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, @@ -129,13 +130,19 @@ PACKAGES_CONFIG_SCHEMA = vol.Schema({ {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names }) +CUSTOMIZE_DICT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(ATTR_ASSUMED_STATE): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + CUSTOMIZE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.entity_id: dict}), + vol.Schema({cv.entity_id: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): - vol.Schema({cv.string: dict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): - vol.Schema({cv.string: OrderedDict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), }) CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ diff --git a/tests/test_config.py b/tests/test_config.py index aaa793f91a9..22fcebc6ea4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ from voluptuous import MultipleInvalid from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) @@ -235,6 +236,29 @@ class TestConfig(unittest.TestCase): }, }) + def test_customize_dict_schema(self): + """Test basic customize config validation.""" + values = ( + {ATTR_FRIENDLY_NAME: None}, + {ATTR_HIDDEN: '2'}, + {ATTR_ASSUMED_STATE: '2'}, + ) + + for val in values: + print(val) + with pytest.raises(MultipleInvalid): + config_util.CUSTOMIZE_DICT_SCHEMA(val) + + assert config_util.CUSTOMIZE_DICT_SCHEMA({ + ATTR_FRIENDLY_NAME: 2, + ATTR_HIDDEN: '1', + ATTR_ASSUMED_STATE: '0', + }) == { + ATTR_FRIENDLY_NAME: '2', + ATTR_HIDDEN: True, + ATTR_ASSUMED_STATE: False + } + def test_customize_glob_is_ordered(self): """Test that customize_glob preserves order.""" conf = config_util.CORE_CONFIG_SCHEMA( From 9eda04b787a55416e5ea8a183d38e8e39698f6aa Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 11:31:18 +0200 Subject: [PATCH 185/924] Homekit: Bugfix Thermostat Fahrenheit support (#13477) * Bugfix thermostat temperature conversion * util -> temperature_to_homekit * util -> temperature_to_states * util -> convert_to_float * Added tests, deleted log msg --- homeassistant/components/homekit/__init__.py | 3 -- .../components/homekit/type_sensors.py | 35 ++++------------ .../components/homekit/type_thermostats.py | 33 ++++++++++----- homeassistant/components/homekit/util.py | 21 +++++++++- .../homekit/test_get_accessories.py | 14 +++++++ tests/components/homekit/test_type_sensors.py | 21 +--------- .../homekit/test_type_thermostats.py | 41 ++++++++++++++++++- tests/components/homekit/test_util.py | 23 ++++++++++- 8 files changed, 126 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4854a828e41..8ef8445aa70 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -186,9 +186,6 @@ class HomeKit(): for state in self._hass.states.all(): self.add_bridge_accessory(state) - for entity_id in self._config: - _LOGGER.warning('The entity "%s" was not setup when HomeKit ' - 'was started', entity_id) self.bridge.set_broker(self.driver) if not self.bridge.paired: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7575acb5c35..e980ce4a316 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,8 +2,7 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.util.temperature import fahrenheit_to_celsius + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from . import TYPES from .accessories import ( @@ -11,33 +10,12 @@ from .accessories import ( from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) +from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) -def calc_temperature(state, unit=TEMP_CELSIUS): - """Calculate temperature from state and unit. - - Always return temperature as Celsius value. - Conversion is handled on the device. - """ - try: - value = float(state) - except ValueError: - return None - - return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value - - -def calc_humidity(state): - """Calculate humidity from state.""" - try: - return float(state) - except ValueError: - return None - - @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -63,9 +41,10 @@ class TemperatureSensor(HomeAccessory): if new_state is None: return - unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] - temperature = calc_temperature(new_state.state, unit) + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + temperature = convert_to_float(new_state.state) if temperature: + temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) @@ -92,8 +71,8 @@ class HumiditySensor(HomeAccessory): if new_state is None: return - humidity = calc_humidity(new_state.state) + humidity = convert_to_float(new_state.state) if humidity: self.char_humidity.set_value(humidity, should_callback=False) - _LOGGER.debug('%s: Current humidity set to %d%%', + _LOGGER.debug('%s: Percent set to %d%%', self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 3f545e90eb3..d49c1ca626b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -16,6 +16,7 @@ from .const import ( CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) +from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Thermostat(HomeAccessory): self._hass = hass self._entity_id = entity_id self._call_timer = None + self._unit = TEMP_CELSIUS self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False @@ -107,33 +109,38 @@ class Thermostat(HomeAccessory): def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self._entity_id, value) self.coolingthresh_flag_target_state = True self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value + low = temperature_to_states(low, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heating threshold temperature to %.2f', + _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self._entity_id, value) self.heatingthresh_flag_target_state = True self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value + high = temperature_to_states(high, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=high, target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set target temperature to %.2f', + _LOGGER.debug('%s: Set target temperature to %.2f°C', self._entity_id, value) self.temperature_flag_target_state = True self.char_target_temp.set_value(value, should_callback=False) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) @@ -142,14 +149,19 @@ class Thermostat(HomeAccessory): if new_state is None: return + self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS) + # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): + current_temp = temperature_to_homekit(current_temp, self._unit) self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): + target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) @@ -158,7 +170,9 @@ class Thermostat(HomeAccessory): # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh: + if isinstance(cooling_thresh, (int, float)): + cooling_thresh = temperature_to_homekit(cooling_thresh, + self._unit) if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) @@ -167,18 +181,17 @@ class Thermostat(HomeAccessory): # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh: + if isinstance(heating_thresh, (int, float)): + heating_thresh = temperature_to_homekit(heating_thresh, + self._unit) if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) self.heatingthresh_flag_target_state = False # Update display units - display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units \ - and display_units in UNIT_HASS_TO_HOMEKIT: - self.char_display_units.set_value( - UNIT_HASS_TO_HOMEKIT[display_units]) + if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: + self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f18eb2273db..2fa2ebd396a 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE) + ATTR_CODE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv +import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID _LOGGER = logging.getLogger(__name__) @@ -44,3 +45,21 @@ def show_setup_message(bridge, hass): def dismiss_setup_message(hass): """Dismiss persistent notification and remove QR code.""" hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + + +def convert_to_float(state): + """Return float of state, catch errors.""" + try: + return float(state) + except (ValueError, TypeError): + return None + + +def temperature_to_homekit(temperature, unit): + """Convert temperature to Celsius for HomeKit.""" + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + + +def temperature_to_states(temperature, unit): + """Convert temperature back from Celsius to Home Assistant unit.""" + return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index ee7baae2755..e29ed85b5fc 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,6 +16,20 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} +def test_get_accessory_invalid_aid(caplog): + """Test with unsupported component.""" + assert get_accessory(None, State('light.demo', 'on'), + aid=None, config=None) is None + assert caplog.records[0].levelname == 'WARNING' + assert 'invalid aid' in caplog.records[0].msg + + +def test_not_supported(): + """Test if none is returned if entity isn't supported.""" + assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \ + is None + + class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 551dfc6780d..c04c250613d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,32 +3,13 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) + TemperatureSensor, HumiditySensor) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -def test_calc_temperature(): - """Test if temperature in Celsius is calculated correctly.""" - assert calc_temperature(STATE_UNKNOWN) is None - assert calc_temperature('test') is None - - assert calc_temperature('20') == 20 - assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - - -def test_calc_humidity(): - """Test if humidity is a integer.""" - assert calc_humidity(STATE_UNKNOWN) is None - assert calc_humidity('test') is None - - assert calc_humidity('20') == 20 - assert calc_humidity('75.2') == 75.2 - - class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 6505bf72efb..011fe73377d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -10,7 +10,7 @@ from homeassistant.components.homekit.type_thermostats import ( Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -238,3 +238,42 @@ class TestHomekitThermostats(unittest.TestCase): self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + + def test_thermostat_fahrenheit(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + acc = Thermostat(self.hass, climate, 'Climate', True) + acc.run() + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) + self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) + self.assertEqual(acc.char_current_temp.value, 23.0) + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 1) + + # Set from HomeKit + acc.char_cooling_thresh_temp.set_value(23) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + + acc.char_heating_thresh_temp.set_value(22) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + + acc.char_target_temp.set_value(24.0) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f95db9a4a13..d6ef5856f85 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,13 +7,15 @@ from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, ATTR_CODE) + show_setup_message, dismiss_setup_message, convert_to_float, + temperature_to_homekit, temperature_to_states, ATTR_CODE) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -81,3 +83,20 @@ class TestUtil(unittest.TestCase): self.assertEqual( data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), HOMEKIT_NOTIFY_ID) + + def test_convert_to_float(self): + """Test convert_to_float method.""" + self.assertEqual(convert_to_float(12), 12) + self.assertEqual(convert_to_float(12.4), 12.4) + self.assertIsNone(convert_to_float(STATE_UNKNOWN)) + self.assertIsNone(convert_to_float(None)) + + def test_temperature_to_homekit(self): + """Test temperature conversion from HA to HomeKit.""" + self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5) + self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4) + + def test_temperature_to_states(self): + """Test temperature conversion from HomeKit to HA.""" + self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0) + self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4) From 06aded1a4db86043d4f49b30dab87879bd816fba Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 27 Mar 2018 13:09:01 +0200 Subject: [PATCH 186/924] Upgrade python-mystrom to 0.4.2 (#13485) --- homeassistant/components/light/mystrom.py | 4 ++-- homeassistant/components/switch/mystrom.py | 6 +++--- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/mystrom.py b/homeassistant/components/light/mystrom.py index d9312e6aadc..8d7fb807c6d 100644 --- a/homeassistant/components/light/mystrom.py +++ b/homeassistant/components/light/mystrom.py @@ -15,7 +15,7 @@ from homeassistant.components.light import ( ATTR_HS_COLOR) from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, STATE_UNKNOWN -REQUIREMENTS = ['python-mystrom==0.3.8'] +REQUIREMENTS = ['python-mystrom==0.4.2'] _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the myStrom Light platform.""" - from pymystrom import MyStromBulb + from pymystrom.bulb import MyStromBulb from pymystrom.exceptions import MyStromConnectionError host = config.get(CONF_HOST) diff --git a/homeassistant/components/switch/mystrom.py b/homeassistant/components/switch/mystrom.py index e813da43dfa..0a87d41d2fe 100644 --- a/homeassistant/components/switch/mystrom.py +++ b/homeassistant/components/switch/mystrom.py @@ -12,7 +12,7 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import (CONF_NAME, CONF_HOST) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-mystrom==0.3.8'] +REQUIREMENTS = ['python-mystrom==0.4.2'] DEFAULT_NAME = 'myStrom Switch' @@ -26,7 +26,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Find and return myStrom switch.""" - from pymystrom import MyStromPlug, exceptions + from pymystrom.switch import MyStromPlug, exceptions name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -45,7 +45,7 @@ class MyStromSwitch(SwitchDevice): def __init__(self, name, resource): """Initialize the myStrom switch.""" - from pymystrom import MyStromPlug + from pymystrom.switch import MyStromPlug self._name = name self._resource = resource diff --git a/requirements_all.txt b/requirements_all.txt index e8baacf7d2e..026892099f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -955,7 +955,7 @@ python-mpd2==0.5.5 # homeassistant.components.light.mystrom # homeassistant.components.switch.mystrom -python-mystrom==0.3.8 +python-mystrom==0.4.2 # homeassistant.components.nest python-nest==3.7.0 From 264be677872ae04b96b9d42f48126258641008ca Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 27 Mar 2018 20:29:18 +0200 Subject: [PATCH 187/924] New service added to control the power mode of the yeelight (#13267) * New service added to control the power mode of the yeelight * Debug output removed. * Strict validation of the available power modes * Service description added * Service parameter name fixed --- homeassistant/components/light/services.yaml | 10 ++++ homeassistant/components/light/yeelight.py | 60 ++++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 44e887e62c4..9645e50d06e 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -179,3 +179,13 @@ xiaomi_miio_set_delayed_turn_off: time_period: description: Time period for the delayed turn off. example: "5, '0:05', {'minutes': 5}" + +yeelight_set_mode: + description: Set a operation mode. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + mode: + description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. + example: 'moonlight' diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 585db950efc..7061c24aac6 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_COLOR_TEMP, ATTR_FLASH, FLASH_SHORT, FLASH_LONG, ATTR_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, SUPPORT_FLASH, - SUPPORT_EFFECT, Light, PLATFORM_SCHEMA) + SUPPORT_EFFECT, Light, PLATFORM_SCHEMA, ATTR_ENTITY_ID, DOMAIN) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -30,7 +30,7 @@ DEFAULT_TRANSITION = 350 CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' -DOMAIN = 'yeelight' +DATA_KEY = 'light.yeelight' DEVICE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, @@ -90,6 +90,13 @@ YEELIGHT_EFFECT_LIST = [ EFFECT_TWITTER, EFFECT_STOP] +SERVICE_SET_MODE = 'yeelight_set_mode' +ATTR_MODE = 'mode' + +YEELIGHT_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, +}) + def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" @@ -106,6 +113,11 @@ def _cmd(func): def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Yeelight bulbs.""" + from yeelight.enums import PowerMode + + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} + lights = [] if discovery_info is not None: _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) @@ -115,16 +127,44 @@ def setup_platform(hass, config, add_devices, discovery_info=None): discovery_info['properties']['mac']) device = {'name': name, 'ipaddr': discovery_info['host']} - lights.append(YeelightLight(device, DEVICE_SCHEMA({}))) + light = YeelightLight(device, DEVICE_SCHEMA({})) + lights.append(light) + hass.data[DATA_KEY][name] = light else: for ipaddr, device_config in config[CONF_DEVICES].items(): - _LOGGER.debug("Adding configured %s", device_config[CONF_NAME]) + name = device_config[CONF_NAME] + _LOGGER.debug("Adding configured %s", name) - device = {'name': device_config[CONF_NAME], 'ipaddr': ipaddr} - lights.append(YeelightLight(device, device_config)) + device = {'name': name, 'ipaddr': ipaddr} + light = YeelightLight(device, device_config) + lights.append(light) + hass.data[DATA_KEY][name] = light add_devices(lights, True) + def service_handler(service): + """Dispatch service calls to target entities.""" + params = {key: value for key, value in service.data.items() + if key != ATTR_ENTITY_ID} + entity_ids = service.data.get(ATTR_ENTITY_ID) + if entity_ids: + target_devices = [dev for dev in hass.data[DATA_KEY].values() + if dev.entity_id in entity_ids] + else: + target_devices = hass.data[DATA_KEY].values() + + for target_device in target_devices: + if service.service == SERVICE_SET_MODE: + target_device.set_mode(**params) + + service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend({ + vol.Required(ATTR_MODE): + vol.In([mode.name.lower() for mode in PowerMode]) + }) + hass.services.register( + DOMAIN, SERVICE_SET_MODE, service_handler, + schema=service_schema_set_mode) + class YeelightLight(Light): """Representation of a Yeelight light.""" @@ -444,3 +484,11 @@ class YeelightLight(Light): self._bulb.turn_off(duration=duration) except yeelight.BulbException as ex: _LOGGER.error("Unable to turn the bulb off: %s", ex) + + def set_mode(self, mode: str): + """Set a power mode.""" + import yeelight + try: + self._bulb.set_power_mode(yeelight.enums.PowerMode[mode.upper()]) + except yeelight.BulbException as ex: + _LOGGER.error("Unable to set the power mode: %s", ex) From 2bebfec3a62824c5d86cf8c2dc1da5ac1a7085df Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 23:39:25 +0200 Subject: [PATCH 188/924] Homekit: Fix security systems (#13499) * Fix alarm_code=None * Added test --- .../components/homekit/type_security_systems.py | 4 +++- .../homekit/test_type_security_systems.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 146fca95b53..b23522f0ea2 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -58,7 +58,9 @@ class SecuritySystem(HomeAccessory): hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} + params = {ATTR_ENTITY_ID: self._entity_id} + if self._alarm_code: + params[ATTR_CODE] = self._alarm_code self._hass.services.call('alarm_control_panel', service, params) def update_state(self, entity_id=None, old_state=None, new_state=None): diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 4d61fc4a44c..c689a73bac2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -102,3 +102,19 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) + + def test_no_alarm_code(self): + """Test accessory if security_system doesn't require a alarm_code.""" + acp = 'alarm_control_panel.test' + + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code=None, aid=2) + acc.run() + + # Set from HomeKit + acc.char_target_state.set_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) From 00c6df54b27b0d7be47463ef2752f99c259ab98d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 28 Mar 2018 08:27:56 +0200 Subject: [PATCH 189/924] Upgrade slacker to 0.9.65 (#13496) --- homeassistant/components/notify/slack.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/slack.py b/homeassistant/components/notify/slack.py index 30aadfc8297..b50260e4c61 100644 --- a/homeassistant/components/notify/slack.py +++ b/homeassistant/components/notify/slack.py @@ -17,7 +17,7 @@ from homeassistant.components.notify import ( BaseNotificationService) from homeassistant.const import (CONF_API_KEY, CONF_USERNAME, CONF_ICON) -REQUIREMENTS = ['slacker==0.9.60'] +REQUIREMENTS = ['slacker==0.9.65'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 026892099f1..d57ecd7f93a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1136,7 +1136,7 @@ simplisafe-python==1.0.5 skybellpy==0.1.1 # homeassistant.components.notify.slack -slacker==0.9.60 +slacker==0.9.65 # homeassistant.components.notify.xmpp sleekxmpp==1.3.2 From bdb4d754ae47073c7298d2a4288e7d0303677fe8 Mon Sep 17 00:00:00 2001 From: Mikael Svensson Date: Wed, 28 Mar 2018 09:04:18 +0200 Subject: [PATCH 190/924] Adds template function state_attr to get attribute from a state (#13378) * Adds template function state_attr to get attribute from a state Refactored is_state_attr to use new function Adds tests for state_attr * Fixes line too long and test bug * Fixes pylint error * Fixes tests and D401 lint error --- homeassistant/helpers/template.py | 13 ++++++++++--- tests/helpers/test_template.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 28ab4e9bfa0..a04023cfc4f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -28,7 +28,7 @@ DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" _RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M) _RE_GET_ENTITIES = re.compile( - r"(?:(?:states\.|(?:is_state|is_state_attr|states)" + r"(?:(?:states\.|(?:is_state|is_state_attr|state_attr|states)" r"\((?:[\ \'\"]?))([\w]+\.[\w]+)|([\w]+))", re.I | re.M ) @@ -182,6 +182,7 @@ class Template(object): 'distance': template_methods.distance, 'is_state': self.hass.states.is_state, 'is_state_attr': template_methods.is_state_attr, + 'state_attr': template_methods.state_attr, 'states': AllStates(self.hass), }) @@ -405,9 +406,15 @@ class TemplateMethods(object): def is_state_attr(self, entity_id, name, value): """Test if a state is a specific attribute.""" + state_attr = self.state_attr(entity_id, name) + return state_attr is not None and state_attr == value + + def state_attr(self, entity_id, name): + """Get a specific attribute from a state.""" state_obj = self._hass.states.get(entity_id) - return state_obj is not None and \ - state_obj.attributes.get(name) == value + if state_obj is not None: + return state_obj.attributes.get(name) + return None def _resolve_state(self, entity_id_or_state): """Return state or entity_id if given.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index def06ea9284..693c3909924 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -397,6 +397,19 @@ class TestHelpersTemplate(unittest.TestCase): """, self.hass) self.assertEqual('False', tpl.render()) + def test_state_attr(self): + """Test state_attr method.""" + self.hass.states.set('test.object', 'available', {'mode': 'on'}) + tpl = template.Template(""" +{% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %} + """, self.hass) + self.assertEqual('yes', tpl.render()) + + tpl = template.Template(""" +{{ state_attr("test.noobject", "mode") == None }} + """, self.hass) + self.assertEqual('True', tpl.render()) + def test_states_function(self): """Test using states as a function.""" self.hass.states.set('test.object', 'available') From 45ff15bc85b47814343cabb725fd76c373091435 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 28 Mar 2018 12:45:24 +0200 Subject: [PATCH 191/924] Upgrade aiohttp to 3.1.1 (#13510) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 317c1c8bc6c..85f8d5dcf12 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.0 +aiohttp==3.1.1 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index d57ecd7f93a..10e6050bd4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.0 +aiohttp==3.1.1 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index 9324713e71e..db4b1f8df92 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.1.0', + 'aiohttp==3.1.1', 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', From b3b7cf3fa7ac744a976a42b1ac3b642dadc86b11 Mon Sep 17 00:00:00 2001 From: Lewis Juggins Date: Wed, 28 Mar 2018 23:50:09 +0100 Subject: [PATCH 192/924] Update tradfri v5 (#11187) * First pass to support simplified colour management in tradfri * Fix lint * Fix lint * Update imports * Prioritise brightness for transition * Fix bug * None check * Bracket * Import * Fix bugs * Change colour logic * Denormalise colour * Lint * Fix bug * Fix bugs, expose rgb conversion * Fix bug * Fix bug * Fix bug * Improve XY * Improve XY * async/wait for tradfri. * Bump requirement * Formatting. * Remove comma * Line length, shadowing * Switch to new HS colour system, using native data from tradfri gateway. * Lint. * Brightness bug. * Remove guard. * Temp workaround for bug. * Temp workaround for bug. * Temp workaround for bug. * Safety. * Switch logic. * Integrate latest * Fixes. * Fixes. * Mired validation. * Set bounds. * Transition time. * Transition time. * Transition time. * Fix brightness values. --- homeassistant/components/light/tradfri.py | 175 +++++++++------------ homeassistant/components/sensor/tradfri.py | 16 +- homeassistant/components/tradfri.py | 43 +++-- requirements_all.txt | 2 +- 4 files changed, 100 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 1851579a172..ca153042f8d 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -4,11 +4,9 @@ Support for the IKEA Tradfri platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.tradfri/ """ -import asyncio import logging from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, @@ -17,20 +15,19 @@ from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ KEY_API -from homeassistant.util import color as color_util _LOGGER = logging.getLogger(__name__) +ATTR_TRANSITION_TIME = 'transition_time' DEPENDENCIES = ['tradfri'] PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA IKEA = 'IKEA of Sweden' TRADFRI_LIGHT_MANAGER = 'Tradfri Light Manager' SUPPORTED_FEATURES = (SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION) -ALLOWED_TEMPERATURES = {IKEA} -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, + async_add_devices, discovery_info=None): """Set up the IKEA Tradfri Light platform.""" if discovery_info is None: return @@ -40,8 +37,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): gateway = hass.data[KEY_GATEWAY][gateway_id] devices_command = gateway.get_devices() - devices_commands = yield from api(devices_command) - devices = yield from api(devices_commands) + devices_commands = await api(devices_command) + devices = await api(devices_commands) lights = [dev for dev in devices if dev.has_light_control] if lights: async_add_devices(TradfriLight(light, api) for light in lights) @@ -49,8 +46,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] if allow_tradfri_groups: groups_command = gateway.get_groups() - groups_commands = yield from api(groups_command) - groups = yield from api(groups_commands) + groups_commands = await api(groups_command) + groups = await api(groups_commands) if groups: async_add_devices(TradfriGroup(group, api) for group in groups) @@ -66,8 +63,7 @@ class TradfriGroup(Light): self._refresh(light) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() @@ -96,13 +92,11 @@ class TradfriGroup(Light): """Return the brightness of the group lights.""" return self._group.dimmer - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the group lights to turn off.""" - yield from self._api(self._group.set_state(0)) + await self._api(self._group.set_state(0)) - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Instruct the group lights to turn on, or dim.""" keys = {} if ATTR_TRANSITION in kwargs: @@ -112,16 +106,16 @@ class TradfriGroup(Light): if kwargs[ATTR_BRIGHTNESS] == 255: kwargs[ATTR_BRIGHTNESS] = 254 - yield from self._api( + await self._api( self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) else: - yield from self._api(self._group.set_state(1)) + await self._api(self._group.set_state(1)) @callback def _async_start_observe(self, exc=None): """Start observation of light.""" # pylint: disable=import-error - from pytradfri.error import PyTradFriError + from pytradfri.error import PytradfriError if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -131,7 +125,7 @@ class TradfriGroup(Light): err_callback=self._async_start_observe, duration=0) self.hass.async_add_job(self._api(cmd)) - except PyTradFriError as err: + except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -159,7 +153,6 @@ class TradfriLight(Light): self._name = None self._hs_color = None self._features = SUPPORTED_FEATURES - self._temp_supported = False self._available = True self._refresh(light) @@ -167,33 +160,14 @@ class TradfriLight(Light): @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - if self._light_control.max_kelvin is not None: - return color_util.color_temperature_kelvin_to_mired( - self._light_control.max_kelvin - ) + return self._light_control.min_mireds @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - if self._light_control.min_kelvin is not None: - return color_util.color_temperature_kelvin_to_mired( - self._light_control.min_kelvin - ) + return self._light_control.max_mireds - @property - def device_state_attributes(self): - """Return the devices' state attributes.""" - info = self._light.device_info - - attrs = {} - - if info.battery_level is not None: - attrs[ATTR_BATTERY_LEVEL] = info.battery_level - - return attrs - - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() @@ -229,64 +203,73 @@ class TradfriLight(Light): @property def color_temp(self): - """Return the CT color value in mireds.""" - kelvin_color = self._light_data.kelvin_color_inferred - if kelvin_color is not None: - return color_util.color_temperature_kelvin_to_mired( - kelvin_color - ) + """Return the color temp value in mireds.""" + return self._light_data.color_temp @property def hs_color(self): """HS color of the light.""" - return self._hs_color + if self._light_control.can_set_color: + hsbxy = self._light_data.hsb_xy_color + hue = hsbxy[0] / (65535 / 360) + sat = hsbxy[1] / (65279 / 100) + if hue is not None and sat is not None: + return hue, sat - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - yield from self._api(self._light_control.set_state(False)) + await self._api(self._light_control.set_state(False)) - @asyncio.coroutine - def async_turn_on(self, **kwargs): - """ - Instruct the light to turn on. - - After adding "self._light_data.hexcolor is not None" - for ATTR_HS_COLOR, this also supports Philips Hue bulbs. - """ - if ATTR_HS_COLOR in kwargs and self._light_data.hex_color is not None: - rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - yield from self._api( - self._light.light_control.set_rgb_color(*rgb)) - - elif ATTR_COLOR_TEMP in kwargs and \ - self._light_data.hex_color is not None and \ - self._temp_supported: - kelvin = color_util.color_temperature_mired_to_kelvin( - kwargs[ATTR_COLOR_TEMP]) - yield from self._api( - self._light_control.set_kelvin_color(kelvin)) - - keys = {} + async def async_turn_on(self, **kwargs): + """Instruct the light to turn on.""" + params = {} + transition_time = None if ATTR_TRANSITION in kwargs: - keys['transition_time'] = int(kwargs[ATTR_TRANSITION]) * 10 + transition_time = int(kwargs[ATTR_TRANSITION]) * 10 - if ATTR_BRIGHTNESS in kwargs: - if kwargs[ATTR_BRIGHTNESS] == 255: - kwargs[ATTR_BRIGHTNESS] = 254 + brightness = kwargs.get(ATTR_BRIGHTNESS) - yield from self._api( - self._light_control.set_dimmer(kwargs[ATTR_BRIGHTNESS], - **keys)) + if brightness is not None: + if brightness > 254: + brightness = 254 + elif brightness < 0: + brightness = 0 + + if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: + params[ATTR_BRIGHTNESS] = brightness + hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360)) + sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100)) + await self._api( + self._light_control.set_hsb(hue, sat, **params)) + return + + if ATTR_COLOR_TEMP in kwargs and self._light_control.can_set_temp: + temp = kwargs[ATTR_COLOR_TEMP] + if temp > self.max_mireds: + temp = self.max_mireds + elif temp < self.min_mireds: + temp = self.min_mireds + + if brightness is None: + params[ATTR_TRANSITION_TIME] = transition_time + await self._api( + self._light_control.set_color_temp(temp, + **params)) + + if brightness is not None: + params[ATTR_TRANSITION_TIME] = transition_time + await self._api( + self._light_control.set_dimmer(brightness, + **params)) else: - yield from self._api( + await self._api( self._light_control.set_state(True)) @callback def _async_start_observe(self, exc=None): """Start observation of light.""" # pylint: disable=import-error - from pytradfri.error import PyTradFriError + from pytradfri.error import PytradfriError if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -296,7 +279,7 @@ class TradfriLight(Light): err_callback=self._async_start_observe, duration=0) self.hass.async_add_job(self._api(cmd)) - except PyTradFriError as err: + except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() @@ -309,27 +292,15 @@ class TradfriLight(Light): self._light_control = light.light_control self._light_data = light.light_control.lights[0] self._name = light.name - self._hs_color = None self._features = SUPPORTED_FEATURES - if self._light.device_info.manufacturer == IKEA: - if self._light_control.can_set_kelvin: - self._features |= SUPPORT_COLOR_TEMP - if self._light_control.can_set_color: - self._features |= SUPPORT_COLOR - else: - if self._light_data.hex_color is not None: - self._features |= SUPPORT_COLOR - - self._temp_supported = self._light.device_info.manufacturer \ - in ALLOWED_TEMPERATURES + if light.light_control.can_set_color: + self._features |= SUPPORT_COLOR + if light.light_control.can_set_temp: + self._features |= SUPPORT_COLOR_TEMP @callback def _observe_update(self, tradfri_device): """Receive new state data for this light.""" self._refresh(tradfri_device) - rgb = color_util.rgb_hex_to_rgb_list( - self._light_data.hex_color_inferred - ) - self._hs_color = color_util.color_RGB_to_hs(*rgb) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/sensor/tradfri.py b/homeassistant/components/sensor/tradfri.py index d087fdda9f6..df931770cf2 100644 --- a/homeassistant/components/sensor/tradfri.py +++ b/homeassistant/components/sensor/tradfri.py @@ -4,7 +4,6 @@ Support for the IKEA Tradfri platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.tradfri/ """ -import asyncio import logging from datetime import timedelta @@ -20,8 +19,8 @@ DEPENDENCIES = ['tradfri'] SCAN_INTERVAL = timedelta(minutes=5) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the IKEA Tradfri device platform.""" if discovery_info is None: return @@ -31,8 +30,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): gateway = hass.data[KEY_GATEWAY][gateway_id] devices_command = gateway.get_devices() - devices_commands = yield from api(devices_command) - all_devices = yield from api(devices_commands) + devices_commands = await api(devices_command) + all_devices = await api(devices_commands) devices = [dev for dev in all_devices if not dev.has_light_control] async_add_devices(TradfriDevice(device, api) for device in devices) @@ -48,8 +47,7 @@ class TradfriDevice(Entity): self._refresh(device) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() @@ -91,7 +89,7 @@ class TradfriDevice(Entity): def _async_start_observe(self, exc=None): """Start observation of light.""" # pylint: disable=import-error - from pytradfri.error import PyTradFriError + from pytradfri.error import PytradfriError if exc: _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -101,7 +99,7 @@ class TradfriDevice(Entity): err_callback=self._async_start_observe, duration=0) self.hass.async_add_job(self._api(cmd)) - except PyTradFriError as err: + except PytradfriError as err: _LOGGER.warning("Observation failed, trying again", exc_info=err) self._async_start_observe() diff --git a/homeassistant/components/tradfri.py b/homeassistant/components/tradfri.py index 5ac4d2a4eb1..72d1b4c769f 100644 --- a/homeassistant/components/tradfri.py +++ b/homeassistant/components/tradfri.py @@ -1,10 +1,9 @@ """ -Support for Ikea Tradfri. +Support for IKEA Tradfri. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ikea_tradfri/ """ -import asyncio import logging from uuid import uuid4 @@ -16,7 +15,7 @@ from homeassistant.const import CONF_HOST from homeassistant.components.discovery import SERVICE_IKEA_TRADFRI from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pytradfri[async]==4.1.0'] +REQUIREMENTS = ['pytradfri[async]==5.4.2'] DOMAIN = 'tradfri' GATEWAY_IDENTITY = 'homeassistant' @@ -49,8 +48,7 @@ def request_configuration(hass, config, host): if instance: return - @asyncio.coroutine - def configuration_callback(callback_data): + async def configuration_callback(callback_data): """Handle the submitted configuration.""" try: from pytradfri.api.aiocoap_api import APIFactory @@ -67,14 +65,14 @@ def request_configuration(hass, config, host): # pytradfri aiocoap API into an endless loop. # Should just raise a requestError or something. try: - key = yield from api_factory.generate_psk(security_code) + key = await api_factory.generate_psk(security_code) except RequestError: configurator.async_notify_errors(hass, instance, "Security Code not accepted.") return - res = yield from _setup_gateway(hass, config, host, identity, key, - DEFAULT_ALLOW_TRADFRI_GROUPS) + res = await _setup_gateway(hass, config, host, identity, key, + DEFAULT_ALLOW_TRADFRI_GROUPS) if not res: configurator.async_notify_errors(hass, instance, @@ -101,18 +99,16 @@ def request_configuration(hass, config, host): ) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the Tradfri component.""" conf = config.get(DOMAIN, {}) host = conf.get(CONF_HOST) allow_tradfri_groups = conf.get(CONF_ALLOW_TRADFRI_GROUPS) - known_hosts = yield from hass.async_add_job(load_json, - hass.config.path(CONFIG_FILE)) + known_hosts = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) - @asyncio.coroutine - def gateway_discovered(service, info, - allow_tradfri_groups=DEFAULT_ALLOW_TRADFRI_GROUPS): + async def gateway_discovered(service, info, + allow_groups=DEFAULT_ALLOW_TRADFRI_GROUPS): """Run when a gateway is discovered.""" host = info['host'] @@ -121,23 +117,22 @@ def async_setup(hass, config): # identity was hard coded as 'homeassistant' identity = known_hosts[host].get('identity', 'homeassistant') key = known_hosts[host].get('key') - yield from _setup_gateway(hass, config, host, identity, key, - allow_tradfri_groups) + await _setup_gateway(hass, config, host, identity, key, + allow_groups) else: hass.async_add_job(request_configuration, hass, config, host) discovery.async_listen(hass, SERVICE_IKEA_TRADFRI, gateway_discovered) if host: - yield from gateway_discovered(None, - {'host': host}, - allow_tradfri_groups) + await gateway_discovered(None, + {'host': host}, + allow_tradfri_groups) return True -@asyncio.coroutine -def _setup_gateway(hass, hass_config, host, identity, key, - allow_tradfri_groups): +async def _setup_gateway(hass, hass_config, host, identity, key, + allow_tradfri_groups): """Create a gateway.""" from pytradfri import Gateway, RequestError # pylint: disable=import-error try: @@ -151,7 +146,7 @@ def _setup_gateway(hass, hass_config, host, identity, key, loop=hass.loop) api = factory.request gateway = Gateway() - gateway_info_result = yield from api(gateway.get_gateway_info()) + gateway_info_result = await api(gateway.get_gateway_info()) except RequestError: _LOGGER.exception("Tradfri setup failed.") return False diff --git a/requirements_all.txt b/requirements_all.txt index 10e6050bd4b..15f6c1ddbda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1021,7 +1021,7 @@ pytouchline==0.7 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==4.1.0 +pytradfri[async]==5.4.2 # homeassistant.components.device_tracker.unifi pyunifi==2.13 From ef7fd9f380c0f1e7fb3c61e54e1abfb601dcbcde Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 29 Mar 2018 00:55:05 +0200 Subject: [PATCH 193/924] python-miio version bumped (Closes: 13449) (#13511) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index a1cb0431381..0eb0823a116 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_MODEL = 'model' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index a21c86f49c0..999d0f7f0e6 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 91f753391fc..13ec9c873b1 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index cb172735ac4..33ba5793fe0 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 9f0f163df69..27c3c4c72f1 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index f42a895f94f..887a50fdcce 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 15f6c1ddbda..b07ed3d8fac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -948,7 +948,7 @@ python-juicenet==0.0.5 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.8 +python-miio==0.3.9 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 From 3b537f6e2afc2792b5f12f97b6e89e5371aa396c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 29 Mar 2018 10:40:41 +0200 Subject: [PATCH 194/924] Fix typos and update link (fixes #13520) (#13529) --- .github/ISSUE_TEMPLATE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 84464220749..8772a136eb3 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,9 +1,9 @@ **Home Assistant release with the issue:** @@ -23,7 +23,7 @@ Please provide details about your environment. **Component/platform:** From cea2de5eb5dacc8b398b5437425472e4c5e2e978 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Mar 2018 18:35:57 +0200 Subject: [PATCH 195/924] HomeKit: Fix setting light brightness (#13518) * Added test --- .../components/homekit/type_lights.py | 21 +++++--- tests/components/homekit/test_type_lights.py | 50 ++++++++++++------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 2415bb1a4df..d88e7100131 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -32,6 +32,7 @@ class Light(HomeAccessory): self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} + self._state = 0 self.chars = [] self._features = self._hass.states.get(self._entity_id) \ @@ -47,7 +48,7 @@ class Light(HomeAccessory): serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) self.char_on = serv_light.get_characteristic(CHAR_ON) self.char_on.setter_callback = self.set_state - self.char_on.value = 0 + self.char_on.value = self._state if CHAR_BRIGHTNESS in self.chars: self.char_brightness = serv_light \ @@ -66,7 +67,7 @@ class Light(HomeAccessory): def set_state(self, value): """Set state if call came from HomeKit.""" - if self._flag[CHAR_BRIGHTNESS]: + if self._state == value: return _LOGGER.debug('%s: Set state to %d', self._entity_id, value) @@ -83,8 +84,11 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True self.char_brightness.set_value(value, should_callback=False) - self._hass.components.light.turn_on( - self._entity_id, brightness_pct=value) + if value != 0: + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + else: + self._hass.components.light.turn_off(self._entity_id) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -121,10 +125,11 @@ class Light(HomeAccessory): # Handle State state = new_state.state - if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ - self.char_on.value != (state == STATE_ON): - self.char_on.set_value(state == STATE_ON, should_callback=False) - self._flag[CHAR_ON] = False + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ON] and self.char_on.value != self._state: + self.char_on.set_value(self._state, should_callback=False) + self._flag[CHAR_ON] = False # Handle Brightness if CHAR_BRIGHTNESS in self.chars: diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index b4d4d5a5945..ee1900fd7c5 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -57,19 +57,21 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_on.value, 0) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - acc.char_on.set_value(False) + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + + acc.char_on.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + self.hass.states.set(entity_id, STATE_OFF) self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) # Remove entity self.hass.states.remove(entity_id) @@ -95,15 +97,27 @@ class TestHomekitLights(unittest.TestCase): acc.char_brightness.set_value(20) acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - print(self.events[0].data) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + acc.char_on.set_value(1) + acc.char_brightness.set_value(40) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + + acc.char_on.set_value(1) + acc.char_brightness.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -123,10 +137,8 @@ class TestHomekitLights(unittest.TestCase): acc.char_hue.set_value(145) acc.char_saturation.set_value(75) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) From 298e6eeef189fb36ccb0a381af1695c36531cdfc Mon Sep 17 00:00:00 2001 From: NovapaX Date: Thu, 29 Mar 2018 21:39:56 +0200 Subject: [PATCH 196/924] Tradfri - unique_id's and color_temp support for rgb-bulbs (#13531) * unique_ids for tradfri lights and groups * set color temperature on CWS bulb Cannot set_color_temp on color bulb, needs conversion from mired to hsb * make travis happy * change condition so we ensure color bulbs are included, change comments. --- homeassistant/components/light/tradfri.py | 51 ++++++++++++++++++----- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index ca153042f8d..227ed419aec 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -15,6 +15,7 @@ from homeassistant.components.light import \ PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA from homeassistant.components.tradfri import KEY_GATEWAY, KEY_TRADFRI_GROUPS, \ KEY_API +import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,8 @@ async def async_setup_platform(hass, config, devices = await api(devices_commands) lights = [dev for dev in devices if dev.has_light_control] if lights: - async_add_devices(TradfriLight(light, api) for light in lights) + async_add_devices( + TradfriLight(light, api, gateway_id) for light in lights) allow_tradfri_groups = hass.data[KEY_TRADFRI_GROUPS][gateway_id] if allow_tradfri_groups: @@ -49,24 +51,31 @@ async def async_setup_platform(hass, config, groups_commands = await api(groups_command) groups = await api(groups_commands) if groups: - async_add_devices(TradfriGroup(group, api) for group in groups) + async_add_devices( + TradfriGroup(group, api, gateway_id) for group in groups) class TradfriGroup(Light): """The platform class required by hass.""" - def __init__(self, light, api): + def __init__(self, group, api, gateway_id): """Initialize a Group.""" self._api = api - self._group = light - self._name = light.name + self._unique_id = "group-{}-{}".format(gateway_id, group.id) + self._group = group + self._name = group.name - self._refresh(light) + self._refresh(group) async def async_added_to_hass(self): """Start thread when added to hass.""" self._async_start_observe() + @property + def unique_id(self): + """Return unique ID for this group.""" + return self._unique_id + @property def should_poll(self): """No polling needed for tradfri group.""" @@ -144,9 +153,10 @@ class TradfriGroup(Light): class TradfriLight(Light): """The platform class required by Home Assistant.""" - def __init__(self, light, api): + def __init__(self, light, api, gateway_id): """Initialize a Light.""" self._api = api + self._unique_id = "light-{}-{}".format(gateway_id, light.id) self._light = None self._light_control = None self._light_data = None @@ -157,6 +167,11 @@ class TradfriLight(Light): self._refresh(light) + @property + def unique_id(self): + """Return unique ID for light.""" + return self._unique_id + @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" @@ -243,7 +258,8 @@ class TradfriLight(Light): self._light_control.set_hsb(hue, sat, **params)) return - if ATTR_COLOR_TEMP in kwargs and self._light_control.can_set_temp: + if ATTR_COLOR_TEMP in kwargs and (self._light_control.can_set_temp or + self._light_control.can_set_color): temp = kwargs[ATTR_COLOR_TEMP] if temp > self.max_mireds: temp = self.max_mireds @@ -252,9 +268,22 @@ class TradfriLight(Light): if brightness is None: params[ATTR_TRANSITION_TIME] = transition_time - await self._api( - self._light_control.set_color_temp(temp, - **params)) + # White Spectrum bulb + if (self._light_control.can_set_temp and + not self._light_control.can_set_color): + await self._api( + self._light_control.set_color_temp(temp, **params)) + # Color bulb (CWS) + # color_temp needs to be set with hue/saturation + if self._light_control.can_set_color: + params[ATTR_BRIGHTNESS] = brightness + temp_k = color_util.color_temperature_mired_to_kelvin(temp) + hs_color = color_util.color_temperature_to_hs(temp_k) + hue = int(hs_color[0] * (65535 / 360)) + sat = int(hs_color[1] * (65279 / 100)) + await self._api( + self._light_control.set_hsb(hue, sat, + **params)) if brightness is not None: params[ATTR_TRANSITION_TIME] = transition_time From 3fdb0002a79abfe75f9207d7c01aff472d25bfb5 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 29 Mar 2018 23:29:46 +0200 Subject: [PATCH 197/924] Qwikswitch async refactor & sensor (#13509) --- homeassistant/components/light/qwikswitch.py | 32 +++- homeassistant/components/qwikswitch.py | 171 ++++++++---------- homeassistant/components/sensor/qwikswitch.py | 69 +++++++ homeassistant/components/switch/qwikswitch.py | 22 ++- requirements_all.txt | 2 +- 5 files changed, 185 insertions(+), 111 deletions(-) create mode 100644 homeassistant/components/sensor/qwikswitch.py diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index c4faf0f9ca0..26741525b8f 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -4,18 +4,32 @@ Support for Qwikswitch Relays and Dimmers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.qwikswitch/ """ -import logging +from homeassistant.components.qwikswitch import ( + QSToggleEntity, DOMAIN as QWIKSWITCH) +from homeassistant.components.light import SUPPORT_BRIGHTNESS, Light -DEPENDENCIES = ['qwikswitch'] +DEPENDENCIES = [QWIKSWITCH] -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, _, add_devices, discovery_info=None): """Add lights from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - "Configure Qwikswitch Light component failed") - return False + return - add_devices(hass.data['qwikswitch']['light']) - return True + qsusb = hass.data[QWIKSWITCH] + devs = [QSLight(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSLight(QSToggleEntity, Light): + """Light based on a Qwikswitch relay/dimmer module.""" + + @property + def brightness(self): + """Return the brightness of this light (0-255).""" + return self._qsusb[self.qsid, 1] if self._dim else None + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS if self._dim else 0 diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index c4901805e3e..708eff7cf11 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -4,21 +4,21 @@ Support for Qwikswitch devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/qwikswitch/ """ -import asyncio import logging import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL) + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL, + CONF_SENSORS, CONF_SWITCHES) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import load_platform -from homeassistant.components.light import ( - ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light) -from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.entity import Entity +from homeassistant.components.light import ATTR_BRIGHTNESS +import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.5'] +REQUIREMENTS = ['pyqwikswitch==0.6'] _LOGGER = logging.getLogger(__name__) @@ -33,11 +33,14 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_URL, default='http://127.0.0.1:2020'): vol.Coerce(str), vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE, - vol.Optional(CONF_BUTTON_EVENTS): vol.Coerce(str) + vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv, + vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}), + vol.Optional(CONF_SWITCHES, default=[]): vol.All( + cv.ensure_list, [str]) })}, extra=vol.ALLOW_EXTRA) -class QSToggleEntity(object): +class QSToggleEntity(Entity): """Representation of a Qwikswitch Entity. Implement base QS methods. Modeled around HA ToggleEntity[1] & should only @@ -55,7 +58,7 @@ class QSToggleEntity(object): def __init__(self, qsid, qsusb): """Initialize the ToggleEntity.""" from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) - self._id = qsid + self.qsid = qsid self._qsusb = qsusb.devices dev = qsusb.devices[qsid] self._dim = dev[QS_TYPE] == QSType.dimmer @@ -74,129 +77,113 @@ class QSToggleEntity(object): @property def is_on(self): """Check if device is on (non-zero).""" - return self._qsusb[self._id, 1] > 0 + return self._qsusb[self.qsid, 1] > 0 - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self._id, new) + self._qsusb.set_value(self.qsid, new) - @asyncio.coroutine - def async_turn_on(self, **kwargs): - """Turn the device on.""" - new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self._id, new) - - def turn_off(self, **kwargs): # pylint: disable=unused-argument + async def async_turn_off(self, **_): """Turn the device off.""" - self._qsusb.set_value(self._id, 0) + self._qsusb.set_value(self.qsid, 0) - @asyncio.coroutine - def async_turn_off(self, **kwargs): # pylint: disable=unused-argument - """Turn the device off.""" - self._qsusb.set_value(self._id, 0) + def _update(self, _packet=None): + """Schedule an update - match dispather_send signature.""" + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self._update) -class QSSwitch(QSToggleEntity, SwitchDevice): - """Switch based on a Qwikswitch relay module.""" - - pass - - -class QSLight(QSToggleEntity, Light): - """Light based on a Qwikswitch relay/dimmer module.""" - - @property - def brightness(self): - """Return the brightness of this light (0-255).""" - return self._qsusb[self._id, 1] if self._dim else None - - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_BRIGHTNESS if self._dim else None - - -@asyncio.coroutine -def async_setup(hass, config): - """Setup qwiskswitch component.""" - from pyqwikswitch.async import QSUsb +async def async_setup(hass, config): + """Qwiskswitch component setup.""" + from pyqwikswitch.async_ import QSUsb from pyqwikswitch import ( - CMD_BUTTONS, QS_CMD, QSDATA, QS_ID, QS_NAME, QS_TYPE, QSType) + CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType) - hass.data[DOMAIN] = {} - - # Override which cmd's in /&listen packets will fire events + # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] - cmd_buttons = config[DOMAIN].get(CONF_BUTTON_EVENTS, ','.join(CMD_BUTTONS)) - cmd_buttons = cmd_buttons.split(',') + cmd_buttons = set(CMD_BUTTONS) + for btn in config[DOMAIN][CONF_BUTTON_EVENTS]: + cmd_buttons.add(btn) url = config[DOMAIN][CONF_URL] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] + sensors = config[DOMAIN]['sensors'] + switches = config[DOMAIN]['switches'] - def callback_value_changed(qsdevices, key, new): \ - # pylint: disable=unused-argument - """Update entiry values based on device change.""" - entity = hass.data[DOMAIN].get(key) - if entity is not None: - entity.schedule_update_ha_state() # Part of Entity/ToggleEntity + def callback_value_changed(_qsd, qsid, _val): + """Update entity values based on device change.""" + _LOGGER.debug("Dispatch %s (update from devices)", qsid) + hass.helpers.dispatcher.async_dispatcher_send(qsid, None) session = async_get_clientsession(hass) qsusb = QSUsb(url=url, dim_adj=dimmer_adjust, session=session, callback_value_changed=callback_value_changed) - @callback - def async_stop(event): # pylint: disable=unused-argument - """Stop the listener queue and clean up.""" - nonlocal qsusb - qsusb.stop() - qsusb = None - hass.data[DOMAIN] = {} - _LOGGER.info("Waiting for long poll to QSUSB to time out") - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) - # Discover all devices in QSUSB - yield from qsusb.update_from_devices() - hass.data[DOMAIN]['switch'] = [] - hass.data[DOMAIN]['light'] = [] + if not await qsusb.update_from_devices(): + return False + + hass.data[DOMAIN] = qsusb + + _new = {'switch': [], 'light': [], 'sensor': sensors} for _id, item in qsusb.devices: - if (item[QS_TYPE] == QSType.relay and - item[QSDATA][QS_NAME].lower().endswith(' switch')): - item[QSDATA][QS_NAME] = item[QSDATA][QS_NAME][:-7] # Remove switch - new_dev = QSSwitch(_id, qsusb) - hass.data[DOMAIN]['switch'].append(new_dev) + if _id in switches: + if item[QS_TYPE] != QSType.relay: + _LOGGER.warning( + "You specified a switch that is not a relay %s", _id) + continue + _new['switch'].append(_id) elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: - new_dev = QSLight(_id, qsusb) - hass.data[DOMAIN]['light'].append(new_dev) + _new['light'].append(_id) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", item) continue - hass.data[DOMAIN][_id] = new_dev # Load platforms - for comp_name in ('switch', 'light'): - if hass.data[DOMAIN][comp_name]: - load_platform(hass, comp_name, 'qwikswitch', {}, config) + for comp_name, comp_conf in _new.items(): + if comp_conf: + load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) def callback_qs_listen(item): """Typically a button press or update signal.""" - if qsusb is None: # Shutting down - return - # If button pressed, fire a hass event - if item.get(QS_CMD, '') in cmd_buttons and QS_ID in item: - hass.bus.async_fire('qwikswitch.button.{}'.format(item[QS_ID])) - return + if QS_ID in item: + if item.get(QS_CMD, '') in cmd_buttons: + hass.bus.async_fire( + 'qwikswitch.button.{}'.format(item[QS_ID]), item) + return + + # Private method due to bad __iter__ design in qsusb + # qsusb.devices returns a list of tuples + if item[QS_ID] not in \ + qsusb.devices._data: # pylint: disable=protected-access + # Not a standard device in, component can handle packet + # i.e. sensors + _LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item) + hass.helpers.dispatcher.async_dispatcher_send( + item[QS_ID], item) # Update all ha_objects hass.async_add_job(qsusb.update_from_devices) @callback - def async_start(event): # pylint: disable=unused-argument + def async_start(_): """Start listening.""" hass.async_add_job(qsusb.listen, callback_qs_listen) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_start) + @callback + def async_stop(_): + """Stop the listener queue and clean up.""" + hass.data[DOMAIN].stop() + _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)") + + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) + return True diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py new file mode 100644 index 00000000000..19b32e93670 --- /dev/null +++ b/homeassistant/components/sensor/qwikswitch.py @@ -0,0 +1,69 @@ +""" +Support for Qwikswitch Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.qwikswitch/ +""" +import logging + +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = [QWIKSWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, _, add_devices, discovery_info=None): + """Add lights from the main Qwikswitch component.""" + if discovery_info is None: + return + + qsusb = hass.data[QWIKSWITCH] + _LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info) + devs = [QSSensor(name, qsid) + for name, qsid in discovery_info[QWIKSWITCH].items()] + add_devices(devs) + + +class QSSensor(Entity): + """Sensor based on a Qwikswitch relay/dimmer module.""" + + _val = {} + + def __init__(self, sensor_name, sensor_id): + """Initialize the sensor.""" + self._name = sensor_name + self.qsid = sensor_id + + def update_packet(self, packet): + """Receive update packet from QSUSB.""" + _LOGGER.debug("Update %s (%s): %s", self.entity_id, self.qsid, packet) + self._val = packet + self.async_schedule_update_ha_state() + + @property + def state(self): + """Return the value of the sensor.""" + return self._val.get('data', 0) + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return self._val + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return None + + @property + def poll(self): + """QS sensors gets packets in update_packet.""" + return False + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + # Part of Entity/ToggleEntity + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self.update_packet) diff --git a/homeassistant/components/switch/qwikswitch.py b/homeassistant/components/switch/qwikswitch.py index 258e1141052..193c2722534 100644 --- a/homeassistant/components/switch/qwikswitch.py +++ b/homeassistant/components/switch/qwikswitch.py @@ -4,18 +4,22 @@ Support for Qwikswitch relays. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.qwikswitch/ """ -import logging +from homeassistant.components.qwikswitch import ( + QSToggleEntity, DOMAIN as QWIKSWITCH) +from homeassistant.components.switch import SwitchDevice -DEPENDENCIES = ['qwikswitch'] +DEPENDENCIES = [QWIKSWITCH] -# pylint: disable=unused-argument -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, _, add_devices, discovery_info=None): """Add switches from the main Qwikswitch component.""" if discovery_info is None: - logging.getLogger(__name__).error( - "Configure Qwikswitch Switch component failed") - return False + return - add_devices(hass.data['qwikswitch']['switch']) - return True + qsusb = hass.data[QWIKSWITCH] + devs = [QSSwitch(qsid, qsusb) for qsid in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSSwitch(QSToggleEntity, SwitchDevice): + """Switch based on a Qwikswitch relay module.""" diff --git a/requirements_all.txt b/requirements_all.txt index b07ed3d8fac..5b204ff621d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -860,7 +860,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.5 +pyqwikswitch==0.6 # homeassistant.components.rainbird pyrainbird==0.1.3 From 27865f58f11774a74f5a014e8c54feedb180ef30 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 17:00:16 -0700 Subject: [PATCH 198/924] Bump frontend to 20180330.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index dad07c87cb6..b2f50148bd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180326.0'] +REQUIREMENTS = ['home-assistant-frontend==20180330.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index e30b70eb976..df81dae9f00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,7 +356,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180326.0 +home-assistant-frontend==20180330.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27d3bd21ad7..33527a913a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180326.0 +home-assistant-frontend==20180330.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f391cbae27ed6b7e23960acaa298fc58f63f7f85 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 29 Mar 2018 20:10:27 -0400 Subject: [PATCH 199/924] Fix Insteon Leak Sensor (#13515) * update leak sensor * Fix error when insteon state type is unknown * Bump insteon version to 0.8.3 * Update requirements all and test * Fix requirements conflicts due to lack of commit sync * Requirements sync * Rerun script/gen_requirements_all.py * Try requirements update again * Update requirements --- .../components/binary_sensor/insteon_plm.py | 16 +++++----- homeassistant/components/insteon_plm.py | 31 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 09c4b5c8ea7..06079d6aa3b 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', - 'leakSensor': 'moisture'} + 'wetLeakSensor': 'moisture'} @asyncio.coroutine @@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): address = discovery_info['address'] device = plm.devices[address] state_key = discovery_info['state_key'] + name = device.states[state_key].name + if name != 'dryLeakSensor': + _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + device.address.hex, device.states[state_key].name) - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', - device.address.hex, device.states[state_key].name) + new_entity = InsteonPLMBinarySensor(device, state_key) - new_entity = InsteonPLMBinarySensor(device, state_key) - - async_add_devices([new_entity]) + async_add_devices([new_entity]) class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @property def is_on(self): """Return the boolean response if the node is on.""" - sensorstate = self._insteon_device_state.value - return bool(sensorstate) + return bool(self._insteon_device_state.value) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 2381e3db69e..6f5c5223ea0 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.2'] +REQUIREMENTS = ['insteonplm==0.8.3'] _LOGGER = logging.getLogger(__name__) @@ -64,19 +64,20 @@ def async_setup(hass, config): """Detect device from transport to be delegated to platform.""" for state_key in device.states: platform_info = ipdb[device.states[state_key]] - platform = platform_info.platform - if platform is not None: - _LOGGER.info("New INSTEON PLM device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform) + if platform_info: + platform = platform_info.platform + if platform: + _LOGGER.info("New INSTEON PLM device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform) - hass.async_add_job( - discovery.async_load_platform( - hass, platform, DOMAIN, - discovered={'address': device.address.hex, - 'state_key': state_key}, - hass_config=config)) + hass.async_add_job( + discovery.async_load_platform( + hass, platform, DOMAIN, + discovered={'address': device.address.hex, + 'state_key': state_key}, + hass_config=config)) _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( @@ -127,13 +128,15 @@ class IPDB(object): from insteonplm.states.sensor import (VariableSensor, OnOffSensor, SmokeCO2Sensor, - IoLincSensor) + IoLincSensor, + LeakSensorDryWet) self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), + State(LeakSensorDryWet, 'binary_sensor'), State(IoLincSensor, 'binary_sensor'), State(SmokeCO2Sensor, 'sensor'), State(OnOffSensor, 'binary_sensor'), diff --git a/requirements_all.txt b/requirements_all.txt index 5b204ff621d..1b3f8d163ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.2 +insteonplm==0.8.3 # homeassistant.components.verisure jsonpath==0.75 From 0a0b33af030ba7c448fac6b2293021efdadc883c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Mar 2018 02:10:56 +0200 Subject: [PATCH 200/924] Fix mysensors light supported features (#13512) * Different types of light should have different supported features. --- homeassistant/components/light/mysensors.py | 29 ++++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 14a770b7632..7aa1e754c43 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -12,8 +12,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | - SUPPORT_WHITE_VALUE) +SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE def setup_platform(hass, config, add_devices, discovery_info=None): @@ -64,11 +63,6 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): """Return true if device is on.""" return self._state - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_MYSENSORS - def _turn_on_light(self): """Turn on light child device.""" set_req = self.gateway.const.SetReq @@ -171,6 +165,11 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -188,6 +187,14 @@ class MySensorsLightDimmer(MySensorsLight): class MySensorsLightRGB(MySensorsLight): """RGB child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_COLOR + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -209,6 +216,14 @@ class MySensorsLightRGBW(MySensorsLightRGB): # pylint: disable=too-many-ancestors + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW + return SUPPORT_MYSENSORS_RGBW + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() From ab9b9157312bb4dff4a407d640b2542737b86af0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 30 Mar 2018 02:12:11 +0200 Subject: [PATCH 201/924] Construct version pinned (#13528) * Construct added to the requirements * requirements_all.txt updated --- homeassistant/components/climate/eq3btsmart.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- .../components/sensor/eddystone_temperature.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 10 ++++++++++ 9 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 5c0a3530006..820e715b00d 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.9'] +REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 0eb0823a116..8dc6bb54bd1 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'zhimi.humidifier.ca1']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_MODEL = 'model' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 999d0f7f0e6..21a27c33203 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 13ec9c873b1..b71eb2cb447 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fb5fa2c1fba..06accb26eb6 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['beacontools[scan]==1.2.1'] +REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 33ba5793fe0..066dc384007 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 27c3c4c72f1..6110b6dc469 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 887a50fdcce..b2451ed495c 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1b3f8d163ca..8c355606194 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,6 +189,16 @@ colorlog==3.1.2 # homeassistant.components.binary_sensor.concord232 concord232==0.15 +# homeassistant.components.climate.eq3btsmart +# homeassistant.components.fan.xiaomi_miio +# homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.eddystone_temperature +# homeassistant.components.sensor.xiaomi_miio +# homeassistant.components.switch.xiaomi_miio +# homeassistant.components.vacuum.xiaomi_miio +construct==2.9.41 + # homeassistant.scripts.credstash # credstash==1.14.0 From a6b63b669e87494962d979f3a6d5f65d5db37bee Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 02:13:08 +0200 Subject: [PATCH 202/924] Don't add Falsy items to list #13412 (#13536) --- homeassistant/config.py | 4 ++++ tests/test_config.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/config.py b/homeassistant/config.py index 53e611ac725..28936ae12e9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -554,6 +554,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): continue if hasattr(component, 'PLATFORM_SCHEMA'): + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue @@ -562,6 +564,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): merge_type, _ = _identify_config_schema(component) if merge_type == 'list': + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue diff --git a/tests/test_config.py b/tests/test_config.py index 22fcebc6ea4..652b931366a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -592,6 +592,25 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None +def test_merge_try_falsy(merge_log_err): + """Ensure we dont add falsy items like empty OrderedDict() to list.""" + packages = { + 'pack_falsy_to_lst': {'automation': OrderedDict()}, + 'pack_list2': {'light': OrderedDict()}, + } + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'automation': {'do': 'something'}, + 'light': {'some': 'light'}, + } + config_util.merge_packages_config(config, packages) + + assert merge_log_err.call_count == 0 + assert len(config) == 3 + assert len(config['automation']) == 1 + assert len(config['light']) == 1 + + def test_merge_new(merge_log_err): """Test adding new components to outer scope.""" packages = { From d897a07d0be7c1fd62a838344fef4f1745b72a23 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Mon, 26 Mar 2018 19:10:22 -0600 Subject: [PATCH 203/924] Fix Google Calendar caching when offline (#13375) * Fix Google Calendar caching when offline Events from Google Calendar were not firing under the following circumstances: 1. Start ha as normal with Google Calendar configured as per instructions. 2. ha loses network connectivity to Google 3. ha attempts update of Google Calendar 4. calendar/google component throws uncaught Exception causing update method to not return 5. (cached) Google Calendar event does not fire, remains "Off" Catching the Exception and returning False from the update() method causes the correct behavior (i.e., the calendar component firing the event as scheduled using cached data). * Add requirements * Revert code cleanup * Remove explicit return value from update() * Revert "Remove explicit return value from update()" This reverts commit 7cd77708af658ccea855de47a32ce4ac5262ac30. * Use MockDependency decorator No need to whitelist google-python-api-client for a single unit test at this point. --- homeassistant/components/calendar/google.py | 9 ++++++++- tests/components/calendar/test_google.py | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index 098c7c70834..a8763e8ca9e 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -62,7 +62,14 @@ class GoogleCalendarData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data.""" - service = self.calendar_service.get() + from httplib2 import ServerNotFoundError + + try: + service = self.calendar_service.get() + except ServerNotFoundError: + _LOGGER.warning("Unable to connect to Google, using cached data") + return False + params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params['timeMin'] = dt.now().isoformat('T') params['calendarId'] = self.calendar_id diff --git a/tests/components/calendar/test_google.py b/tests/components/calendar/test_google.py index 62c8ea8854f..9f94ea9f44c 100644 --- a/tests/components/calendar/test_google.py +++ b/tests/components/calendar/test_google.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import logging import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock import pytest @@ -11,7 +11,7 @@ import homeassistant.components.calendar.google as calendar import homeassistant.util.dt as dt_util from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, MockDependency TEST_PLATFORM = {calendar_base.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -421,3 +421,16 @@ class TestComponentsGoogleCalendar(unittest.TestCase): 'location': event['location'], 'description': event['description'] }) + + @MockDependency("httplib2") + def test_update_false(self, mock_httplib2): + """Test that the update returns False upon Error.""" + mock_service = Mock() + mock_service.get = Mock( + side_effect=mock_httplib2.ServerNotFoundError("unit test")) + + cal = calendar.GoogleCalendarEventDevice(self.hass, mock_service, None, + {'name': "test"}) + result = cal.data.update() + + self.assertFalse(result) From 020669fc6026a4083062debef33ccb4d400ae71c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 11:31:18 +0200 Subject: [PATCH 204/924] Homekit: Bugfix Thermostat Fahrenheit support (#13477) * Bugfix thermostat temperature conversion * util -> temperature_to_homekit * util -> temperature_to_states * util -> convert_to_float * Added tests, deleted log msg --- homeassistant/components/homekit/__init__.py | 3 -- .../components/homekit/type_sensors.py | 35 ++++------------ .../components/homekit/type_thermostats.py | 33 ++++++++++----- homeassistant/components/homekit/util.py | 21 +++++++++- .../homekit/test_get_accessories.py | 14 +++++++ tests/components/homekit/test_type_sensors.py | 21 +--------- .../homekit/test_type_thermostats.py | 41 ++++++++++++++++++- tests/components/homekit/test_util.py | 23 ++++++++++- 8 files changed, 126 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4854a828e41..8ef8445aa70 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -186,9 +186,6 @@ class HomeKit(): for state in self._hass.states.all(): self.add_bridge_accessory(state) - for entity_id in self._config: - _LOGGER.warning('The entity "%s" was not setup when HomeKit ' - 'was started', entity_id) self.bridge.set_broker(self.driver) if not self.bridge.paired: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7575acb5c35..e980ce4a316 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,8 +2,7 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_FAHRENHEIT, TEMP_CELSIUS) -from homeassistant.util.temperature import fahrenheit_to_celsius + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from . import TYPES from .accessories import ( @@ -11,33 +10,12 @@ from .accessories import ( from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) +from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) -def calc_temperature(state, unit=TEMP_CELSIUS): - """Calculate temperature from state and unit. - - Always return temperature as Celsius value. - Conversion is handled on the device. - """ - try: - value = float(state) - except ValueError: - return None - - return fahrenheit_to_celsius(value) if unit == TEMP_FAHRENHEIT else value - - -def calc_humidity(state): - """Calculate humidity from state.""" - try: - return float(state) - except ValueError: - return None - - @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -63,9 +41,10 @@ class TemperatureSensor(HomeAccessory): if new_state is None: return - unit = new_state.attributes[ATTR_UNIT_OF_MEASUREMENT] - temperature = calc_temperature(new_state.state, unit) + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + temperature = convert_to_float(new_state.state) if temperature: + temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', self._entity_id, temperature) @@ -92,8 +71,8 @@ class HumiditySensor(HomeAccessory): if new_state is None: return - humidity = calc_humidity(new_state.state) + humidity = convert_to_float(new_state.state) if humidity: self.char_humidity.set_value(humidity, should_callback=False) - _LOGGER.debug('%s: Current humidity set to %d%%', + _LOGGER.debug('%s: Percent set to %d%%', self._entity_id, humidity) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 3f545e90eb3..d49c1ca626b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -16,6 +16,7 @@ from .const import ( CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) +from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Thermostat(HomeAccessory): self._hass = hass self._entity_id = entity_id self._call_timer = None + self._unit = TEMP_CELSIUS self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False @@ -107,33 +109,38 @@ class Thermostat(HomeAccessory): def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set cooling threshold temperature to %.2f', + _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self._entity_id, value) self.coolingthresh_flag_target_state = True self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value + low = temperature_to_states(low, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=value, target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set heating threshold temperature to %.2f', + _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self._entity_id, value) self.heatingthresh_flag_target_state = True self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value + high = temperature_to_states(high, self._unit) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( entity_id=self._entity_id, target_temp_high=high, target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" - _LOGGER.debug('%s: Set target temperature to %.2f', + _LOGGER.debug('%s: Set target temperature to %.2f°C', self._entity_id, value) self.temperature_flag_target_state = True self.char_target_temp.set_value(value, should_callback=False) + value = temperature_to_states(value, self._unit) self._hass.components.climate.set_temperature( temperature=value, entity_id=self._entity_id) @@ -142,14 +149,19 @@ class Thermostat(HomeAccessory): if new_state is None: return + self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS) + # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): + current_temp = temperature_to_homekit(current_temp, self._unit) self.char_current_temp.set_value(current_temp) # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): + target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: self.char_target_temp.set_value(target_temp, should_callback=False) @@ -158,7 +170,9 @@ class Thermostat(HomeAccessory): # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) - if cooling_thresh: + if isinstance(cooling_thresh, (int, float)): + cooling_thresh = temperature_to_homekit(cooling_thresh, + self._unit) if not self.coolingthresh_flag_target_state: self.char_cooling_thresh_temp.set_value( cooling_thresh, should_callback=False) @@ -167,18 +181,17 @@ class Thermostat(HomeAccessory): # Update heating threshold temperature if characteristic exists if self.char_heating_thresh_temp: heating_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_LOW) - if heating_thresh: + if isinstance(heating_thresh, (int, float)): + heating_thresh = temperature_to_homekit(heating_thresh, + self._unit) if not self.heatingthresh_flag_target_state: self.char_heating_thresh_temp.set_value( heating_thresh, should_callback=False) self.heatingthresh_flag_target_state = False # Update display units - display_units = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if display_units \ - and display_units in UNIT_HASS_TO_HOMEKIT: - self.char_display_units.set_value( - UNIT_HASS_TO_HOMEKIT[display_units]) + if self._unit and self._unit in UNIT_HASS_TO_HOMEKIT: + self.char_display_units.set_value(UNIT_HASS_TO_HOMEKIT[self._unit]) # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index f18eb2273db..2fa2ebd396a 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE) + ATTR_CODE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv +import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID _LOGGER = logging.getLogger(__name__) @@ -44,3 +45,21 @@ def show_setup_message(bridge, hass): def dismiss_setup_message(hass): """Dismiss persistent notification and remove QR code.""" hass.components.persistent_notification.dismiss(HOMEKIT_NOTIFY_ID) + + +def convert_to_float(state): + """Return float of state, catch errors.""" + try: + return float(state) + except (ValueError, TypeError): + return None + + +def temperature_to_homekit(temperature, unit): + """Convert temperature to Celsius for HomeKit.""" + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) + + +def temperature_to_states(temperature, unit): + """Convert temperature back from Celsius to Home Assistant unit.""" + return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index ee7baae2755..e29ed85b5fc 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -16,6 +16,20 @@ _LOGGER = logging.getLogger(__name__) CONFIG = {} +def test_get_accessory_invalid_aid(caplog): + """Test with unsupported component.""" + assert get_accessory(None, State('light.demo', 'on'), + aid=None, config=None) is None + assert caplog.records[0].levelname == 'WARNING' + assert 'invalid aid' in caplog.records[0].msg + + +def test_not_supported(): + """Test if none is returned if entity isn't supported.""" + assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \ + is None + + class TestGetAccessories(unittest.TestCase): """Methods to test the get_accessory method.""" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 551dfc6780d..c04c250613d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,32 +3,13 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, calc_temperature, calc_humidity) + TemperatureSensor, HumiditySensor) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant -def test_calc_temperature(): - """Test if temperature in Celsius is calculated correctly.""" - assert calc_temperature(STATE_UNKNOWN) is None - assert calc_temperature('test') is None - - assert calc_temperature('20') == 20 - assert calc_temperature('20.12', TEMP_CELSIUS) == 20.12 - assert calc_temperature('75.2', TEMP_FAHRENHEIT) == 24 - - -def test_calc_humidity(): - """Test if humidity is a integer.""" - assert calc_humidity(STATE_UNKNOWN) is None - assert calc_humidity('test') is None - - assert calc_humidity('20') == 20 - assert calc_humidity('75.2') == 75.2 - - class TestHomekitSensors(unittest.TestCase): """Test class for all accessory types regarding sensors.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 6505bf72efb..011fe73377d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -10,7 +10,7 @@ from homeassistant.components.homekit.type_thermostats import ( Thermostat, STATE_OFF) from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -238,3 +238,42 @@ class TestHomekitThermostats(unittest.TestCase): self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + + def test_thermostat_fahrenheit(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + acc = Thermostat(self.hass, climate, 'Climate', True) + acc.run() + + self.hass.states.set(climate, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + self.hass.block_till_done() + self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) + self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) + self.assertEqual(acc.char_current_temp.value, 23.0) + self.assertEqual(acc.char_target_temp.value, 22.0) + self.assertEqual(acc.char_display_units.value, 1) + + # Set from HomeKit + acc.char_cooling_thresh_temp.set_value(23) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + + acc.char_heating_thresh_temp.set_value(22) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) + self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + + acc.char_target_temp.set_value(24.0) + self.hass.block_till_done() + service_data = self.events[-1].data[ATTR_SERVICE_DATA] + self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f95db9a4a13..d6ef5856f85 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -7,13 +7,15 @@ from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, ATTR_CODE) + show_setup_message, dismiss_setup_message, convert_to_float, + temperature_to_homekit, temperature_to_states, ATTR_CODE) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA) + EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, + TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -81,3 +83,20 @@ class TestUtil(unittest.TestCase): self.assertEqual( data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), HOMEKIT_NOTIFY_ID) + + def test_convert_to_float(self): + """Test convert_to_float method.""" + self.assertEqual(convert_to_float(12), 12) + self.assertEqual(convert_to_float(12.4), 12.4) + self.assertIsNone(convert_to_float(STATE_UNKNOWN)) + self.assertIsNone(convert_to_float(None)) + + def test_temperature_to_homekit(self): + """Test temperature conversion from HA to HomeKit.""" + self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5) + self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4) + + def test_temperature_to_states(self): + """Test temperature conversion from HomeKit to HA.""" + self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0) + self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4) From e04b01daad3b25335fc9479e1552e44915c9fc7f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 04:50:29 +0200 Subject: [PATCH 205/924] Validate basic customize entries (#13478) * Added schema to validate customize dictionary * Added test --- homeassistant/config.py | 13 ++++++++++--- tests/test_config.py | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 58cfe845e8f..53e611ac725 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -13,6 +13,7 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, @@ -129,13 +130,19 @@ PACKAGES_CONFIG_SCHEMA = vol.Schema({ {cv.slug: vol.Any(dict, list, None)}) # Only slugs for component names }) +CUSTOMIZE_DICT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_FRIENDLY_NAME): cv.string, + vol.Optional(ATTR_HIDDEN): cv.boolean, + vol.Optional(ATTR_ASSUMED_STATE): cv.boolean, +}, extra=vol.ALLOW_EXTRA) + CUSTOMIZE_CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_CUSTOMIZE, default={}): - vol.Schema({cv.entity_id: dict}), + vol.Schema({cv.entity_id: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): - vol.Schema({cv.string: dict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): - vol.Schema({cv.string: OrderedDict}), + vol.Schema({cv.string: CUSTOMIZE_DICT_SCHEMA}), }) CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ diff --git a/tests/test_config.py b/tests/test_config.py index aaa793f91a9..22fcebc6ea4 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -12,6 +12,7 @@ from voluptuous import MultipleInvalid from homeassistant.core import DOMAIN, HomeAssistantError, Config import homeassistant.config as config_util from homeassistant.const import ( + ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIT_SYSTEM, CONF_NAME, CONF_TIME_ZONE, CONF_ELEVATION, CONF_CUSTOMIZE, __version__, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT) @@ -235,6 +236,29 @@ class TestConfig(unittest.TestCase): }, }) + def test_customize_dict_schema(self): + """Test basic customize config validation.""" + values = ( + {ATTR_FRIENDLY_NAME: None}, + {ATTR_HIDDEN: '2'}, + {ATTR_ASSUMED_STATE: '2'}, + ) + + for val in values: + print(val) + with pytest.raises(MultipleInvalid): + config_util.CUSTOMIZE_DICT_SCHEMA(val) + + assert config_util.CUSTOMIZE_DICT_SCHEMA({ + ATTR_FRIENDLY_NAME: 2, + ATTR_HIDDEN: '1', + ATTR_ASSUMED_STATE: '0', + }) == { + ATTR_FRIENDLY_NAME: '2', + ATTR_HIDDEN: True, + ATTR_ASSUMED_STATE: False + } + def test_customize_glob_is_ordered(self): """Test that customize_glob preserves order.""" conf = config_util.CORE_CONFIG_SCHEMA( From b0073b437f04510a305286d54e1eeb989bee8518 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 27 Mar 2018 23:39:25 +0200 Subject: [PATCH 206/924] Homekit: Fix security systems (#13499) * Fix alarm_code=None * Added test --- .../components/homekit/type_security_systems.py | 4 +++- .../homekit/test_type_security_systems.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 146fca95b53..b23522f0ea2 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -58,7 +58,9 @@ class SecuritySystem(HomeAccessory): hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id, ATTR_CODE: self._alarm_code} + params = {ATTR_ENTITY_ID: self._entity_id} + if self._alarm_code: + params[ATTR_CODE] = self._alarm_code self._hass.services.call('alarm_control_panel', service, params) def update_state(self, entity_id=None, old_state=None, new_state=None): diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 4d61fc4a44c..c689a73bac2 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -102,3 +102,19 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 3) + + def test_no_alarm_code(self): + """Test accessory if security_system doesn't require a alarm_code.""" + acp = 'alarm_control_panel.test' + + acc = SecuritySystem(self.hass, acp, 'SecuritySystem', + alarm_code=None, aid=2) + acc.run() + + # Set from HomeKit + acc.char_target_state.set_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) From 26fb3d7faa2c4eb485b5450fb5838c41128d66dd Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 29 Mar 2018 00:55:05 +0200 Subject: [PATCH 207/924] python-miio version bumped (Closes: 13449) (#13511) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 09df55200a2..3e6aee6ba3a 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index a21c86f49c0..999d0f7f0e6 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 91f753391fc..13ec9c873b1 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index cb172735ac4..33ba5793fe0 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 9f0f163df69..27c3c4c72f1 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index f42a895f94f..887a50fdcce 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.8'] +REQUIREMENTS = ['python-miio==0.3.9'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index df81dae9f00..b900328cab5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -948,7 +948,7 @@ python-juicenet==0.0.5 # homeassistant.components.sensor.xiaomi_miio # homeassistant.components.switch.xiaomi_miio # homeassistant.components.vacuum.xiaomi_miio -python-miio==0.3.8 +python-miio==0.3.9 # homeassistant.components.media_player.mpd python-mpd2==0.5.5 From e993d095cbff9976f45141944054d39b44ceaf1c Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 30 Mar 2018 02:10:56 +0200 Subject: [PATCH 208/924] Fix mysensors light supported features (#13512) * Different types of light should have different supported features. --- homeassistant/components/light/mysensors.py | 29 ++++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 14a770b7632..7aa1e754c43 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -12,8 +12,7 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.util.color import rgb_hex_to_rgb_list import homeassistant.util.color as color_util -SUPPORT_MYSENSORS = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR | - SUPPORT_WHITE_VALUE) +SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE def setup_platform(hass, config, add_devices, discovery_info=None): @@ -64,11 +63,6 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): """Return true if device is on.""" return self._state - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORT_MYSENSORS - def _turn_on_light(self): """Turn on light child device.""" set_req = self.gateway.const.SetReq @@ -171,6 +165,11 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): class MySensorsLightDimmer(MySensorsLight): """Dimmer child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -188,6 +187,14 @@ class MySensorsLightDimmer(MySensorsLight): class MySensorsLightRGB(MySensorsLight): """RGB child class to MySensorsLight.""" + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_COLOR + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() @@ -209,6 +216,14 @@ class MySensorsLightRGBW(MySensorsLightRGB): # pylint: disable=too-many-ancestors + @property + def supported_features(self): + """Flag supported features.""" + set_req = self.gateway.const.SetReq + if set_req.V_DIMMER in self._values: + return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW + return SUPPORT_MYSENSORS_RGBW + def turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() From dfd15900c76de6c286a6a7555cd19ddc5f6f32eb Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 29 Mar 2018 20:10:27 -0400 Subject: [PATCH 209/924] Fix Insteon Leak Sensor (#13515) * update leak sensor * Fix error when insteon state type is unknown * Bump insteon version to 0.8.3 * Update requirements all and test * Fix requirements conflicts due to lack of commit sync * Requirements sync * Rerun script/gen_requirements_all.py * Try requirements update again * Update requirements --- .../components/binary_sensor/insteon_plm.py | 16 +++++----- homeassistant/components/insteon_plm.py | 31 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 09c4b5c8ea7..06079d6aa3b 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = {'openClosedSensor': 'opening', 'motionSensor': 'motion', 'doorSensor': 'door', - 'leakSensor': 'moisture'} + 'wetLeakSensor': 'moisture'} @asyncio.coroutine @@ -28,13 +28,14 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): address = discovery_info['address'] device = plm.devices[address] state_key = discovery_info['state_key'] + name = device.states[state_key].name + if name != 'dryLeakSensor': + _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', + device.address.hex, device.states[state_key].name) - _LOGGER.debug('Adding device %s entity %s to Binary Sensor platform', - device.address.hex, device.states[state_key].name) + new_entity = InsteonPLMBinarySensor(device, state_key) - new_entity = InsteonPLMBinarySensor(device, state_key) - - async_add_devices([new_entity]) + async_add_devices([new_entity]) class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @@ -53,5 +54,4 @@ class InsteonPLMBinarySensor(InsteonPLMEntity, BinarySensorDevice): @property def is_on(self): """Return the boolean response if the node is on.""" - sensorstate = self._insteon_device_state.value - return bool(sensorstate) + return bool(self._insteon_device_state.value) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 2381e3db69e..6f5c5223ea0 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.2'] +REQUIREMENTS = ['insteonplm==0.8.3'] _LOGGER = logging.getLogger(__name__) @@ -64,19 +64,20 @@ def async_setup(hass, config): """Detect device from transport to be delegated to platform.""" for state_key in device.states: platform_info = ipdb[device.states[state_key]] - platform = platform_info.platform - if platform is not None: - _LOGGER.info("New INSTEON PLM device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform) + if platform_info: + platform = platform_info.platform + if platform: + _LOGGER.info("New INSTEON PLM device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform) - hass.async_add_job( - discovery.async_load_platform( - hass, platform, DOMAIN, - discovered={'address': device.address.hex, - 'state_key': state_key}, - hass_config=config)) + hass.async_add_job( + discovery.async_load_platform( + hass, platform, DOMAIN, + discovered={'address': device.address.hex, + 'state_key': state_key}, + hass_config=config)) _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( @@ -127,13 +128,15 @@ class IPDB(object): from insteonplm.states.sensor import (VariableSensor, OnOffSensor, SmokeCO2Sensor, - IoLincSensor) + IoLincSensor, + LeakSensorDryWet) self.states = [State(OnOffSwitch_OutletTop, 'switch'), State(OnOffSwitch_OutletBottom, 'switch'), State(OpenClosedRelay, 'switch'), State(OnOffSwitch, 'switch'), + State(LeakSensorDryWet, 'binary_sensor'), State(IoLincSensor, 'binary_sensor'), State(SmokeCO2Sensor, 'sensor'), State(OnOffSensor, 'binary_sensor'), diff --git a/requirements_all.txt b/requirements_all.txt index b900328cab5..a72d33e35ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.2 +insteonplm==0.8.3 # homeassistant.components.verisure jsonpath==0.75 From 0428559f694e4ec67061ccc087d13e2bc688c786 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 29 Mar 2018 18:35:57 +0200 Subject: [PATCH 210/924] HomeKit: Fix setting light brightness (#13518) * Added test --- .../components/homekit/type_lights.py | 21 +++++--- tests/components/homekit/test_type_lights.py | 50 ++++++++++++------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 2415bb1a4df..d88e7100131 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -32,6 +32,7 @@ class Light(HomeAccessory): self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} + self._state = 0 self.chars = [] self._features = self._hass.states.get(self._entity_id) \ @@ -47,7 +48,7 @@ class Light(HomeAccessory): serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) self.char_on = serv_light.get_characteristic(CHAR_ON) self.char_on.setter_callback = self.set_state - self.char_on.value = 0 + self.char_on.value = self._state if CHAR_BRIGHTNESS in self.chars: self.char_brightness = serv_light \ @@ -66,7 +67,7 @@ class Light(HomeAccessory): def set_state(self, value): """Set state if call came from HomeKit.""" - if self._flag[CHAR_BRIGHTNESS]: + if self._state == value: return _LOGGER.debug('%s: Set state to %d', self._entity_id, value) @@ -83,8 +84,11 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) self._flag[CHAR_BRIGHTNESS] = True self.char_brightness.set_value(value, should_callback=False) - self._hass.components.light.turn_on( - self._entity_id, brightness_pct=value) + if value != 0: + self._hass.components.light.turn_on( + self._entity_id, brightness_pct=value) + else: + self._hass.components.light.turn_off(self._entity_id) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -121,10 +125,11 @@ class Light(HomeAccessory): # Handle State state = new_state.state - if not self._flag[CHAR_ON] and state in [STATE_ON, STATE_OFF] and \ - self.char_on.value != (state == STATE_ON): - self.char_on.set_value(state == STATE_ON, should_callback=False) - self._flag[CHAR_ON] = False + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ON] and self.char_on.value != self._state: + self.char_on.set_value(self._state, should_callback=False) + self._flag[CHAR_ON] = False # Handle Brightness if CHAR_BRIGHTNESS in self.chars: diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index b4d4d5a5945..ee1900fd7c5 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -57,19 +57,21 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_on.value, 0) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - acc.char_on.set_value(False) + self.hass.states.set(entity_id, STATE_ON) + self.hass.block_till_done() + + acc.char_on.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) + + self.hass.states.set(entity_id, STATE_OFF) self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) # Remove entity self.hass.states.remove(entity_id) @@ -95,15 +97,27 @@ class TestHomekitLights(unittest.TestCase): acc.char_brightness.set_value(20) acc.char_on.set_value(1) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - print(self.events[0].data) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + acc.char_on.set_value(1) + acc.char_brightness.set_value(40) + self.hass.block_till_done() + self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + + acc.char_on.set_value(1) + acc.char_brightness.set_value(0) + self.hass.block_till_done() + self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -123,10 +137,8 @@ class TestHomekitLights(unittest.TestCase): acc.char_hue.set_value(145) acc.char_saturation.set_value(75) self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) self.assertEqual( self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) From 867010240accca53ad706f9878484af17328f138 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 30 Mar 2018 02:12:11 +0200 Subject: [PATCH 211/924] Construct version pinned (#13528) * Construct added to the requirements * requirements_all.txt updated --- homeassistant/components/climate/eq3btsmart.py | 2 +- homeassistant/components/fan/xiaomi_miio.py | 2 +- homeassistant/components/light/xiaomi_miio.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- .../components/sensor/eddystone_temperature.py | 2 +- homeassistant/components/sensor/xiaomi_miio.py | 2 +- homeassistant/components/switch/xiaomi_miio.py | 2 +- homeassistant/components/vacuum/xiaomi_miio.py | 2 +- requirements_all.txt | 10 ++++++++++ 9 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/eq3btsmart.py b/homeassistant/components/climate/eq3btsmart.py index 5c0a3530006..820e715b00d 100644 --- a/homeassistant/components/climate/eq3btsmart.py +++ b/homeassistant/components/climate/eq3btsmart.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_MAC, CONF_DEVICES, TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_HALVES) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-eq3bt==0.1.9'] +REQUIREMENTS = ['python-eq3bt==0.1.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 3e6aee6ba3a..4df85711cfd 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -29,7 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_TEMPERATURE = 'temperature' ATTR_HUMIDITY = 'humidity' diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 999d0f7f0e6..21a27c33203 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.candle2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] # The light does not accept cct values < 1 CCT_MIN = 1 diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index 13ec9c873b1..b71eb2cb447 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -22,7 +22,7 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index fb5fa2c1fba..06accb26eb6 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['beacontools[scan]==1.2.1'] +REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/xiaomi_miio.py b/homeassistant/components/sensor/xiaomi_miio.py index 33ba5793fe0..066dc384007 100644 --- a/homeassistant/components/sensor/xiaomi_miio.py +++ b/homeassistant/components/sensor/xiaomi_miio.py @@ -25,7 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_CHARGING = 'charging' diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 27c3c4c72f1..6110b6dc469 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -37,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'chuangmi.plug.v2']), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] ATTR_POWER = 'power' ATTR_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index 887a50fdcce..b2451ed495c 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TOKEN, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a72d33e35ec..7ac9bd5fd7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -189,6 +189,16 @@ colorlog==3.1.2 # homeassistant.components.binary_sensor.concord232 concord232==0.15 +# homeassistant.components.climate.eq3btsmart +# homeassistant.components.fan.xiaomi_miio +# homeassistant.components.light.xiaomi_miio +# homeassistant.components.remote.xiaomi_miio +# homeassistant.components.sensor.eddystone_temperature +# homeassistant.components.sensor.xiaomi_miio +# homeassistant.components.switch.xiaomi_miio +# homeassistant.components.vacuum.xiaomi_miio +construct==2.9.41 + # homeassistant.scripts.credstash # credstash==1.14.0 From 32b0712089e9c52a6b840dd1d177f96627d39d2b Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 02:13:08 +0200 Subject: [PATCH 212/924] Don't add Falsy items to list #13412 (#13536) --- homeassistant/config.py | 4 ++++ tests/test_config.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/config.py b/homeassistant/config.py index 53e611ac725..28936ae12e9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -554,6 +554,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): continue if hasattr(component, 'PLATFORM_SCHEMA'): + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue @@ -562,6 +564,8 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): merge_type, _ = _identify_config_schema(component) if merge_type == 'list': + if not comp_conf: + continue # Ensure we dont add Falsy items to list config[comp_name] = cv.ensure_list(config.get(comp_name)) config[comp_name].extend(cv.ensure_list(comp_conf)) continue diff --git a/tests/test_config.py b/tests/test_config.py index 22fcebc6ea4..652b931366a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -592,6 +592,25 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None +def test_merge_try_falsy(merge_log_err): + """Ensure we dont add falsy items like empty OrderedDict() to list.""" + packages = { + 'pack_falsy_to_lst': {'automation': OrderedDict()}, + 'pack_list2': {'light': OrderedDict()}, + } + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'automation': {'do': 'something'}, + 'light': {'some': 'light'}, + } + config_util.merge_packages_config(config, packages) + + assert merge_log_err.call_count == 0 + assert len(config) == 3 + assert len(config['automation']) == 1 + assert len(config['light']) == 1 + + def test_merge_new(merge_log_err): """Test adding new components to outer scope.""" packages = { From f26aff48859ed11a04acd22dfeaf34741d6f5aa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 17:21:48 -0700 Subject: [PATCH 213/924] Version bump to 0.66.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 58c49289989..ccb75634601 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0.b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5908b55bbabc5803833cc9ec88cbc17e8c2e48f1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 18:01:47 -0700 Subject: [PATCH 214/924] Fix merge conflict --- homeassistant/const.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0ac3899cba6..d286aa85458 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,13 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -<<<<<<< HEAD MINOR_VERSION = 67 PATCH_VERSION = '0.dev0' -======= -MINOR_VERSION = 66 -PATCH_VERSION = '0b3' ->>>>>>> origin/rc __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From df78eecc1b689bcba705cdd5ae60de5515f3251e Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 30 Mar 2018 02:10:20 +0100 Subject: [PATCH 215/924] Adds folder_watcher component (#12918) * Create watchdog_file_watcher.py * Rename watchdog_file_watcher.py to folder_watcher.py * Address a number of issues * Adds filter * Adds pattern matching * Adds create_event_handler() * Update folder_watcher.py * Adds run_setup() * Remove stop_watching() * Adds shutdown() * Update config to allow patterns on each folder * Update to patterns from filters * Adds watchdog * Fix indents on schema * Update folder_watcher.py * Create test_file_watcher.py * Fix lints * Add test_invalid_path() * Adds folder_watcher * Update test_file_watcher.py * Update folder_watcher.py * Simplify config * Adapt for new config * Run observer.schedule() on EVENT_HOMEASSISTANT_START * Amend Watcher removing entity and tidying startup * Tidy config * Rename process to on_any_event for consistency * Rename on_any_event back to process Using `on_any_event` resulted in 2 events being fired * Update folder_watcher.py * Fix return False on setup * Update test_file_watcher.py * Update folder_watcher.py * Adds watchdog * Undo adding watchdog * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Update test_file_watcher.py * Add event * Update test_file_watcher.py * Update .coveragerc * Update test_file_watcher.py * Update test_file_watcher.py * debug + join * test event * lint * lint * Rename test_file_watcher.py to test_folder_watcher.py * hound * Tidy test * Further refine test * Adds to test_all * Fix test for py35 * Change test again * Update test_folder_watcher.py * Fix test * Add watchdog to test * Update folder_watcher.py * add watchdog * Update folder_watcher.py --- .coveragerc | 1 + homeassistant/components/folder_watcher.py | 111 +++++++++++++++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/test_folder_watcher.py | 64 ++++++++++++ 6 files changed, 183 insertions(+) create mode 100644 homeassistant/components/folder_watcher.py create mode 100644 tests/components/test_folder_watcher.py diff --git a/.coveragerc b/.coveragerc index a2c0dde77b1..d6cc126ef52 100644 --- a/.coveragerc +++ b/.coveragerc @@ -402,6 +402,7 @@ omit = homeassistant/components/fan/mqtt.py homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py + homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py homeassistant/components/goalfeed.py homeassistant/components/ifttt.py diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py new file mode 100644 index 00000000000..011ae892bc5 --- /dev/null +++ b/homeassistant/components/folder_watcher.py @@ -0,0 +1,111 @@ +""" +Component for monitoring activity on a folder. + +For more details about this platform, refer to the documentation at +https://home-assistant.io/components/folder_watcher/ +""" +import os +import logging +import voluptuous as vol +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['watchdog==0.8.3'] +_LOGGER = logging.getLogger(__name__) + +CONF_FOLDER = 'folder' +CONF_PATTERNS = 'patterns' +CONF_WATCHERS = 'watchers' +DEFAULT_PATTERN = '*' +DOMAIN = "folder_watcher" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_FOLDER): cv.isdir, + vol.Optional(CONF_PATTERNS, default=[DEFAULT_PATTERN]): + vol.All(cv.ensure_list, [cv.string]), + })]) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the folder watcher.""" + conf = config[DOMAIN] + for watcher in conf: + path = watcher[CONF_FOLDER] + patterns = watcher[CONF_PATTERNS] + if not hass.config.is_allowed_path(path): + _LOGGER.error("folder %s is not valid or allowed", path) + return False + Watcher(path, patterns, hass) + + return True + + +def create_event_handler(patterns, hass): + """"Return the Watchdog EventHandler object.""" + from watchdog.events import PatternMatchingEventHandler + + class EventHandler(PatternMatchingEventHandler): + """Class for handling Watcher events.""" + + def __init__(self, patterns, hass): + """Initialise the EventHandler.""" + super().__init__(patterns) + self.hass = hass + + def process(self, event): + """On Watcher event, fire HA event.""" + _LOGGER.debug("process(%s)", event) + if not event.is_directory: + folder, file_name = os.path.split(event.src_path) + self.hass.bus.fire( + DOMAIN, { + "event_type": event.event_type, + 'path': event.src_path, + 'file': file_name, + 'folder': folder, + }) + + def on_modified(self, event): + """File modified.""" + self.process(event) + + def on_moved(self, event): + """File moved.""" + self.process(event) + + def on_created(self, event): + """File created.""" + self.process(event) + + def on_deleted(self, event): + """File deleted.""" + self.process(event) + + return EventHandler(patterns, hass) + + +class Watcher(): + """Class for starting Watchdog.""" + + def __init__(self, path, patterns, hass): + """Initialise the watchdog observer.""" + from watchdog.observers import Observer + self._observer = Observer() + self._observer.schedule( + create_event_handler(patterns, hass), + path, + recursive=True) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, self.startup) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, self.shutdown) + + def startup(self, event): + """Start the watcher.""" + self._observer.start() + + def shutdown(self, event): + """Shutdown the watcher.""" + self._observer.stop() + self._observer.join() diff --git a/requirements_all.txt b/requirements_all.txt index e79249151b1..d983028cead 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1279,6 +1279,9 @@ waqiasync==1.0.0 # homeassistant.components.cloud warrant==0.6.1 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + # homeassistant.components.waterfurnace waterfurnace==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8a57488d80..6630c09c1c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -199,5 +199,8 @@ wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + # homeassistant.components.sensor.yahoo_finance yahoo-finance==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d8fc7b1ed60..1f5348136c6 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -90,6 +90,7 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', + 'watchdog', 'vultr' ) diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py new file mode 100644 index 00000000000..587d8b7ad6d --- /dev/null +++ b/tests/components/test_folder_watcher.py @@ -0,0 +1,64 @@ +"""The tests for the folder_watcher component.""" +import unittest +from unittest.mock import MagicMock +import os + +from homeassistant.components import folder_watcher +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + +CWD = os.path.join(os.path.dirname(__file__)) +FILE = 'file.txt' + + +class TestFolderWatcher(unittest.TestCase): + """Test the file_watcher component.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.whitelist_external_dirs = set((CWD)) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_path_setup(self): + """Test that a invalid path is not setup.""" + config = { + folder_watcher.DOMAIN: [{ + folder_watcher.CONF_FOLDER: 'invalid_path' + }] + } + self.assertFalse( + setup_component(self.hass, folder_watcher.DOMAIN, config)) + + def test_valid_path_setup(self): + """Test that a valid path is setup.""" + config = { + folder_watcher.DOMAIN: [{folder_watcher.CONF_FOLDER: CWD}] + } + + self.assertTrue(setup_component( + self.hass, folder_watcher.DOMAIN, config)) + + def test_event(self): + """Check that HASS events are fired correctly on watchdog event.""" + from watchdog.events import FileModifiedEvent + + # Cant use setup_component as need to retrieve Watcher object. + w = folder_watcher.Watcher(CWD, + folder_watcher.DEFAULT_PATTERN, + self.hass) + w.startup(None) + + self.hass.bus.fire = MagicMock() + + # Trigger a fake filesystem event through the Watcher Observer emitter. + (emitter,) = w._observer.emitters + emitter.queue_event(FileModifiedEvent(FILE)) + + # Wait for the event to propagate. + self.hass.block_till_done() + + assert self.hass.bus.fire.called From 8617177ff136d8ef8bce69165dbd4010efafd331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85skar=20Andersson?= Date: Fri, 30 Mar 2018 04:45:25 +0200 Subject: [PATCH 216/924] Update rflink to 0.0.37 (#12603) * Update requirements_all.txt * Update rflink.py --- homeassistant/components/rflink.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rflink.py b/homeassistant/components/rflink.py index 439f938beb3..87e2a7a2331 100644 --- a/homeassistant/components/rflink.py +++ b/homeassistant/components/rflink.py @@ -22,7 +22,7 @@ from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['rflink==0.0.34'] +REQUIREMENTS = ['rflink==0.0.37'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d983028cead..b21452dc385 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1085,7 +1085,7 @@ regenmaschine==0.4.1 restrictedpython==4.0b2 # homeassistant.components.rflink -rflink==0.0.34 +rflink==0.0.37 # homeassistant.components.ring ring_doorbell==0.1.8 From 3e5462ebff2dfa8fe03e4c0c3a78f8fd2fad6049 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Fri, 30 Mar 2018 04:47:49 +0200 Subject: [PATCH 217/924] Added file path validity checks to file sensor (#12505) * Added file validity checks to file sensor * Patched out 'is_allowed_path' for file sensor tests --- homeassistant/components/sensor/file.py | 9 ++++++--- tests/components/sensor/test_file.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/file.py b/homeassistant/components/sensor/file.py index afa305a0fb0..cbdd4eef227 100644 --- a/homeassistant/components/sensor/file.py +++ b/homeassistant/components/sensor/file.py @@ -25,7 +25,7 @@ DEFAULT_NAME = 'File' ICON = 'mdi:file' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_FILE_PATH): cv.string, + vol.Required(CONF_FILE_PATH): cv.isfile, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -43,8 +43,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if value_template is not None: value_template.hass = hass - async_add_devices( - [FileSensor(name, file_path, unit, value_template)], True) + if hass.config.is_allowed_path(file_path): + async_add_devices( + [FileSensor(name, file_path, unit, value_template)], True) + else: + _LOGGER.error("'%s' is not a whitelisted directory", file_path) class FileSensor(Entity): diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py index aa048f7a62e..7171289de69 100644 --- a/tests/components/sensor/test_file.py +++ b/tests/components/sensor/test_file.py @@ -18,6 +18,8 @@ class TestFileSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + # Patch out 'is_allowed_path' as the mock files aren't allowed + self.hass.config.is_allowed_path = Mock(return_value=True) mock_registry(self.hass) def teardown_method(self, method): From 507c658fe9947be064c2e4188b22998efdec8fb1 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 04:57:19 +0200 Subject: [PATCH 218/924] Check whitelisted paths #13107 (#13154) --- homeassistant/core.py | 10 +++++++--- tests/test_core.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 65db82a1fbe..feb8d331ae8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1060,15 +1060,19 @@ class Config(object): """Check if the path is valid for access from outside.""" assert path is not None - parent = pathlib.Path(path) + thepath = pathlib.Path(path) try: - parent = parent.resolve() # pylint: disable=no-member + # The file path does not have to exist (it's parent should) + if thepath.exists(): + thepath = thepath.resolve() + else: + thepath = thepath.parent.resolve() except (FileNotFoundError, RuntimeError, PermissionError): return False for whitelisted_path in self.whitelist_external_dirs: try: - parent.relative_to(whitelisted_path) + thepath.relative_to(whitelisted_path) return True except ValueError: pass diff --git a/tests/test_core.py b/tests/test_core.py index 7a1610c0966..1fcd9416f36 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -809,7 +809,8 @@ class TestConfig(unittest.TestCase): valid = [ test_file, - tmp_dir + tmp_dir, + os.path.join(tmp_dir, 'notfound321') ] for path in valid: assert self.config.is_allowed_path(path) From 170763ef2ff4c8291417873ec9b9a0547ffe1fe6 Mon Sep 17 00:00:00 2001 From: Andy Castille Date: Thu, 29 Mar 2018 22:00:26 -0500 Subject: [PATCH 219/924] Allow for overriding the DoorBird push notification URL in configuration (#13268) * Allow for overriding the DoorBird push notification URL in configuration * rename override config key --- homeassistant/components/doorbird.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird.py b/homeassistant/components/doorbird.py index 34758023f60..48f229b49ca 100644 --- a/homeassistant/components/doorbird.py +++ b/homeassistant/components/doorbird.py @@ -22,6 +22,7 @@ DOMAIN = 'doorbird' API_URL = '/api/{}'.format(DOMAIN) CONF_DOORBELL_EVENTS = 'doorbell_events' +CONF_CUSTOM_URL = 'hass_url_override' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_DOORBELL_EVENTS): cv.boolean, + vol.Optional(CONF_CUSTOM_URL): cv.string, }) }, extra=vol.ALLOW_EXTRA) @@ -61,9 +63,17 @@ def setup(hass, config): # Provide an endpoint for the device to call to trigger events hass.http.register_view(DoorbirdRequestView()) + # Get the URL of this server + hass_url = hass.config.api.base_url + + # Override it if another is specified in the component configuration + if config[DOMAIN].get(CONF_CUSTOM_URL): + hass_url = config[DOMAIN].get(CONF_CUSTOM_URL) + _LOGGER.info("DoorBird will connect to this instance via %s", + hass_url) + # This will make HA the only service that gets doorbell events - url = '{}{}/{}'.format( - hass.config.api.base_url, API_URL, SENSOR_DOORBELL) + url = '{}{}/{}'.format(hass_url, API_URL, SENSOR_DOORBELL) device.reset_notifications() device.subscribe_notification(SENSOR_DOORBELL, url) From 1ae8b6ee08adecc66a3081d471fee9b9d2d48435 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 20:02:21 -0700 Subject: [PATCH 220/924] Fix requirements --- requirements_test_all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6630c09c1c4..31a7874409a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ pywebpush==1.6.0 restrictedpython==4.0b2 # homeassistant.components.rflink -rflink==0.0.34 +rflink==0.0.37 # homeassistant.components.ring ring_doorbell==0.1.8 From 184f2be83e3c3f3824efa566fa29a779a57832e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 29 Mar 2018 20:15:40 -0700 Subject: [PATCH 221/924] Convert Hue to always use config entries (#13034) --- homeassistant/components/discovery.py | 26 +- homeassistant/components/hue/__init__.py | 371 +++----------------- homeassistant/components/hue/bridge.py | 143 ++++++++ homeassistant/components/hue/config_flow.py | 235 +++++++++++++ homeassistant/components/hue/const.py | 6 + homeassistant/components/hue/errors.py | 14 + homeassistant/components/hue/strings.json | 5 +- homeassistant/config_entries.py | 2 +- tests/components/hue/conftest.py | 17 - tests/components/hue/test_bridge.py | 136 +++---- tests/components/hue/test_config_flow.py | 213 +++++++++-- tests/components/hue/test_init.py | 169 +++++++++ tests/components/hue/test_setup.py | 70 ---- tests/components/test_discovery.py | 34 +- 14 files changed, 914 insertions(+), 527 deletions(-) create mode 100644 homeassistant/components/hue/bridge.py create mode 100644 homeassistant/components/hue/config_flow.py create mode 100644 homeassistant/components/hue/const.py create mode 100644 homeassistant/components/hue/errors.py delete mode 100644 tests/components/hue/conftest.py create mode 100644 tests/components/hue/test_init.py delete mode 100644 tests/components/hue/test_setup.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index eb53782d698..b2aa5b890a8 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -13,6 +13,7 @@ import os import voluptuous as vol +from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv @@ -40,6 +41,10 @@ SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' +CONFIG_ENTRY_HANDLERS = { + SERVICE_HUE: 'hue', +} + SERVICE_HANDLERS = { SERVICE_HASS_IOS_APP: ('ios', None), SERVICE_NETGEAR: ('device_tracker', None), @@ -51,7 +56,6 @@ SERVICE_HANDLERS = { SERVICE_WINK: ('wink', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), - SERVICE_HUE: ('hue', None), SERVICE_DECONZ: ('deconz', None), SERVICE_DAIKIN: ('daikin', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), @@ -105,6 +109,20 @@ async def async_setup(hass, config): logger.info("Ignoring service: %s %s", service, info) return + discovery_hash = json.dumps([service, info], sort_keys=True) + if discovery_hash in already_discovered: + return + + already_discovered.add(discovery_hash) + + if service in CONFIG_ENTRY_HANDLERS: + await hass.config_entries.flow.async_init( + CONFIG_ENTRY_HANDLERS[service], + source=config_entries.SOURCE_DISCOVERY, + data=info + ) + return + comp_plat = SERVICE_HANDLERS.get(service) # We do not know how to handle this service. @@ -112,12 +130,6 @@ async def async_setup(hass, config): logger.info("Unknown service discovered: %s %s", service, info) return - discovery_hash = json.dumps([service, info], sort_keys=True) - if discovery_hash in already_discovered: - return - - already_discovered.add(discovery_hash) - logger.info("Found new service: %s %s", service, info) component, platform = comp_plat diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index b70021e0304..557a47f3e05 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -4,31 +4,23 @@ This component provides basic support for the Philips Hue system. For more details about this component, please refer to the documentation at https://home-assistant.io/components/hue/ """ -import asyncio -import json import ipaddress import logging -import os -import async_timeout import voluptuous as vol -from homeassistant.core import callback -from homeassistant.components.discovery import SERVICE_HUE from homeassistant.const import CONF_FILENAME, CONF_HOST -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery, aiohttp_client -from homeassistant import config_entries -from homeassistant.util.json import save_json +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN, API_NUPNP +from .bridge import HueBridge +# Loading the config flow file will register the flow +from .config_flow import configured_hosts REQUIREMENTS = ['aiohue==1.3.0'] _LOGGER = logging.getLogger(__name__) -DOMAIN = "hue" -SERVICE_HUE_SCENE = "hue_activate_scene" -API_NUPNP = 'https://www.meethue.com/api/nupnp' - CONF_BRIDGES = "bridges" CONF_ALLOW_UNREACHABLE = 'allow_unreachable' @@ -42,6 +34,7 @@ DEFAULT_ALLOW_HUE_GROUPS = True BRIDGE_CONFIG_SCHEMA = vol.Schema({ # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), + # This is for legacy reasons and is only used for importing auth. vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional(CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, @@ -56,19 +49,6 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) -ATTR_GROUP_NAME = "group_name" -ATTR_SCENE_NAME = "scene_name" -SCENE_SCHEMA = vol.Schema({ - vol.Required(ATTR_GROUP_NAME): cv.string, - vol.Required(ATTR_SCENE_NAME): cv.string, -}) - -CONFIG_INSTRUCTIONS = """ -Press the button on the bridge to register Philips Hue with Home Assistant. - -![Location of button on bridge](/static/images/config_philips_hue.jpg) -""" - async def async_setup(hass, config): """Set up the Hue platform.""" @@ -76,20 +56,8 @@ async def async_setup(hass, config): if conf is None: conf = {} - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - - async def async_bridge_discovered(service, discovery_info): - """Dispatcher for Hue discovery events.""" - # Ignore emulated hue - if "HASS Bridge" in discovery_info.get('name', ''): - return - - await async_setup_bridge( - hass, discovery_info['host'], - 'phue-{}.conf'.format(discovery_info['serial'])) - - discovery.async_listen(hass, SERVICE_HUE, async_bridge_discovered) + hass.data[DOMAIN] = {} + configured = configured_hosts(hass) # User has configured bridges if CONF_BRIDGES in conf: @@ -103,12 +71,19 @@ async def async_setup(hass, config): async with websession.get(API_NUPNP) as req: hosts = await req.json() - # Run through config schema to populate defaults - bridges = [BRIDGE_CONFIG_SCHEMA({ - CONF_HOST: entry['internalipaddress'], - CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), - }) for entry in hosts] + bridges = [] + for entry in hosts: + # Filter out already configured hosts + if entry['internalipaddress'] in configured: + continue + # Run through config schema to populate defaults + bridges.append(BRIDGE_CONFIG_SCHEMA({ + CONF_HOST: entry['internalipaddress'], + # Careful with using entry['id'] for other reasons. The + # value is in lowercase but is returned uppercase from hub. + CONF_FILENAME: '.hue_{}.conf'.format(entry['id']), + })) else: # Component not specified in config, we're loaded via discovery bridges = [] @@ -116,277 +91,43 @@ async def async_setup(hass, config): if not bridges: return True - await asyncio.wait([ - async_setup_bridge( - hass, bridge[CONF_HOST], bridge[CONF_FILENAME], - bridge[CONF_ALLOW_UNREACHABLE], bridge[CONF_ALLOW_HUE_GROUPS] - ) for bridge in bridges - ]) + for bridge_conf in bridges: + host = bridge_conf[CONF_HOST] + + # Store config in hass.data so the config entry can find it + hass.data[DOMAIN][host] = bridge_conf + + # If configured, the bridge will be set up during config entry phase + if host in configured: + continue + + # No existing config entry found, try importing it or trigger link + # config flow if no existing auth. Because we're inside the setup of + # this component we'll have to use hass.async_add_job to avoid a + # deadlock: creating a config entry will set up the component but the + # setup would block till the entry is created! + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + 'host': bridge_conf[CONF_HOST], + 'path': bridge_conf[CONF_FILENAME], + } + )) return True -async def async_setup_bridge( - hass, host, filename=None, - allow_unreachable=DEFAULT_ALLOW_UNREACHABLE, - allow_hue_groups=DEFAULT_ALLOW_HUE_GROUPS, - username=None): - """Set up a given Hue bridge.""" - assert filename or username, 'Need to pass at least a username or filename' - - # Only register a device once - if host in hass.data[DOMAIN]: - return - - if username is None: - username = await hass.async_add_job( - _find_username_from_config, hass, filename) - - bridge = HueBridge(host, hass, filename, username, allow_unreachable, - allow_hue_groups) - await bridge.async_setup() - - -def _find_username_from_config(hass, filename): - """Load username from config.""" - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - with open(path) as inp: - return list(json.load(inp).values())[0]['username'] - - -class HueBridge(object): - """Manages a single Hue bridge.""" - - def __init__(self, host, hass, filename, username, - allow_unreachable=False, allow_groups=True): - """Initialize the system.""" - self.host = host - self.hass = hass - self.filename = filename - self.username = username - self.allow_unreachable = allow_unreachable - self.allow_groups = allow_groups - self.available = True - self.config_request_id = None - self.api = None - - async def async_setup(self): - """Set up a phue bridge based on host parameter.""" - import aiohue - - api = aiohue.Bridge( - self.host, - username=self.username, - websession=aiohttp_client.async_get_clientsession(self.hass) - ) - - try: - with async_timeout.timeout(5): - # Initialize bridge and validate our username - if not self.username: - await api.create_user('home-assistant') - await api.initialize() - except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): - _LOGGER.warning("Connected to Hue at %s but not registered.", - self.host) - self.async_request_configuration() - return - except (asyncio.TimeoutError, aiohue.RequestError): - _LOGGER.error("Error connecting to the Hue bridge at %s", - self.host) - return - except aiohue.AiohueException: - _LOGGER.exception('Unknown Hue linking error occurred') - self.async_request_configuration() - return - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error connecting with Hue bridge at %s", - self.host) - return - - self.hass.data[DOMAIN][self.host] = self - - # If we came here and configuring this host, mark as done - if self.config_request_id: - request_id = self.config_request_id - self.config_request_id = None - self.hass.components.configurator.async_request_done(request_id) - - self.username = api.username - - # Save config file - await self.hass.async_add_job( - save_json, self.hass.config.path(self.filename), - {self.host: {'username': api.username}}) - - self.api = api - - self.hass.async_add_job(discovery.async_load_platform( - self.hass, 'light', DOMAIN, - {'host': self.host})) - - self.hass.services.async_register( - DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, - schema=SCENE_SCHEMA) - - @callback - def async_request_configuration(self): - """Request configuration steps from the user.""" - configurator = self.hass.components.configurator - - # We got an error if this method is called while we are configuring - if self.config_request_id: - configurator.async_notify_errors( - self.config_request_id, - "Failed to register, please try again.") - return - - async def config_callback(data): - """Callback for configurator data.""" - await self.async_setup() - - self.config_request_id = configurator.async_request_config( - "Philips Hue", config_callback, - description=CONFIG_INSTRUCTIONS, - entity_picture="/static/images/logo_philips_hue.png", - submit_caption="I have pressed the button" - ) - - async def hue_activate_scene(self, call, updated=False): - """Service to call directly into bridge to set scenes.""" - group_name = call.data[ATTR_GROUP_NAME] - scene_name = call.data[ATTR_SCENE_NAME] - - group = next( - (group for group in self.api.groups.values() - if group.name == group_name), None) - - scene_id = next( - (scene.id for scene in self.api.scenes.values() - if scene.name == scene_name), None) - - # If we can't find it, fetch latest info. - if not updated and (group is None or scene_id is None): - await self.api.groups.update() - await self.api.scenes.update() - await self.hue_activate_scene(call, updated=True) - return - - if group is None: - _LOGGER.warning('Unable to find group %s', group_name) - return - - if scene_id is None: - _LOGGER.warning('Unable to find scene %s', scene_name) - return - - await group.set_action(scene=scene_id) - - -@config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(config_entries.ConfigFlowHandler): - """Handle a Hue config flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize the Hue flow.""" - self.host = None - - @property - def _websession(self): - """Return a websession. - - Cannot assign in init because hass variable is not set yet. - """ - return aiohttp_client.async_get_clientsession(self.hass) - - async def async_step_init(self, user_input=None): - """Handle a flow start.""" - from aiohue.discovery import discover_nupnp - - if user_input is not None: - self.host = user_input['host'] - return await self.async_step_link() - - try: - with async_timeout.timeout(5): - bridges = await discover_nupnp(websession=self._websession) - except asyncio.TimeoutError: - return self.async_abort( - reason='discover_timeout' - ) - - if not bridges: - return self.async_abort( - reason='no_bridges' - ) - - # Find already configured hosts - configured_hosts = set( - entry.data['host'] for entry - in self.hass.config_entries.async_entries(DOMAIN)) - - hosts = [bridge.host for bridge in bridges - if bridge.host not in configured_hosts] - - if not hosts: - return self.async_abort( - reason='all_configured' - ) - - elif len(hosts) == 1: - self.host = hosts[0] - return await self.async_step_link() - - return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - vol.Required('host'): vol.In(hosts) - }) - ) - - async def async_step_link(self, user_input=None): - """Attempt to link with the Hue bridge.""" - import aiohue - errors = {} - - if user_input is not None: - bridge = aiohue.Bridge(self.host, websession=self._websession) - try: - with async_timeout.timeout(5): - # Create auth token - await bridge.create_user('home-assistant') - # Fetches name and id - await bridge.initialize() - except (asyncio.TimeoutError, aiohue.RequestError, - aiohue.LinkButtonNotPressed): - errors['base'] = 'register_failed' - except aiohue.AiohueException: - errors['base'] = 'linking' - _LOGGER.exception('Unknown Hue linking error occurred') - else: - return self.async_create_entry( - title=bridge.config.name, - data={ - 'host': bridge.host, - 'bridge_id': bridge.config.bridgeid, - 'username': bridge.username, - } - ) - - return self.async_show_form( - step_id='link', - errors=errors, - ) - - async def async_setup_entry(hass, entry): - """Set up a bridge for a config entry.""" - await async_setup_bridge(hass, entry.data['host'], - username=entry.data['username']) - return True + """Set up a bridge from a config entry.""" + host = entry.data['host'] + config = hass.data[DOMAIN].get(host) + + if config is None: + allow_unreachable = DEFAULT_ALLOW_UNREACHABLE + allow_groups = DEFAULT_ALLOW_HUE_GROUPS + else: + allow_unreachable = config[CONF_ALLOW_UNREACHABLE] + allow_groups = config[CONF_ALLOW_HUE_GROUPS] + + bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) + hass.data[DOMAIN][host] = bridge + return await bridge.async_setup() diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py new file mode 100644 index 00000000000..790831a4d6c --- /dev/null +++ b/homeassistant/components/hue/bridge.py @@ -0,0 +1,143 @@ +"""Code to handle a Hue bridge.""" +import asyncio + +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN, LOGGER +from .errors import AuthenticationRequired, CannotConnect + +SERVICE_HUE_SCENE = "hue_activate_scene" +ATTR_GROUP_NAME = "group_name" +ATTR_SCENE_NAME = "scene_name" +SCENE_SCHEMA = vol.Schema({ + vol.Required(ATTR_GROUP_NAME): cv.string, + vol.Required(ATTR_SCENE_NAME): cv.string, +}) + + +class HueBridge(object): + """Manages a single Hue bridge.""" + + def __init__(self, hass, config_entry, allow_unreachable, allow_groups): + """Initialize the system.""" + self.config_entry = config_entry + self.hass = hass + self.allow_unreachable = allow_unreachable + self.allow_groups = allow_groups + self.available = True + self.api = None + + async def async_setup(self, tries=0): + """Set up a phue bridge based on host parameter.""" + host = self.config_entry.data['host'] + + try: + self.api = await get_bridge( + self.hass, host, + self.config_entry.data['username'] + ) + except AuthenticationRequired: + # usernames can become invalid if hub is reset or user removed. + # We are going to fail the config entry setup and initiate a new + # linking procedure. When linking succeeds, it will remove the + # old config entry. + self.hass.async_add_job(self.hass.config_entries.flow.async_init( + DOMAIN, source='import', data={ + 'host': host, + } + )) + return False + + except CannotConnect: + retry_delay = 2 ** (tries + 1) + LOGGER.error("Error connecting to the Hue bridge at %s. Retrying " + "in %d seconds", host, retry_delay) + + async def retry_setup(_now): + """Retry setup.""" + if await self.async_setup(tries + 1): + # This feels hacky, we should find a better way to do this + self.config_entry.state = config_entries.ENTRY_STATE_LOADED + + # Unhandled edge case: cancel this if we discover bridge on new IP + self.hass.helpers.event.async_call_later(retry_delay, retry_setup) + + return False + + except Exception: # pylint: disable=broad-except + LOGGER.exception('Unknown error connecting with Hue bridge at %s', + host) + return False + + self.hass.async_add_job( + self.hass.helpers.discovery.async_load_platform( + 'light', DOMAIN, {'host': host})) + + self.hass.services.async_register( + DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, + schema=SCENE_SCHEMA) + + return True + + async def hue_activate_scene(self, call, updated=False): + """Service to call directly into bridge to set scenes.""" + group_name = call.data[ATTR_GROUP_NAME] + scene_name = call.data[ATTR_SCENE_NAME] + + group = next( + (group for group in self.api.groups.values() + if group.name == group_name), None) + + scene_id = next( + (scene.id for scene in self.api.scenes.values() + if scene.name == scene_name), None) + + # If we can't find it, fetch latest info. + if not updated and (group is None or scene_id is None): + await self.api.groups.update() + await self.api.scenes.update() + await self.hue_activate_scene(call, updated=True) + return + + if group is None: + LOGGER.warning('Unable to find group %s', group_name) + return + + if scene_id is None: + LOGGER.warning('Unable to find scene %s', scene_name) + return + + await group.set_action(scene=scene_id) + + +async def get_bridge(hass, host, username=None): + """Create a bridge object and verify authentication.""" + import aiohue + + bridge = aiohue.Bridge( + host, username=username, + websession=aiohttp_client.async_get_clientsession(hass) + ) + + try: + with async_timeout.timeout(5): + # Create username if we don't have one + if not username: + await bridge.create_user('home-assistant') + # Initialize bridge (and validate our username) + await bridge.initialize() + + return bridge + except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): + LOGGER.warning("Connected to Hue at %s but not registered.", host) + raise AuthenticationRequired + except (asyncio.TimeoutError, aiohue.RequestError): + LOGGER.error("Error connecting to the Hue bridge at %s", host) + raise CannotConnect + except aiohue.AiohueException: + LOGGER.exception('Unknown Hue linking error occurred') + raise AuthenticationRequired diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py new file mode 100644 index 00000000000..11e399c984d --- /dev/null +++ b/homeassistant/components/hue/config_flow.py @@ -0,0 +1,235 @@ +"""Config flow to configure Philips Hue.""" +import asyncio +import json +import os + +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .bridge import get_bridge +from .const import DOMAIN, LOGGER +from .errors import AuthenticationRequired, CannotConnect + + +@callback +def configured_hosts(hass): + """Return a set of the configured hosts.""" + return set(entry.data['host'] for entry + in hass.config_entries.async_entries(DOMAIN)) + + +def _find_username_from_config(hass, filename): + """Load username from config. + + This was a legacy way of configuring Hue until Home Assistant 0.67. + """ + path = hass.config.path(filename) + + if not os.path.isfile(path): + return None + + with open(path) as inp: + try: + return list(json.load(inp).values())[0]['username'] + except ValueError: + # If we get invalid JSON + return None + + +@config_entries.HANDLERS.register(DOMAIN) +class HueFlowHandler(config_entries.ConfigFlowHandler): + """Handle a Hue config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the Hue flow.""" + self.host = None + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + from aiohue.discovery import discover_nupnp + + if user_input is not None: + self.host = user_input['host'] + return await self.async_step_link() + + websession = aiohttp_client.async_get_clientsession(self.hass) + + try: + with async_timeout.timeout(5): + bridges = await discover_nupnp(websession=websession) + except asyncio.TimeoutError: + return self.async_abort( + reason='discover_timeout' + ) + + if not bridges: + return self.async_abort( + reason='no_bridges' + ) + + # Find already configured hosts + configured = configured_hosts(self.hass) + + hosts = [bridge.host for bridge in bridges + if bridge.host not in configured] + + if not hosts: + return self.async_abort( + reason='all_configured' + ) + + elif len(hosts) == 1: + self.host = hosts[0] + return await self.async_step_link() + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required('host'): vol.In(hosts) + }) + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the Hue bridge. + + Given a configured host, will ask the user to press the link button + to connect to the bridge. + """ + errors = {} + + # We will always try linking in case the user has already pressed + # the link button. + try: + bridge = await get_bridge( + self.hass, self.host, username=None + ) + + return await self._entry_from_bridge(bridge) + except AuthenticationRequired: + errors['base'] = 'register_failed' + + except CannotConnect: + LOGGER.error("Error connecting to the Hue bridge at %s", self.host) + errors['base'] = 'linking' + + except Exception: # pylint: disable=broad-except + LOGGER.exception( + 'Unknown error connecting with Hue bridge at %s', + self.host) + errors['base'] = 'linking' + + # If there was no user input, do not show the errors. + if user_input is None: + errors = {} + + return self.async_show_form( + step_id='link', + errors=errors, + ) + + async def async_step_discovery(self, discovery_info): + """Handle a discovered Hue bridge. + + This flow is triggered by the discovery component. It will check if the + host is already configured and delegate to the import step if not. + """ + # Filter out emulated Hue + if "HASS Bridge" in discovery_info.get('name', ''): + return self.async_abort(reason='already_configured') + + host = discovery_info.get('host') + + if host in configured_hosts(self.hass): + return self.async_abort(reason='already_configured') + + # This value is based off host/description.xml and is, weirdly, missing + # 4 characters in the middle of the serial compared to results returned + # from the NUPNP API or when querying the bridge API for bridgeid. + # (on first gen Hue hub) + serial = discovery_info.get('serial') + + return await self.async_step_import({ + 'host': host, + # This format is the legacy format that Hue used for discovery + 'path': 'phue-{}.conf'.format(serial) + }) + + async def async_step_import(self, import_info): + """Import a new bridge as a config entry. + + Will read authentication from Phue config file if available. + + This flow is triggered by `async_setup` for both configured and + discovered bridges. Triggered for any bridge that does not have a + config entry yet (based on host). + + This flow is also triggered by `async_step_discovery`. + + If an existing config file is found, we will validate the credentials + and create an entry. Otherwise we will delegate to `link` step which + will ask user to link the bridge. + """ + host = import_info['host'] + path = import_info.get('path') + + if path is not None: + username = await self.hass.async_add_job( + _find_username_from_config, self.hass, + self.hass.config.path(path)) + else: + username = None + + try: + bridge = await get_bridge( + self.hass, host, username + ) + + LOGGER.info('Imported authentication for %s from %s', host, path) + + return await self._entry_from_bridge(bridge) + except AuthenticationRequired: + self.host = host + + LOGGER.info('Invalid authentication for %s, requesting link.', + host) + + return await self.async_step_link() + + except CannotConnect: + LOGGER.error("Error connecting to the Hue bridge at %s", host) + return self.async_abort(reason='cannot_connect') + + except Exception: # pylint: disable=broad-except + LOGGER.exception('Unknown error connecting with Hue bridge at %s', + host) + return self.async_abort(reason='unknown') + + async def _entry_from_bridge(self, bridge): + """Return a config entry from an initialized bridge.""" + # Remove all other entries of hubs with same ID or host + host = bridge.host + bridge_id = bridge.config.bridgeid + + same_hub_entries = [entry.entry_id for entry + in self.hass.config_entries.async_entries(DOMAIN) + if entry.data['bridge_id'] == bridge_id or + entry.data['host'] == host] + + if same_hub_entries: + await asyncio.wait([self.hass.config_entries.async_remove(entry_id) + for entry_id in same_hub_entries]) + + return self.async_create_entry( + title=bridge.config.name, + data={ + 'host': host, + 'bridge_id': bridge_id, + 'username': bridge.username, + } + ) diff --git a/homeassistant/components/hue/const.py b/homeassistant/components/hue/const.py new file mode 100644 index 00000000000..2eb30d47804 --- /dev/null +++ b/homeassistant/components/hue/const.py @@ -0,0 +1,6 @@ +"""Constants for the Hue component.""" +import logging + +LOGGER = logging.getLogger('homeassistant.components.hue') +DOMAIN = "hue" +API_NUPNP = 'https://www.meethue.com/api/nupnp' diff --git a/homeassistant/components/hue/errors.py b/homeassistant/components/hue/errors.py new file mode 100644 index 00000000000..dd217c3bc26 --- /dev/null +++ b/homeassistant/components/hue/errors.py @@ -0,0 +1,14 @@ +"""Errors for the Hue component.""" +from homeassistant.exceptions import HomeAssistantError + + +class HueException(HomeAssistantError): + """Base class for Hue exceptions.""" + + +class CannotConnect(HueException): + """Unable to connect to the bridge.""" + + +class AuthenticationRequired(HueException): + """Unknown error occurred.""" diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 59b1ecd3cd1..fc9e91c93d7 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -20,7 +20,10 @@ "abort": { "discover_timeout": "Unable to discover Hue bridges", "no_bridges": "No Philips Hue bridges discovered", - "all_configured": "All Philips Hue bridges are already configured" + "all_configured": "All Philips Hue bridges are already configured", + "unknown": "Unknown error occurred", + "cannot_connect": "Unable to connect to the bridge", + "already_configured": "Bridge is already configured" } } } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eb05e800683..b02026ac6dd 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -384,7 +384,7 @@ class FlowManager: handler = HANDLERS.get(domain) if handler is None: - raise self.hass.helpers.UnknownHandler + raise UnknownHandler # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py deleted file mode 100644 index 7ccc202b31b..00000000000 --- a/tests/components/hue/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Fixtures for Hue tests.""" -from unittest.mock import patch - -import pytest - -from tests.common import mock_coro_func - - -@pytest.fixture -def mock_bridge(): - """Mock the HueBridge from initializing.""" - with patch('homeassistant.components.hue._find_username_from_config', - return_value=None), \ - patch('homeassistant.components.hue.HueBridge') as mock_bridge: - mock_bridge().async_setup = mock_coro_func() - mock_bridge.reset_mock() - yield mock_bridge diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 39351699df5..0845aa2f077 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,99 +1,57 @@ """Test Hue bridge.""" -import asyncio from unittest.mock import Mock, patch -import aiohue -import pytest - -from homeassistant.components import hue +from homeassistant.components.hue import bridge, errors from tests.common import mock_coro -class MockBridge(hue.HueBridge): - """Class that sets default for constructor.""" +async def test_bridge_setup(): + """Test a successful setup.""" + hass = Mock() + entry = Mock() + api = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) - def __init__(self, hass, host='1.2.3.4', filename='mock-bridge.conf', - username=None, **kwargs): - """Initialize a mock bridge.""" - super().__init__(host, hass, filename, username, **kwargs) + with patch.object(bridge, 'get_bridge', return_value=mock_coro(api)): + assert await hue_bridge.async_setup() is True - -@pytest.fixture -def mock_request(): - """Mock configurator.async_request_config.""" - with patch('homeassistant.components.configurator.' - 'async_request_config') as mock_request: - yield mock_request - - -async def test_setup_request_config_button_not_pressed(hass, mock_request): - """Test we request config if link button has not been pressed.""" - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 1 - - -async def test_setup_request_config_invalid_username(hass, mock_request): - """Test we request config if username is no longer whitelisted.""" - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.Unauthorized): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 1 - - -async def test_setup_timeout(hass, mock_request): - """Test we give up when there is a timeout.""" - with patch('aiohue.Bridge.create_user', - side_effect=asyncio.TimeoutError): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 0 - - -async def test_only_create_no_username(hass): - """.""" - with patch('aiohue.Bridge.create_user') as mock_create, \ - patch('aiohue.Bridge.initialize') as mock_init: - await MockBridge(hass, username='bla').async_setup() - - assert len(mock_create.mock_calls) == 0 - assert len(mock_init.mock_calls) == 1 - - -async def test_configurator_callback(hass, mock_request): - """.""" - hass.data[hue.DOMAIN] = {} - with patch('aiohue.Bridge.create_user', - side_effect=aiohue.LinkButtonNotPressed): - await MockBridge(hass).async_setup() - - assert len(mock_request.mock_calls) == 1 - - callback = mock_request.mock_calls[0][1][2] - - mock_init = Mock(return_value=mock_coro()) - mock_create = Mock(return_value=mock_coro()) - - with patch('aiohue.Bridge') as mock_bridge, \ - patch('homeassistant.helpers.discovery.async_load_platform', - return_value=mock_coro()) as mock_load_platform, \ - patch('homeassistant.components.hue.save_json') as mock_save: - inst = mock_bridge() - inst.username = 'mock-user' - inst.create_user = mock_create - inst.initialize = mock_init - await callback(None) - - assert len(mock_create.mock_calls) == 1 - assert len(mock_init.mock_calls) == 1 - assert len(mock_save.mock_calls) == 1 - assert mock_save.mock_calls[0][1][1] == { - '1.2.3.4': { - 'username': 'mock-user' - } + assert hue_bridge.api is api + assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1 + assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == { + 'host': '1.2.3.4' } - assert len(mock_load_platform.mock_calls) == 1 + + +async def test_bridge_setup_invalid_username(): + """Test we start config flow if username is no longer whitelisted.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', + side_effect=errors.AuthenticationRequired): + assert await hue_bridge.async_setup() is False + + assert len(hass.async_add_job.mock_calls) == 1 + assert len(hass.config_entries.flow.async_init.mock_calls) == 1 + assert hass.config_entries.flow.async_init.mock_calls[0][2]['data'] == { + 'host': '1.2.3.4' + } + + +async def test_bridge_setup_timeout(hass): + """Test we retry to connect if we cannot connect.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect): + assert await hue_bridge.async_setup() is False + + assert len(hass.helpers.event.async_call_later.mock_calls) == 1 + # Assert we are going to wait 2 seconds + assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2 diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 959e3c6241b..fe3bffe5357 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,28 +1,29 @@ """Tests for Philips Hue config flow.""" import asyncio -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohue import pytest import voluptuous as vol -from homeassistant.components import hue +from homeassistant.components.hue import config_flow, const, errors from tests.common import MockConfigEntry, mock_coro async def test_flow_works(hass, aioclient_mock): """Test config flow .""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'} ]) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass await flow.async_step_init() with patch('aiohue.Bridge') as mock_bridge: - def mock_constructor(host, websession): + def mock_constructor(host, websession, username=None): + """Fake the bridge constructor.""" mock_bridge.host = host return mock_bridge @@ -50,8 +51,8 @@ async def test_flow_works(hass, aioclient_mock): async def test_flow_no_discovered_bridges(hass, aioclient_mock): """Test config flow discovers no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - flow = hue.HueFlowHandler() + aioclient_mock.get(const.API_NUPNP, json=[]) + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -60,13 +61,13 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock): async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): """Test config flow discovers only already configured bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'} ]) MockConfigEntry(domain='hue', data={ 'host': '1.2.3.4' }).add_to_hass(hass) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -75,10 +76,10 @@ async def test_flow_all_discovered_bridges_exist(hass, aioclient_mock): async def test_flow_one_bridge_discovered(hass, aioclient_mock): """Test config flow discovers one bridge.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'} ]) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -88,11 +89,11 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered(hass, aioclient_mock): """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'}, {'internalipaddress': '5.6.7.8', 'id': 'beer'} ]) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -108,14 +109,14 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): """Test config flow discovers two bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[ + aioclient_mock.get(const.API_NUPNP, json=[ {'internalipaddress': '1.2.3.4', 'id': 'bla'}, {'internalipaddress': '5.6.7.8', 'id': 'beer'} ]) MockConfigEntry(domain='hue', data={ 'host': '1.2.3.4' }).add_to_hass(hass) - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass result = await flow.async_step_init() @@ -126,7 +127,7 @@ async def test_flow_two_bridges_discovered_one_new(hass, aioclient_mock): async def test_flow_timeout_discovery(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.discovery.discover_nupnp', @@ -138,7 +139,7 @@ async def test_flow_timeout_discovery(hass): async def test_flow_link_timeout(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.Bridge.create_user', @@ -148,13 +149,13 @@ async def test_flow_link_timeout(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'register_failed' + 'base': 'linking' } async def test_flow_link_button_not_pressed(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.Bridge.create_user', @@ -170,7 +171,7 @@ async def test_flow_link_button_not_pressed(hass): async def test_flow_link_unknown_host(hass): """Test config flow .""" - flow = hue.HueFlowHandler() + flow = config_flow.HueFlowHandler() flow.hass = hass with patch('aiohue.Bridge.create_user', @@ -180,5 +181,175 @@ async def test_flow_link_unknown_host(hass): assert result['type'] == 'form' assert result['step_id'] == 'link' assert result['errors'] == { - 'base': 'register_failed' + 'base': 'linking' } + + +async def test_bridge_discovery(hass): + """Test a bridge being discovered.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_discovery({ + 'host': '0.0.0.0', + 'serial': '1234' + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_bridge_discovery_emulated_hue(hass): + """Test if discovery info is from an emulated hue instance.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_discovery({ + 'name': 'HASS Bridge', + 'host': '0.0.0.0', + 'serial': '1234' + }) + + assert result['type'] == 'abort' + + +async def test_bridge_discovery_already_configured(hass): + """Test if a discovered bridge has already been configured.""" + MockConfigEntry(domain='hue', data={ + 'host': '0.0.0.0' + }).add_to_hass(hass) + + flow = config_flow.HueFlowHandler() + flow.hass = hass + + result = await flow.async_step_discovery({ + 'host': '0.0.0.0', + 'serial': '1234' + }) + + assert result['type'] == 'abort' + + +async def test_import_with_existing_config(hass): + """Test importing a host with an existing config file.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + bridge = Mock() + bridge.username = 'username-abc' + bridge.config.bridgeid = 'bridge-id-1234' + bridge.config.name = 'Mock Bridge' + bridge.host = '0.0.0.0' + + with patch.object(config_flow, '_find_username_from_config', + return_value='mock-user'), \ + patch.object(config_flow, 'get_bridge', + return_value=mock_coro(bridge)): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + 'path': 'bla.conf' + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '0.0.0.0', + 'bridge_id': 'bridge-id-1234', + 'username': 'username-abc' + } + + +async def test_import_with_no_config(hass): + """Test importing a host without an existing config file.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_import_with_existing_but_invalid_config(hass): + """Test importing a host with a config file with invalid username.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, '_find_username_from_config', + return_value='mock-user'), \ + patch.object(config_flow, 'get_bridge', + side_effect=errors.AuthenticationRequired): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + 'path': 'bla.conf' + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_import_cannot_connect(hass): + """Test importing a host that we cannot conncet to.""" + flow = config_flow.HueFlowHandler() + flow.hass = hass + + with patch.object(config_flow, 'get_bridge', + side_effect=errors.CannotConnect): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'abort' + assert result['reason'] == 'cannot_connect' + + +async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): + """Test that we clean up entries for same host and bridge. + + An IP can only hold a single bridge and a single bridge can only be + accessible via a single IP. So when we create a new entry, we'll remove + all existing entries that either have same IP or same bridge_id. + """ + MockConfigEntry(domain='hue', data={ + 'host': '0.0.0.0', + 'bridge_id': 'id-1234' + }).add_to_hass(hass) + + MockConfigEntry(domain='hue', data={ + 'host': '1.2.3.4', + 'bridge_id': 'id-1234' + }).add_to_hass(hass) + + assert len(hass.config_entries.async_entries('hue')) == 2 + + flow = config_flow.HueFlowHandler() + flow.hass = hass + + bridge = Mock() + bridge.username = 'username-abc' + bridge.config.bridgeid = 'id-1234' + bridge.config.name = 'Mock Bridge' + bridge.host = '0.0.0.0' + + with patch.object(config_flow, 'get_bridge', + return_value=mock_coro(bridge)): + result = await flow.async_step_import({ + 'host': '0.0.0.0', + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Mock Bridge' + assert result['data'] == { + 'host': '0.0.0.0', + 'bridge_id': 'id-1234', + 'username': 'username-abc' + } + # We did not process the result of this entry but already removed the old + # ones. So we should have 0 entries. + assert len(hass.config_entries.async_entries('hue')) == 0 diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py new file mode 100644 index 00000000000..47e74b70e83 --- /dev/null +++ b/tests/components/hue/test_init.py @@ -0,0 +1,169 @@ +"""Test Hue setup process.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import hue + +from tests.common import mock_coro, MockConfigEntry + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to setup a bridge.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=[]): + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + + # No flows started + assert len(mock_config_entries.flow.mock_calls) == 0 + + # No configs stored + assert hass.data[hue.DOMAIN] == {} + + +async def test_setup_with_discovery_no_known_auth(hass, aioclient_mock): + """Test discovering a bridge and not having known auth.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + { + 'internalipaddress': '0.0.0.0', + 'id': 'abcd1234' + } + ]) + + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=[]): + mock_config_entries.flow.async_init.return_value = mock_coro() + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: {} + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 1 + assert mock_config_entries.flow.mock_calls[0][2]['data'] == { + 'host': '0.0.0.0', + 'path': '.hue_abcd1234.conf', + } + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == { + '0.0.0.0': { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: '.hue_abcd1234.conf', + hue.CONF_ALLOW_HUE_GROUPS: hue.DEFAULT_ALLOW_HUE_GROUPS, + hue.CONF_ALLOW_UNREACHABLE: hue.DEFAULT_ALLOW_UNREACHABLE, + } + } + + +async def test_setup_with_discovery_known_auth(hass, aioclient_mock): + """Test we don't do anything if we discover already configured hub.""" + aioclient_mock.get(hue.API_NUPNP, json=[ + { + 'internalipaddress': '0.0.0.0', + 'id': 'abcd1234' + } + ]) + + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']): + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: {} + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 0 + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == {} + + +async def test_setup_defined_hosts_known_auth(hass): + """Test we don't initiate a config entry if config bridge is known.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=['0.0.0.0']): + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 0 + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == { + '0.0.0.0': { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + + +async def test_setup_defined_hosts_no_known_auth(hass): + """Test we initiate config entry if config bridge is not known.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(hue, 'configured_hosts', return_value=[]): + mock_config_entries.flow.async_init.return_value = mock_coro() + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + }) is True + + # Flow started for discovered bridge + assert len(mock_config_entries.flow.mock_calls) == 1 + assert mock_config_entries.flow.mock_calls[0][2]['data'] == { + 'host': '0.0.0.0', + 'path': 'bla.conf', + } + + # Config stored for domain. + assert hass.data[hue.DOMAIN] == { + '0.0.0.0': { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + + +async def test_config_passed_to_config_entry(hass): + """Test that configured options for a host are loaded via config entry.""" + entry = MockConfigEntry(domain=hue.DOMAIN, data={ + 'host': '0.0.0.0', + }) + entry.add_to_hass(hass) + + with patch.object(hue, 'HueBridge') as mock_bridge: + mock_bridge.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hue.DOMAIN, { + hue.DOMAIN: { + hue.CONF_BRIDGES: { + hue.CONF_HOST: '0.0.0.0', + hue.CONF_FILENAME: 'bla.conf', + hue.CONF_ALLOW_HUE_GROUPS: False, + hue.CONF_ALLOW_UNREACHABLE: True + } + } + }) is True + + assert len(mock_bridge.mock_calls) == 2 + p_hass, p_entry, p_allow_unreachable, p_allow_groups = \ + mock_bridge.mock_calls[0][1] + + assert p_hass is hass + assert p_entry is entry + assert p_allow_unreachable is True + assert p_allow_groups is False diff --git a/tests/components/hue/test_setup.py b/tests/components/hue/test_setup.py deleted file mode 100644 index f90f58a50c3..00000000000 --- a/tests/components/hue/test_setup.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Test Hue setup process.""" -from homeassistant.setup import async_setup_component -from homeassistant.components import hue -from homeassistant.components.discovery import SERVICE_HUE - - -async def test_setup_with_multiple_hosts(hass, mock_bridge): - """Multiple hosts specified in the config file.""" - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: { - hue.CONF_BRIDGES: [ - {hue.CONF_HOST: '127.0.0.1'}, - {hue.CONF_HOST: '192.168.1.10'}, - ] - } - }) - - assert len(mock_bridge.mock_calls) == 2 - hosts = sorted(mock_call[1][0] for mock_call in mock_bridge.mock_calls) - assert hosts == ['127.0.0.1', '192.168.1.10'] - - -async def test_bridge_discovered(hass, mock_bridge): - """Bridge discovery.""" - assert await async_setup_component(hass, hue.DOMAIN, {}) - - await hass.helpers.discovery.async_discover(SERVICE_HUE, { - 'host': '192.168.1.10', - 'serial': '1234567', - }) - await hass.async_block_till_done() - - assert len(mock_bridge.mock_calls) == 1 - assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - - -async def test_bridge_configure_and_discovered(hass, mock_bridge): - """Bridge is in the config file, then we discover it.""" - assert await async_setup_component(hass, hue.DOMAIN, { - hue.DOMAIN: { - hue.CONF_BRIDGES: { - hue.CONF_HOST: '192.168.1.10' - } - } - }) - - assert len(mock_bridge.mock_calls) == 1 - assert mock_bridge.mock_calls[0][1][0] == '192.168.1.10' - hass.data[hue.DOMAIN] = {'192.168.1.10': {}} - - mock_bridge.reset_mock() - - await hass.helpers.discovery.async_discover(SERVICE_HUE, { - 'host': '192.168.1.10', - 'serial': '1234567', - }) - await hass.async_block_till_done() - - assert len(mock_bridge.mock_calls) == 0 - - -async def test_setup_no_host(hass, aioclient_mock): - """Check we call discovery if domain specified but no bridges.""" - aioclient_mock.get(hue.API_NUPNP, json=[]) - - result = await async_setup_component( - hass, hue.DOMAIN, {hue.DOMAIN: {}}) - assert result - - assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index 580d876982d..b4c80bf3210 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -5,6 +5,7 @@ from unittest.mock import patch, MagicMock import pytest +from homeassistant import config_entries from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery from homeassistant.util.dt import utcnow @@ -44,13 +45,12 @@ def netdisco_mock(): yield -@asyncio.coroutine -def mock_discovery(hass, discoveries, config=BASE_CONFIG): +async def mock_discovery(hass, discoveries, config=BASE_CONFIG): """Helper to mock discoveries.""" - result = yield from async_setup_component(hass, 'discovery', config) + result = await async_setup_component(hass, 'discovery', config) assert result - yield from hass.async_start() + await hass.async_start() with patch.object(discovery, '_discover', discoveries), \ patch('homeassistant.components.discovery.async_discover', @@ -59,8 +59,8 @@ def mock_discovery(hass, discoveries, config=BASE_CONFIG): return_value=mock_coro()) as mock_platform: async_fire_time_changed(hass, utcnow()) # Work around an issue where our loop.call_soon not get caught - yield from hass.async_block_till_done() - yield from hass.async_block_till_done() + await hass.async_block_till_done() + await hass.async_block_till_done() return mock_discover, mock_platform @@ -154,3 +154,25 @@ def test_load_component_hassio(hass): yield from mock_discovery(hass, discover) assert mock_hassio.called + + +async def test_discover_config_flow(hass): + """Test discovery triggering a config flow.""" + discovery_info = { + 'hello': 'world' + } + + def discover(netdisco): + """Fake discovery.""" + return [('mock-service', discovery_info)] + + with patch.dict(discovery.CONFIG_ENTRY_HANDLERS, { + 'mock-service': 'mock-component'}), patch( + 'homeassistant.config_entries.FlowManager.async_init') as m_init: + await mock_discovery(hass, discover) + + assert len(m_init.mock_calls) == 1 + args, kwargs = m_init.mock_calls[0][1:] + assert args == ('mock-component',) + assert kwargs['source'] == config_entries.SOURCE_DISCOVERY + assert kwargs['data'] == discovery_info From 5801d78017c6ef4cce986bc046db4d7eb448449c Mon Sep 17 00:00:00 2001 From: Albert Lee Date: Fri, 30 Mar 2018 01:49:08 -0500 Subject: [PATCH 222/924] Implement thermostat support for Alexa (#13340) * Implement thermostat support for Alexa * util.temperature: Support interval conversions * Use climate.ATTR_OPERATION_MODE for Alexa thermostat mode * Switch coroutines to async/await * Log all Alexa error events --- homeassistant/components/alexa/smart_home.py | 249 ++++++++++++++++++- homeassistant/util/temperature.py | 15 +- tests/components/alexa/test_smart_home.py | 127 ++++++++++ 3 files changed, 374 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 5e5155b3db8..707f8d02958 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -6,18 +6,20 @@ from datetime import datetime from uuid import uuid4 from homeassistant.components import ( - alert, automation, cover, fan, group, input_boolean, light, lock, + alert, automation, cover, climate, fan, group, input_boolean, light, lock, media_player, scene, script, switch, http, sensor) import homeassistant.core as ha import homeassistant.util.color as color_util +from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.decorator import Registry from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_NAME, SERVICE_LOCK, - SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, CONF_NAME, + SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, SERVICE_VOLUME_SET, TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_UNIT_OF_MEASUREMENT, STATE_LOCKED, STATE_UNLOCKED, STATE_ON) + from .const import CONF_FILTER, CONF_ENTITY_CONFIG _LOGGER = logging.getLogger(__name__) @@ -34,6 +36,16 @@ API_TEMP_UNITS = { TEMP_CELSIUS: 'CELSIUS', } +API_THERMOSTAT_MODES = { + climate.STATE_HEAT: 'HEAT', + climate.STATE_COOL: 'COOL', + climate.STATE_AUTO: 'AUTO', + climate.STATE_ECO: 'ECO', + climate.STATE_IDLE: 'OFF', + climate.STATE_FAN_ONLY: 'OFF', + climate.STATE_DRY: 'OFF', +} + SMART_HOME_HTTP_ENDPOINT = '/api/alexa/smart_home' CONF_DESCRIPTION = 'description' @@ -383,8 +395,60 @@ class _AlexaTemperatureSensor(_AlexaInterface): raise _UnsupportedProperty(name) unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp = self.entity.state + if self.entity.domain == climate.DOMAIN: + temp = self.entity.attributes.get( + climate.ATTR_CURRENT_TEMPERATURE) return { - 'value': float(self.entity.state), + 'value': float(temp), + 'scale': API_TEMP_UNITS[unit], + } + + +class _AlexaThermostatController(_AlexaInterface): + def name(self): + return 'Alexa.ThermostatController' + + def properties_supported(self): + properties = [] + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & climate.SUPPORT_TARGET_TEMPERATURE: + properties.append({'name': 'targetSetpoint'}) + if supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW: + properties.append({'name': 'lowerSetpoint'}) + if supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH: + properties.append({'name': 'upperSetpoint'}) + if supported & climate.SUPPORT_OPERATION_MODE: + properties.append({'name': 'thermostatMode'}) + return properties + + def properties_retrievable(self): + return True + + def get_property(self, name): + if name == 'thermostatMode': + ha_mode = self.entity.attributes.get(climate.ATTR_OPERATION_MODE) + mode = API_THERMOSTAT_MODES.get(ha_mode) + if mode is None: + _LOGGER.error("%s (%s) has unsupported %s value '%s'", + self.entity.entity_id, type(self.entity), + climate.ATTR_OPERATION_MODE, ha_mode) + raise _UnsupportedProperty(name) + return mode + + unit = self.entity.attributes[CONF_UNIT_OF_MEASUREMENT] + temp = None + if name == 'targetSetpoint': + temp = self.entity.attributes.get(ATTR_TEMPERATURE) + elif name == 'lowerSetpoint': + temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_LOW) + elif name == 'upperSetpoint': + temp = self.entity.attributes.get(climate.ATTR_TARGET_TEMP_HIGH) + if temp is None: + raise _UnsupportedProperty(name) + + return { + 'value': float(temp), 'scale': API_TEMP_UNITS[unit], } @@ -415,6 +479,16 @@ class _SwitchCapabilities(_AlexaEntity): return [_AlexaPowerController(self.entity)] +@ENTITY_ADAPTERS.register(climate.DOMAIN) +class _ClimateCapabilities(_AlexaEntity): + def default_display_categories(self): + return [_DisplayCategory.THERMOSTAT] + + def interfaces(self): + yield _AlexaThermostatController(self.entity) + yield _AlexaTemperatureSensor(self.entity) + + @ENTITY_ADAPTERS.register(cover.DOMAIN) class _CoverCapabilities(_AlexaEntity): def default_display_categories(self): @@ -682,17 +756,26 @@ def api_message(request, return response -def api_error(request, error_type='INTERNAL_ERROR', error_message=""): +def api_error(request, + namespace='Alexa', + error_type='INTERNAL_ERROR', + error_message="", + payload=None): """Create a API formatted error response. Async friendly. """ - payload = { - 'type': error_type, - 'message': error_message, - } + payload = payload or {} + payload['type'] = error_type + payload['message'] = error_message - return api_message(request, name='ErrorResponse', payload=payload) + _LOGGER.info("Request %s/%s error %s: %s", + request[API_HEADER]['namespace'], + request[API_HEADER]['name'], + error_type, error_message) + + return api_message( + request, name='ErrorResponse', namespace=namespace, payload=payload) @HANDLERS.register(('Alexa.Discovery', 'Discover')) @@ -1104,7 +1187,6 @@ def async_api_select_input(hass, config, request, entity): else: msg = 'failed to map input {} to a media source on {}'.format( media_input, entity.entity_id) - _LOGGER.error(msg) return api_error( request, error_type='INVALID_VALUE', error_message=msg) @@ -1276,6 +1358,149 @@ def async_api_previous(hass, config, request, entity): return api_message(request) +def api_error_temp_range(request, temp, min_temp, max_temp, unit): + """Create temperature value out of range API error response. + + Async friendly. + """ + temp_range = { + 'minimumValue': { + 'value': min_temp, + 'scale': API_TEMP_UNITS[unit], + }, + 'maximumValue': { + 'value': max_temp, + 'scale': API_TEMP_UNITS[unit], + }, + } + + msg = 'The requested temperature {} is out of range'.format(temp) + return api_error( + request, + error_type='TEMPERATURE_VALUE_OUT_OF_RANGE', + error_message=msg, + payload={'validRange': temp_range}, + ) + + +def temperature_from_object(temp_obj, to_unit, interval=False): + """Get temperature from Temperature object in requested unit.""" + from_unit = TEMP_CELSIUS + temp = float(temp_obj['value']) + + if temp_obj['scale'] == 'FAHRENHEIT': + from_unit = TEMP_FAHRENHEIT + elif temp_obj['scale'] == 'KELVIN': + # convert to Celsius if absolute temperature + if not interval: + temp -= 273.15 + + return convert_temperature(temp, from_unit, to_unit, interval) + + +@HANDLERS.register(('Alexa.ThermostatController', 'SetTargetTemperature')) +@extract_entity +async def async_api_set_target_temp(hass, config, request, entity): + """Process a set target temperature request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) + max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + + data = { + ATTR_ENTITY_ID: entity.entity_id + } + + payload = request[API_PAYLOAD] + if 'targetSetpoint' in payload: + temp = temperature_from_object( + payload['targetSetpoint'], unit) + if temp < min_temp or temp > max_temp: + return api_error_temp_range( + request, temp, min_temp, max_temp, unit) + data[ATTR_TEMPERATURE] = temp + if 'lowerSetpoint' in payload: + temp_low = temperature_from_object( + payload['lowerSetpoint'], unit) + if temp_low < min_temp or temp_low > max_temp: + return api_error_temp_range( + request, temp_low, min_temp, max_temp, unit) + data[climate.ATTR_TARGET_TEMP_LOW] = temp_low + if 'upperSetpoint' in payload: + temp_high = temperature_from_object( + payload['upperSetpoint'], unit) + if temp_high < min_temp or temp_high > max_temp: + return api_error_temp_range( + request, temp_high, min_temp, max_temp, unit) + data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high + + await hass.services.async_call( + entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False) + + return api_message(request) + + +@HANDLERS.register(('Alexa.ThermostatController', 'AdjustTargetTemperature')) +@extract_entity +async def async_api_adjust_target_temp(hass, config, request, entity): + """Process an adjust target temperature request.""" + unit = entity.attributes[CONF_UNIT_OF_MEASUREMENT] + min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) + max_temp = entity.attributes.get(climate.ATTR_MAX_TEMP) + + temp_delta = temperature_from_object( + request[API_PAYLOAD]['targetSetpointDelta'], unit, interval=True) + target_temp = float(entity.attributes.get(ATTR_TEMPERATURE)) + temp_delta + + if target_temp < min_temp or target_temp > max_temp: + return api_error_temp_range( + request, target_temp, min_temp, max_temp, unit) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + ATTR_TEMPERATURE: target_temp, + } + + await hass.services.async_call( + entity.domain, climate.SERVICE_SET_TEMPERATURE, data, blocking=False) + + return api_message(request) + + +@HANDLERS.register(('Alexa.ThermostatController', 'SetThermostatMode')) +@extract_entity +async def async_api_set_thermostat_mode(hass, config, request, entity): + """Process a set thermostat mode request.""" + mode = request[API_PAYLOAD]['thermostatMode'] + + operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) + # Work around a pylint false positive due to + # https://github.com/PyCQA/pylint/issues/1830 + # pylint: disable=stop-iteration-return + ha_mode = next( + (k for k, v in API_THERMOSTAT_MODES.items() if v == mode), + None + ) + if ha_mode not in operation_list: + msg = 'The requested thermostat mode {} is not supported'.format(mode) + return api_error( + request, + namespace='Alexa.ThermostatController', + error_type='UNSUPPORTED_THERMOSTAT_MODE', + error_message=msg + ) + + data = { + ATTR_ENTITY_ID: entity.entity_id, + climate.ATTR_OPERATION_MODE: ha_mode, + } + + await hass.services.async_call( + entity.domain, climate.SERVICE_SET_OPERATION_MODE, data, + blocking=False) + + return api_message(request) + + @HANDLERS.register(('Alexa', 'ReportState')) @extract_entity @asyncio.coroutine diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index b7e2412f293..913d6456906 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -3,17 +3,22 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_NOT_RECOGNIZED_TEMPLATE, TEMPERATURE) -def fahrenheit_to_celsius(fahrenheit: float) -> float: +def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float: """Convert a temperature in Fahrenheit to Celsius.""" + if interval: + return fahrenheit / 1.8 return (fahrenheit - 32.0) / 1.8 -def celsius_to_fahrenheit(celsius: float) -> float: +def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float: """Convert a temperature in Celsius to Fahrenheit.""" + if interval: + return celsius * 1.8 return celsius * 1.8 + 32.0 -def convert(temperature: float, from_unit: str, to_unit: str) -> float: +def convert(temperature: float, from_unit: str, to_unit: str, + interval: bool = False) -> float: """Convert a temperature from one unit to another.""" if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT): raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format( @@ -25,5 +30,5 @@ def convert(temperature: float, from_unit: str, to_unit: str) -> float: if from_unit == to_unit: return temperature elif from_unit == TEMP_CELSIUS: - return celsius_to_fahrenheit(temperature) - return fahrenheit_to_celsius(temperature) + return celsius_to_fahrenheit(temperature, interval) + return fahrenheit_to_celsius(temperature, interval) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 8199652d09e..dd404b7d57a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -693,6 +693,133 @@ def test_unknown_sensor(hass): yield from discovery_test(device, hass, expected_endpoints=0) +async def test_thermostat(hass): + """Test thermostat discovery.""" + device = ( + 'climate.test_thermostat', + 'cool', + { + 'operation_mode': 'cool', + 'temperature': 70.0, + 'target_temp_high': 80.0, + 'target_temp_low': 60.0, + 'current_temperature': 75.0, + 'friendly_name': "Test Thermostat", + 'supported_features': 1 | 2 | 4 | 128, + 'operation_list': ['heat', 'cool', 'auto', 'off'], + 'min_temp': 50, + 'max_temp': 90, + 'unit_of_measurement': TEMP_FAHRENHEIT, + } + ) + appliance = await discovery_test(device, hass) + + assert appliance['endpointId'] == 'climate#test_thermostat' + assert appliance['displayCategories'][0] == 'THERMOSTAT' + assert appliance['friendlyName'] == "Test Thermostat" + + assert_endpoint_capabilities( + appliance, + 'Alexa.ThermostatController', + 'Alexa.TemperatureSensor', + ) + + properties = await reported_properties( + hass, 'climate#test_thermostat') + properties.assert_equal( + 'Alexa.ThermostatController', 'thermostatMode', 'COOL') + properties.assert_equal( + 'Alexa.ThermostatController', 'targetSetpoint', + {'value': 70.0, 'scale': 'FAHRENHEIT'}) + properties.assert_equal( + 'Alexa.TemperatureSensor', 'temperature', + {'value': 75.0, 'scale': 'FAHRENHEIT'}) + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpoint': {'value': 69.0, 'scale': 'FAHRENHEIT'}} + ) + assert call.data['temperature'] == 69.0 + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpoint': {'value': 0.0, 'scale': 'CELSIUS'}} + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={ + 'targetSetpoint': {'value': 70.0, 'scale': 'FAHRENHEIT'}, + 'lowerSetpoint': {'value': 293.15, 'scale': 'KELVIN'}, + 'upperSetpoint': {'value': 30.0, 'scale': 'CELSIUS'}, + } + ) + assert call.data['temperature'] == 70.0 + assert call.data['target_temp_low'] == 68.0 + assert call.data['target_temp_high'] == 86.0 + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={ + 'lowerSetpoint': {'value': 273.15, 'scale': 'KELVIN'}, + 'upperSetpoint': {'value': 75.0, 'scale': 'FAHRENHEIT'}, + } + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={ + 'lowerSetpoint': {'value': 293.15, 'scale': 'FAHRENHEIT'}, + 'upperSetpoint': {'value': 75.0, 'scale': 'CELSIUS'}, + } + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'AdjustTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpointDelta': {'value': -10.0, 'scale': 'KELVIN'}} + ) + assert call.data['temperature'] == 52.0 + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'AdjustTargetTemperature', + 'climate#test_thermostat', 'climate.set_temperature', + hass, + payload={'targetSetpointDelta': {'value': 20.0, 'scale': 'CELSIUS'}} + ) + assert msg['event']['payload']['type'] == 'TEMPERATURE_VALUE_OUT_OF_RANGE' + + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetThermostatMode', + 'climate#test_thermostat', 'climate.set_operation_mode', + hass, + payload={'thermostatMode': 'HEAT'} + ) + assert call.data['operation_mode'] == 'heat' + + msg = await assert_request_fails( + 'Alexa.ThermostatController', 'SetThermostatMode', + 'climate#test_thermostat', 'climate.set_operation_mode', + hass, + payload={'thermostatMode': 'INVALID'} + ) + assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE' + + @asyncio.coroutine def test_exclude_filters(hass): """Test exclusion filters.""" From 78f3e01854d82c696578fbeb733902997864f81d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Mar 2018 00:23:02 -0700 Subject: [PATCH 223/924] Fix version bump script --- script/version_bump.py | 157 ++++++++++++++++++++++++----------------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/script/version_bump.py b/script/version_bump.py index 0500fc45957..59060a7075b 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -3,82 +3,97 @@ import argparse import re +from packaging.version import Version + from homeassistant import const -PARSE_PATCH = r'(?P\d+)(\.(?P\D+)(?P\d+))?' +def _bump_release(release, bump_type): + """Bump a release tuple consisting of 3 numbers.""" + major, minor, patch = release + + if bump_type == 'patch': + patch += 1 + elif bump_type == 'minor': + minor += 1 + patch = 0 + + return major, minor, patch -def format_patch(patch_parts): - """Format the patch parts back into a patch string.""" - return '{patch}.{prerel}{prerelversion}'.format(**patch_parts) - - -def bump_version(cur_major, cur_minor, cur_patch, bump_type): +def bump_version(version, bump_type): """Return a new version given a current version and action.""" - patch_parts = re.match(PARSE_PATCH, cur_patch).groupdict() - patch_parts['patch'] = int(patch_parts['patch']) - if patch_parts['prerelversion'] is not None: - patch_parts['prerelversion'] = int(patch_parts['prerelversion']) + to_change = {} - if bump_type == 'release_patch': + if bump_type == 'minor': + # Convert 0.67.3 to 0.68.0 + # Convert 0.67.3.b5 to 0.68.0 + # Convert 0.67.3.dev0 to 0.68.0 + # Convert 0.67.0.b5 to 0.67.0 + # Convert 0.67.0.dev0 to 0.67.0 + to_change['dev'] = None + to_change['pre'] = None + + if not version.is_prerelease or version.release[2] != 0: + to_change['release'] = _bump_release(version.release, 'minor') + + elif bump_type == 'patch': # Convert 0.67.3 to 0.67.4 # Convert 0.67.3.b5 to 0.67.3 # Convert 0.67.3.dev0 to 0.67.3 - new_major = cur_major - new_minor = cur_minor + to_change['dev'] = None + to_change['pre'] = None - if patch_parts['prerel'] is None: - new_patch = str(patch_parts['patch'] + 1) - else: - new_patch = str(patch_parts['patch']) + if not version.is_prerelease: + to_change['release'] = _bump_release(version.release, 'patch') elif bump_type == 'dev': # Convert 0.67.3 to 0.67.4.dev0 # Convert 0.67.3.b5 to 0.67.4.dev0 # Convert 0.67.3.dev0 to 0.67.3.dev1 - new_major = cur_major - - if patch_parts['prerel'] == 'dev': - new_minor = cur_minor - patch_parts['prerelversion'] += 1 - new_patch = format_patch(patch_parts) + if version.is_devrelease: + to_change['dev'] = ('dev', version.dev + 1) else: - new_minor = cur_minor + 1 - new_patch = '0.dev0' + to_change['pre'] = ('dev', 0) + to_change['release'] = _bump_release(version.release, 'minor') elif bump_type == 'beta': - # Convert 0.67.5 to 0.67.8.b0 - # Convert 0.67.0.dev0 to 0.67.0.b0 - # Convert 0.67.5.b4 to 0.67.5.b5 - new_major = cur_major - new_minor = cur_minor + # Convert 0.67.5 to 0.67.6b0 + # Convert 0.67.0.dev0 to 0.67.0b0 + # Convert 0.67.5.b4 to 0.67.5b5 - if patch_parts['prerel'] is None: - patch_parts['patch'] += 1 - patch_parts['prerel'] = 'b' - patch_parts['prerelversion'] = 0 + if version.is_devrelease: + to_change['dev'] = None + to_change['pre'] = ('b', 0) - elif patch_parts['prerel'] == 'b': - patch_parts['prerelversion'] += 1 - - elif patch_parts['prerel'] == 'dev': - patch_parts['prerel'] = 'b' - patch_parts['prerelversion'] = 0 + elif version.is_prerelease: + if version.pre[0] == 'a': + to_change['pre'] = ('b', 0) + if version.pre[0] == 'b': + to_change['pre'] = ('b', version.pre[1] + 1) + else: + to_change['pre'] = ('b', 0) + to_change['release'] = _bump_release(version.release, 'patch') else: - raise Exception('Can only bump from beta or no prerel version') + to_change['release'] = _bump_release(version.release, 'patch') + to_change['pre'] = ('b', 0) - new_patch = format_patch(patch_parts) + else: + assert False, 'Unsupported type: {}'.format(bump_type) - return new_major, new_minor, new_patch + temp = Version('0') + temp._version = version._version._replace(**to_change) + return Version(str(temp)) -def write_version(major, minor, patch): +def write_version(version): """Update Home Assistant constant file with new version.""" with open('homeassistant/const.py') as fil: content = fil.read() + major, minor, patch = str(version).split('.', 2) + content = re.sub('MAJOR_VERSION = .*\n', 'MAJOR_VERSION = {}\n'.format(major), content) @@ -100,35 +115,45 @@ def main(): parser.add_argument( 'type', help="The type of the bump the version to.", - choices=['beta', 'dev', 'release_patch'], + choices=['beta', 'dev', 'patch', 'minor'], ) arguments = parser.parse_args() - write_version(*bump_version(const.MAJOR_VERSION, const.MINOR_VERSION, - const.PATCH_VERSION, arguments.type)) + current = Version(const.__version__) + bumped = bump_version(current, arguments.type) + assert bumped > current, 'BUG! New version is not newer than old version' + write_version(bumped) def test_bump_version(): """Make sure it all works.""" - assert bump_version(0, 56, '0', 'beta') == \ - (0, 56, '1.b0') - assert bump_version(0, 56, '0.b3', 'beta') == \ - (0, 56, '0.b4') - assert bump_version(0, 56, '0.dev0', 'beta') == \ - (0, 56, '0.b0') + assert bump_version(Version('0.56.0'), 'beta') == Version('0.56.1b0') + assert bump_version(Version('0.56.0b3'), 'beta') == Version('0.56.0b4') + assert bump_version(Version('0.56.0.dev0'), 'beta') == Version('0.56.0b0') - assert bump_version(0, 56, '3', 'dev') == \ - (0, 57, '0.dev0') - assert bump_version(0, 56, '0.b3', 'dev') == \ - (0, 57, '0.dev0') - assert bump_version(0, 56, '0.dev0', 'dev') == \ - (0, 56, '0.dev1') + assert bump_version(Version('0.56.3'), 'dev') == Version('0.57.0.dev0') + assert bump_version(Version('0.56.0b3'), 'dev') == Version('0.57.0.dev0') + assert bump_version(Version('0.56.0.dev0'), 'dev') == \ + Version('0.56.0.dev1') - assert bump_version(0, 56, '3', 'release_patch') == \ - (0, 56, '4') - assert bump_version(0, 56, '3.b3', 'release_patch') == \ - (0, 56, '3') - assert bump_version(0, 56, '0.dev0', 'release_patch') == \ - (0, 56, '0') + assert bump_version(Version('0.56.3'), 'patch') == \ + Version('0.56.4') + assert bump_version(Version('0.56.3.b3'), 'patch') == \ + Version('0.56.3') + assert bump_version(Version('0.56.0.dev0'), 'patch') == \ + Version('0.56.0') + + assert bump_version(Version('0.56.0'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.3'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.0.b3'), 'minor') == \ + Version('0.56.0') + assert bump_version(Version('0.56.3.b3'), 'minor') == \ + Version('0.57.0') + assert bump_version(Version('0.56.0.dev0'), 'minor') == \ + Version('0.56.0') + assert bump_version(Version('0.56.2.dev0'), 'minor') == \ + Version('0.57.0') if __name__ == '__main__': From 931bceefd99e29d4128ff5c2f6bf09b522e4cd8a Mon Sep 17 00:00:00 2001 From: Kane610 Date: Fri, 30 Mar 2018 09:34:26 +0200 Subject: [PATCH 224/924] deCONZ config entry (#13402) * Try config entries * Testing * Working flow * Config entry text strings * Removed manual inputs for config flow * Support unloading of config entry * Bump requirement to v33 * Fix comments from test * Make sure that only one deCONZ instance can be set up * Hass doesn't support unloading platforms yet * Modify get_api_key to be testable * Fix hound comments * Add test dependency * Add test for no key * Bump requirement to v35 Add pydeconz to list of test components * Don't have a check in async_setup that domain exists in hass.data --- .../components/deconz/.translations/en.json | 25 +++++ homeassistant/components/deconz/__init__.py | 90 ++++++++++++++++- homeassistant/components/deconz/strings.json | 25 +++++ homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/test_deconz.py | 97 +++++++++++++++++++ 8 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/deconz/.translations/en.json create mode 100644 homeassistant/components/deconz/strings.json create mode 100644 tests/components/test_deconz.py diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json new file mode 100644 index 00000000000..69165dbbbaf --- /dev/null +++ b/homeassistant/components/deconz/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "deCONZ", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port (default value: '80')" + } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "no_bridges": "No deCONZ bridges discovered", + "one_instance_only": "Component only supports one deCONZ instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 26d9fb401e4..85ba271ec3a 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -8,16 +8,17 @@ import logging import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, aiohttp_client from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==32'] +REQUIREMENTS = ['pydeconz==35'] _LOGGER = logging.getLogger(__name__) @@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config): async def async_configuration_callback(data): """Set up actions to do when our configuration callback is called.""" from pydeconz.utils import async_get_api_key - api_key = await async_get_api_key(hass.loop, **deconz_config) + websession = async_get_clientsession(hass) + api_key = await async_get_api_key(websession, **deconz_config) if api_key: deconz_config[CONF_API_KEY] = api_key result = await async_setup_deconz(hass, config, deconz_config) @@ -186,3 +188,85 @@ async def async_request_configuration(hass, config, deconz_config): entity_picture="/static/images/logo_deconz.jpeg", submit_caption="I have unlocked the gateway", ) + + +@config_entries.HANDLERS.register(DOMAIN) +class DeconzFlowHandler(config_entries.ConfigFlowHandler): + """Handle a deCONZ config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the deCONZ flow.""" + self.bridges = [] + self.deconz_config = {} + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + from pydeconz.utils import async_discovery + + if DOMAIN in self.hass.data: + return self.async_abort( + reason='one_instance_only' + ) + + if user_input is not None: + for bridge in self.bridges: + if bridge[CONF_HOST] == user_input[CONF_HOST]: + self.deconz_config = bridge + return await self.async_step_link() + + session = aiohttp_client.async_get_clientsession(self.hass) + self.bridges = await async_discovery(session) + + if len(self.bridges) == 1: + self.deconz_config = self.bridges[0] + return await self.async_step_link() + elif len(self.bridges) > 1: + hosts = [] + for bridge in self.bridges: + hosts.append(bridge[CONF_HOST]) + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(CONF_HOST): vol.In(hosts) + }) + ) + + return self.async_abort( + reason='no_bridges' + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the deCONZ bridge.""" + from pydeconz.utils import async_get_api_key + errors = {} + + if user_input is not None: + session = aiohttp_client.async_get_clientsession(self.hass) + api_key = await async_get_api_key(session, **self.deconz_config) + if api_key: + self.deconz_config[CONF_API_KEY] = api_key + return self.async_create_entry( + title='deCONZ', + data=self.deconz_config + ) + else: + errors['base'] = 'no_key' + + return self.async_show_form( + step_id='link', + errors=errors, + ) + + +async def async_setup_entry(hass, entry): + """Set up a bridge for a config entry.""" + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + result = await async_setup_deconz(hass, None, entry.data) + if result: + return True + return False diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json new file mode 100644 index 00000000000..69165dbbbaf --- /dev/null +++ b/homeassistant/components/deconz/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "deCONZ", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port (default value: '80')" + } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "no_bridges": "No deCONZ bridges discovered", + "one_instance_only": "Component only supports one deCONZ instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b02026ac6dd..6b2000b2ea6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -128,6 +128,7 @@ HANDLERS = Registry() FLOWS = [ 'config_entry_example', 'hue', + 'deconz', ] SOURCE_USER = 'user' diff --git a/requirements_all.txt b/requirements_all.txt index b21452dc385..676945eb42e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -714,7 +714,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==32 +pydeconz==35 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31a7874409a..456bec7d6a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -129,6 +129,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.4.1 +# homeassistant.components.deconz +pydeconz==35 + # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1f5348136c6..fa39c307f18 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -67,6 +67,7 @@ TEST_REQUIREMENTS = ( 'prometheus_client', 'pushbullet.py', 'py-canary', + 'pydeconz', 'pydispatcher', 'PyJWT', 'pylitejet', diff --git a/tests/components/test_deconz.py b/tests/components/test_deconz.py new file mode 100644 index 00000000000..2c7c656d560 --- /dev/null +++ b/tests/components/test_deconz.py @@ -0,0 +1,97 @@ +"""Tests for deCONZ config flow.""" +import pytest + +import voluptuous as vol + +import homeassistant.components.deconz as deconz +import pydeconz + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} + ]) + aioclient_mock.post('http://1.2.3.4:80/api', json=[ + {"success": {"username": "1234567890ABCDEF"}} + ]) + + flow = deconz.DeconzFlowHandler() + flow.hass = hass + await flow.async_step_init() + result = await flow.async_step_link(user_input={}) + + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': '80', + 'api_key': '1234567890ABCDEF' + } + + +async def test_flow_already_registered_bridge(hass, aioclient_mock): + """Test config flow don't allow more than one bridge to be registered.""" + flow = deconz.DeconzFlowHandler() + flow.hass = hass + flow.hass.data[deconz.DOMAIN] = True + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} + ]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': '80'}, + {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': '80'} + ]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_flow_no_api_key(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.post('http://1.2.3.4:80/api', json=[]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', 'port': 80} + + result = await flow.async_step_link(user_input={}) + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'no_key'} From 6314aabc6fba709fe7c09b8ef46070099cb3d9df Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 30 Mar 2018 16:16:29 +0300 Subject: [PATCH 225/924] Remove andrey-git from requirements monitoring (#13547) --- CODEOWNERS | 3 --- 1 file changed, 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9528e7a09e9..932f07573b2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -29,9 +29,6 @@ homeassistant/components/weblink.py @home-assistant/core homeassistant/components/websocket_api.py @home-assistant/core homeassistant/components/zone.py @home-assistant/core -# To monitor non-pypi additions -requirements_all.txt @andrey-git - # HomeAssistant developer Teams Dockerfile @home-assistant/docker virtualization/Docker/* @home-assistant/docker From 979a8f87728b23ffeef78159deb1fa6570ac628a Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Fri, 30 Mar 2018 18:12:57 +0200 Subject: [PATCH 226/924] Fix BMW device tracker toggling state if vehicle tracking is disabled (#12999) * if tracking is disabled, the position is not set in the device tracker. This fixes an issue with a toggling vehicle state. * removed useless attributes --- .../device_tracker/bmw_connected_drive.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py index 1e501c0e199..2267bb51944 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/device_tracker/bmw_connected_drive.py @@ -36,16 +36,20 @@ class BMWDeviceTracker(object): self.vehicle = vehicle def update(self) -> None: - """Update the device info.""" + """Update the device info. + + Only update the state in home assistant if tracking in + the car is enabled. + """ dev_id = slugify(self.vehicle.name) + + if not self.vehicle.state.is_vehicle_tracking_enabled: + _LOGGER.debug('Tracking is disabled for vehicle %s', dev_id) + return + _LOGGER.debug('Updating %s', dev_id) - attrs = { - 'trackr_id': dev_id, - 'id': dev_id, - 'name': self.vehicle.name - } + self._see( dev_id=dev_id, host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, attributes=attrs, - icon='mdi:car' + gps=self.vehicle.state.gps_position, icon='mdi:car' ) From 9cfcd38c1e561c2c195a98babf403bcd4db68fca Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 30 Mar 2018 21:02:02 +0200 Subject: [PATCH 227/924] Xiaomi MiIO Switch: Support for the Xiaomi Chuangmi Plug V3 (#13271) * Device support of the Xiaomi Chuangmi Plug V3 added * Refactoring. * Additional attributes added. * New miio device class used --- .../components/switch/xiaomi_miio.py | 101 ++++++++++-------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/switch/xiaomi_miio.py b/homeassistant/components/switch/xiaomi_miio.py index 6110b6dc469..149acd76c07 100644 --- a/homeassistant/components/switch/xiaomi_miio.py +++ b/homeassistant/components/switch/xiaomi_miio.py @@ -24,6 +24,7 @@ DATA_KEY = 'switch.xiaomi_miio' CONF_MODEL = 'model' MODEL_POWER_STRIP_V2 = 'zimi.powerstrip.v2' +MODEL_PLUG_V3 = 'chuangmi.plug.v3' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, @@ -34,7 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'qmi.powerstrip.v1', 'zimi.powerstrip.v2', 'chuangmi.plug.m1', - 'chuangmi.plug.v2']), + 'chuangmi.plug.v2', + 'chuangmi.plug.v3']), }) REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] @@ -51,18 +53,20 @@ ATTR_PRICE = 'price' SUCCESS = ['ok'] -SUPPORT_SET_POWER_MODE = 1 -SUPPORT_SET_WIFI_LED = 2 -SUPPORT_SET_POWER_PRICE = 4 +FEATURE_SET_POWER_MODE = 1 +FEATURE_SET_WIFI_LED = 2 +FEATURE_SET_POWER_PRICE = 4 -ADDITIONAL_SUPPORT_FLAGS_GENERIC = 0 +FEATURE_FLAGS_GENERIC = 0 -ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 = (SUPPORT_SET_POWER_MODE | - SUPPORT_SET_WIFI_LED | - SUPPORT_SET_POWER_PRICE) +FEATURE_FLAGS_POWER_STRIP_V1 = (FEATURE_SET_POWER_MODE | + FEATURE_SET_WIFI_LED | + FEATURE_SET_POWER_PRICE) -ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 = (SUPPORT_SET_WIFI_LED | - SUPPORT_SET_POWER_PRICE) +FEATURE_FLAGS_POWER_STRIP_V2 = (FEATURE_SET_WIFI_LED | + FEATURE_SET_POWER_PRICE) + +FEATURE_FLAGS_PLUG_V3 = (FEATURE_SET_WIFI_LED) SERVICE_SET_WIFI_LED_ON = 'xiaomi_miio_set_wifi_led_on' SERVICE_SET_WIFI_LED_OFF = 'xiaomi_miio_set_wifi_led_off' @@ -124,29 +128,27 @@ async def async_setup_platform(hass, config, async_add_devices, except DeviceException: raise PlatformNotReady - if model in ['chuangmi.plug.v1']: - from miio import PlugV1 - plug = PlugV1(host, token) + if model in ['chuangmi.plug.v1', 'chuangmi.plug.v3']: + from miio import ChuangmiPlug + plug = ChuangmiPlug(host, token, model=model) # The device has two switchable channels (mains and a USB port). # A switch device per channel will be created. for channel_usb in [True, False]: - device = ChuangMiPlugV1Switch( + device = ChuangMiPlugSwitch( name, plug, model, unique_id, channel_usb) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['qmi.powerstrip.v1', - 'zimi.powerstrip.v2']: + elif model in ['qmi.powerstrip.v1', 'zimi.powerstrip.v2']: from miio import PowerStrip plug = PowerStrip(host, token) device = XiaomiPowerStripSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['chuangmi.plug.m1', - 'chuangmi.plug.v2']: - from miio import Plug - plug = Plug(host, token) + elif model in ['chuangmi.plug.m1', 'chuangmi.plug.v2']: + from miio import ChuangmiPlug + plug = ChuangmiPlug(host, token, model=model) device = XiaomiPlugGenericSwitch(name, plug, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device @@ -204,7 +206,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): ATTR_TEMPERATURE: None, ATTR_MODEL: self._model, } - self._additional_supported_features = ADDITIONAL_SUPPORT_FLAGS_GENERIC + self._device_features = FEATURE_FLAGS_GENERIC self._skip_update = False @property @@ -251,6 +253,10 @@ class XiaomiPlugGenericSwitch(SwitchDevice): _LOGGER.debug("Response received from plug: %s", result) + # The Chuangmi Plug V3 returns 0 on success on usb_on/usb_off. + if func in ['usb_on', 'usb_off'] and result == 0: + return True + return result == SUCCESS except DeviceException as exc: _LOGGER.error(mask_error, exc) @@ -300,7 +306,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_set_wifi_led_on(self): """Turn the wifi led on.""" - if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + if self._device_features & FEATURE_SET_WIFI_LED == 0: return await self._try_command( @@ -309,7 +315,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_set_wifi_led_off(self): """Turn the wifi led on.""" - if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 0: + if self._device_features & FEATURE_SET_WIFI_LED == 0: return await self._try_command( @@ -318,7 +324,7 @@ class XiaomiPlugGenericSwitch(SwitchDevice): async def async_set_power_price(self, price: int): """Set the power price.""" - if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 0: + if self._device_features & FEATURE_SET_POWER_PRICE == 0: return await self._try_command( @@ -331,26 +337,24 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): def __init__(self, name, plug, model, unique_id): """Initialize the plug switch.""" - XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) + super().__init__(name, plug, model, unique_id) if self._model == MODEL_POWER_STRIP_V2: - self._additional_supported_features = \ - ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V2 + self._device_features = FEATURE_FLAGS_POWER_STRIP_V2 else: - self._additional_supported_features = \ - ADDITIONAL_SUPPORT_FLAGS_POWER_STRIP_V1 + self._device_features = FEATURE_FLAGS_POWER_STRIP_V1 self._state_attrs.update({ ATTR_LOAD_POWER: None, }) - if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 1: + if self._device_features & FEATURE_SET_POWER_MODE == 1: self._state_attrs[ATTR_POWER_MODE] = None - if self._additional_supported_features & SUPPORT_SET_WIFI_LED == 1: + if self._device_features & FEATURE_SET_WIFI_LED == 1: self._state_attrs[ATTR_WIFI_LED] = None - if self._additional_supported_features & SUPPORT_SET_POWER_PRICE == 1: + if self._device_features & FEATURE_SET_POWER_PRICE == 1: self._state_attrs[ATTR_POWER_PRICE] = None async def async_update(self): @@ -373,16 +377,16 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): ATTR_LOAD_POWER: state.load_power, }) - if self._additional_supported_features & \ - SUPPORT_SET_POWER_MODE == 1 and state.mode: + if self._device_features & FEATURE_SET_POWER_MODE == 1 and \ + state.mode: self._state_attrs[ATTR_POWER_MODE] = state.mode.value - if self._additional_supported_features & \ - SUPPORT_SET_WIFI_LED == 1 and state.wifi_led: + if self._device_features & FEATURE_SET_WIFI_LED == 1 and \ + state.wifi_led: self._state_attrs[ATTR_WIFI_LED] = state.wifi_led - if self._additional_supported_features & \ - SUPPORT_SET_POWER_PRICE == 1 and state.power_price: + if self._device_features & FEATURE_SET_POWER_PRICE == 1 and \ + state.power_price: self._state_attrs[ATTR_POWER_PRICE] = state.power_price except DeviceException as ex: @@ -391,7 +395,7 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): async def async_set_power_mode(self, mode: str): """Set the power mode.""" - if self._additional_supported_features & SUPPORT_SET_POWER_MODE == 0: + if self._device_features & FEATURE_SET_POWER_MODE == 0: return from miio.powerstrip import PowerMode @@ -401,8 +405,8 @@ class XiaomiPowerStripSwitch(XiaomiPlugGenericSwitch): self._plug.set_power_mode, PowerMode(mode)) -class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): - """Representation of a Chuang Mi Plug V1.""" +class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): + """Representation of a Chuang Mi Plug V1 and V3.""" def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" @@ -411,9 +415,16 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): if unique_id is not None and channel_usb: unique_id = "{}-{}".format(unique_id, 'usb') - XiaomiPlugGenericSwitch.__init__(self, name, plug, model, unique_id) + super().__init__(name, plug, model, unique_id) self._channel_usb = channel_usb + if self._model == MODEL_PLUG_V3: + self._device_features = FEATURE_FLAGS_PLUG_V3 + self._state_attrs.update({ + ATTR_WIFI_LED: None, + ATTR_LOAD_POWER: None, + }) + async def async_turn_on(self, **kwargs): """Turn a channel on.""" if self._channel_usb: @@ -463,6 +474,12 @@ class ChuangMiPlugV1Switch(XiaomiPlugGenericSwitch): ATTR_TEMPERATURE: state.temperature }) + if state.wifi_led: + self._state_attrs[ATTR_WIFI_LED] = state.wifi_led + + if state.load_power: + self._state_attrs[ATTR_LOAD_POWER] = state.load_power + except DeviceException as ex: self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) From 8fad97a47a7e6ca996a753da1b0ed31cc7005867 Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Fri, 30 Mar 2018 21:33:30 +0200 Subject: [PATCH 228/924] Add FreeDNS component (#13526) * Add FreeDNS component * Implement review changes in FreeDNS component * Implement review changes in FreeDNS component * Implement review changes in FreeDNS component --- homeassistant/components/freedns.py | 103 ++++++++++++++++++++++++++++ tests/components/test_freedns.py | 69 +++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 homeassistant/components/freedns.py create mode 100644 tests/components/test_freedns.py diff --git a/homeassistant/components/freedns.py b/homeassistant/components/freedns.py new file mode 100644 index 00000000000..0512030bdcb --- /dev/null +++ b/homeassistant/components/freedns.py @@ -0,0 +1,103 @@ +""" +Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/freedns/ +""" +import asyncio +from datetime import timedelta +import logging + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import (CONF_URL, CONF_ACCESS_TOKEN) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'freedns' + +DEFAULT_INTERVAL = timedelta(minutes=10) + +TIMEOUT = 10 +UPDATE_URL = 'https://freedns.afraid.org/dynamic/update.php' + +CONF_UPDATE_INTERVAL = 'update_interval' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Exclusive(CONF_URL, DOMAIN): cv.string, + vol.Exclusive(CONF_ACCESS_TOKEN, DOMAIN): cv.string, + vol.Optional(CONF_UPDATE_INTERVAL, default=DEFAULT_INTERVAL): vol.All( + cv.time_period, cv.positive_timedelta), + + }) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Initialize the FreeDNS component.""" + url = config[DOMAIN].get(CONF_URL) + auth_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) + update_interval = config[DOMAIN].get(CONF_UPDATE_INTERVAL) + + session = hass.helpers.aiohttp_client.async_get_clientsession() + + result = yield from _update_freedns( + hass, session, url, auth_token) + + if result is False: + return False + + @asyncio.coroutine + def update_domain_callback(now): + """Update the FreeDNS entry.""" + yield from _update_freedns(hass, session, url, auth_token) + + hass.helpers.event.async_track_time_interval( + update_domain_callback, update_interval) + + return True + + +@asyncio.coroutine +def _update_freedns(hass, session, url, auth_token): + """Update FreeDNS.""" + params = None + + if url is None: + url = UPDATE_URL + + if auth_token is not None: + params = {} + params[auth_token] = "" + + try: + with async_timeout.timeout(TIMEOUT, loop=hass.loop): + resp = yield from session.get(url, params=params) + body = yield from resp.text() + + if "has not changed" in body: + # IP has not changed. + _LOGGER.debug("FreeDNS update skipped: IP has not changed") + return True + + if "ERROR" not in body: + _LOGGER.debug("Updating FreeDNS was successful: %s", body) + return True + + if "Invalid update URL" in body: + _LOGGER.error("FreeDNS update token is invalid") + else: + _LOGGER.warning("Updating FreeDNS failed: %s", body) + + except aiohttp.ClientError: + _LOGGER.warning("Can't connect to FreeDNS API") + + except asyncio.TimeoutError: + _LOGGER.warning("Timeout from FreeDNS API at %s", url) + + return False diff --git a/tests/components/test_freedns.py b/tests/components/test_freedns.py new file mode 100644 index 00000000000..b8e38e9c3a8 --- /dev/null +++ b/tests/components/test_freedns.py @@ -0,0 +1,69 @@ +"""Test the FreeDNS component.""" +import asyncio +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components import freedns +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed + +ACCESS_TOKEN = 'test_token' +UPDATE_INTERVAL = freedns.DEFAULT_INTERVAL +UPDATE_URL = freedns.UPDATE_URL + + +@pytest.fixture +def setup_freedns(hass, aioclient_mock): + """Fixture that sets up FreeDNS.""" + params = {} + params[ACCESS_TOKEN] = "" + aioclient_mock.get( + UPDATE_URL, params=params, text='Successfully updated 1 domains.') + + hass.loop.run_until_complete(async_setup_component(hass, freedns.DOMAIN, { + freedns.DOMAIN: { + 'access_token': ACCESS_TOKEN, + 'update_interval': UPDATE_INTERVAL, + } + })) + + +@asyncio.coroutine +def test_setup(hass, aioclient_mock): + """Test setup works if update passes.""" + params = {} + params[ACCESS_TOKEN] = "" + aioclient_mock.get( + UPDATE_URL, params=params, text='ERROR: Address has not changed.') + + result = yield from async_setup_component(hass, freedns.DOMAIN, { + freedns.DOMAIN: { + 'access_token': ACCESS_TOKEN, + 'update_interval': UPDATE_INTERVAL, + } + }) + assert result + assert aioclient_mock.call_count == 1 + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + yield from hass.async_block_till_done() + assert aioclient_mock.call_count == 2 + + +@asyncio.coroutine +def test_setup_fails_if_wrong_token(hass, aioclient_mock): + """Test setup fails if first update fails through wrong token.""" + params = {} + params[ACCESS_TOKEN] = "" + aioclient_mock.get( + UPDATE_URL, params=params, text='ERROR: Invalid update URL (2)') + + result = yield from async_setup_component(hass, freedns.DOMAIN, { + freedns.DOMAIN: { + 'access_token': ACCESS_TOKEN, + 'update_interval': UPDATE_INTERVAL, + } + }) + assert not result + assert aioclient_mock.call_count == 1 From 0911166c9c89d82a957bc177deb40bc3245830b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 30 Mar 2018 22:34:16 +0300 Subject: [PATCH 229/924] Update pylint to 1.8.3 (#13544) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index afcdec23a00..38b716406fd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.580 pydocstyle==1.1.1 -pylint==1.8.2 +pylint==1.8.3 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 456bec7d6a8..a6b655e95f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.580 pydocstyle==1.1.1 -pylint==1.8.2 +pylint==1.8.3 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From 0f24fea6bbbbeda45e4608eed3af2ed0add7408b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Fri, 30 Mar 2018 22:47:20 +0200 Subject: [PATCH 230/924] Google Maps location sharing device tracker (#12301) * Google Maps location sharing device tracker. * Use ConfigType and change debug logging to _LOGGER.debug() * Update to locationsharinglib 0.3.0 * Remove unneeded lines. * Use hass.config.path for config file location. * Fixed remarks * Return boolean in setup_scanner --- .coveragerc | 1 + .../components/device_tracker/google_maps.py | 83 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 87 insertions(+) create mode 100644 homeassistant/components/device_tracker/google_maps.py diff --git a/.coveragerc b/.coveragerc index d6cc126ef52..65f29767673 100644 --- a/.coveragerc +++ b/.coveragerc @@ -374,6 +374,7 @@ omit = homeassistant/components/device_tracker/cisco_ios.py homeassistant/components/device_tracker/ddwrt.py homeassistant/components/device_tracker/fritz.py + homeassistant/components/device_tracker/google_maps.py homeassistant/components/device_tracker/gpslogger.py homeassistant/components/device_tracker/hitron_coda.py homeassistant/components/device_tracker/huawei_router.py diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py new file mode 100644 index 00000000000..9e257616361 --- /dev/null +++ b/homeassistant/components/device_tracker/google_maps.py @@ -0,0 +1,83 @@ +""" +Support for Google Maps location sharing. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/device_tracker.google_maps/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import ( + PLATFORM_SCHEMA, SOURCE_TYPE_GPS) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['locationsharinglib==0.4.0'] + +CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' + +MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) + + +def setup_scanner(hass, config: ConfigType, see, discovery_info=None): + """Set up the scanner.""" + scanner = GoogleMapsScanner(hass, config, see) + return scanner.success_init + + +class GoogleMapsScanner(object): + """Representation of an Google Maps location sharing account.""" + + def __init__(self, hass, config: ConfigType, see) -> None: + """Initialize the scanner.""" + from locationsharinglib import Service + from locationsharinglib.locationsharinglibexceptions import InvalidUser + + self.see = see + self.username = config[CONF_USERNAME] + self.password = config[CONF_PASSWORD] + + try: + self.service = Service(self.username, self.password, + hass.config.path(CREDENTIALS_FILE)) + self._update_info() + + track_time_interval( + hass, self._update_info, MIN_TIME_BETWEEN_SCANS) + + self.success_init = True + + except InvalidUser: + _LOGGER.error('You have specified invalid login credentials') + self.success_init = False + + def _update_info(self, now=None): + for person in self.service.get_all_people(): + dev_id = 'google_maps_{0}'.format(slugify(person.id)) + + attrs = { + 'id': person.id, + 'nickname': person.nickname, + 'full_name': person.full_name, + 'last_seen': person.datetime, + 'address': person.address + } + self.see( + dev_id=dev_id, + gps=(person.latitude, person.longitude), + picture=person.picture_url, + source_type=SOURCE_TYPE_GPS, + attributes=attrs + ) diff --git a/requirements_all.txt b/requirements_all.txt index 676945eb42e..bd0a55d7c43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -480,6 +480,9 @@ liveboxplaytv==2.0.2 # homeassistant.components.notify.lametric lmnotify==0.0.4 +# homeassistant.components.device_tracker.google_maps +locationsharinglib==0.4.0 + # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From c361b0c4500bec29e79fb6b031d09bed71695665 Mon Sep 17 00:00:00 2001 From: Jonas Skoogh Date: Fri, 30 Mar 2018 22:50:08 +0200 Subject: [PATCH 231/924] Check_config: Handle numbers correctly when printing config (#13377) --- homeassistant/scripts/check_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index ac3ac62e82d..8c78602f3d0 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -252,7 +252,7 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): """ def sort_dict_key(val): """Return the dict key for sorting.""" - key = str.lower(val[0]) + key = str(val[0]).lower() return '0' if key == 'platform' else key indent_str = indent_count * ' ' @@ -261,10 +261,10 @@ def dump_dict(layer, indent_count=3, listi=False, **kwargs): if isinstance(layer, Dict): for key, value in sorted(layer.items(), key=sort_dict_key): if isinstance(value, (dict, list)): - print(indent_str, key + ':', line_info(value, **kwargs)) + print(indent_str, str(key) + ':', line_info(value, **kwargs)) dump_dict(value, indent_count + 2) else: - print(indent_str, key + ':', value) + print(indent_str, str(key) + ':', value) indent_str = indent_count * ' ' if isinstance(layer, Sequence): for i in layer: From f40efe0110e8638987a0447aa81392940d7ffd78 Mon Sep 17 00:00:00 2001 From: dramamoose Date: Fri, 30 Mar 2018 15:10:25 -0600 Subject: [PATCH 232/924] Fix FLUX_LED error when no color is set (#13527) * Handle turn_on situation when no color is set As is, an error gets thrown when turn_on is called without an HS value. By adding an if statement, we only try to set RGB if an HS value is applied. * Fix Whitespace Issues * Made Requested Changes --- homeassistant/components/light/flux_led.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index ed0836f1449..6ffdcc0bb4a 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -204,7 +204,12 @@ class FluxLight(Light): self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) - rgb = color_util.color_hs_to_RGB(*hs_color) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) From 9fc8a8f67900a3db5be22e9d5a589a37db46b823 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Fri, 30 Mar 2018 04:57:19 +0200 Subject: [PATCH 233/924] Check whitelisted paths #13107 (#13154) --- homeassistant/core.py | 10 +++++++--- tests/test_core.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 65db82a1fbe..feb8d331ae8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1060,15 +1060,19 @@ class Config(object): """Check if the path is valid for access from outside.""" assert path is not None - parent = pathlib.Path(path) + thepath = pathlib.Path(path) try: - parent = parent.resolve() # pylint: disable=no-member + # The file path does not have to exist (it's parent should) + if thepath.exists(): + thepath = thepath.resolve() + else: + thepath = thepath.parent.resolve() except (FileNotFoundError, RuntimeError, PermissionError): return False for whitelisted_path in self.whitelist_external_dirs: try: - parent.relative_to(whitelisted_path) + thepath.relative_to(whitelisted_path) return True except ValueError: pass diff --git a/tests/test_core.py b/tests/test_core.py index 7a1610c0966..1fcd9416f36 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -809,7 +809,8 @@ class TestConfig(unittest.TestCase): valid = [ test_file, - tmp_dir + tmp_dir, + os.path.join(tmp_dir, 'notfound321') ] for path in valid: assert self.config.is_allowed_path(path) From 0f2cfe7f2760a32ed287e1de0d9fe30f2e3e6b7e Mon Sep 17 00:00:00 2001 From: dramamoose Date: Fri, 30 Mar 2018 15:10:25 -0600 Subject: [PATCH 234/924] Fix FLUX_LED error when no color is set (#13527) * Handle turn_on situation when no color is set As is, an error gets thrown when turn_on is called without an HS value. By adding an if statement, we only try to set RGB if an HS value is applied. * Fix Whitespace Issues * Made Requested Changes --- homeassistant/components/light/flux_led.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index ed0836f1449..6ffdcc0bb4a 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -204,7 +204,12 @@ class FluxLight(Light): self._bulb.turnOn() hs_color = kwargs.get(ATTR_HS_COLOR) - rgb = color_util.color_hs_to_RGB(*hs_color) + + if hs_color: + rgb = color_util.color_hs_to_RGB(*hs_color) + else: + rgb = None + brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) From 4dea55b29c665955d27d4d1913589d51fae62ca9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Mar 2018 14:11:32 -0700 Subject: [PATCH 235/924] Version bump to 0.66.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ccb75634601..3dce8882015 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ad5a11ba3d379c414bf4e2cb3d0d0529ea7d779e Mon Sep 17 00:00:00 2001 From: Joe Lu Date: Fri, 30 Mar 2018 14:38:29 -0700 Subject: [PATCH 236/924] Add support for Canary Flex (#13280) Add support for Canary Flex --- homeassistant/components/canary.py | 2 +- homeassistant/components/sensor/canary.py | 22 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_canary.py | 57 +++++++++++++++++++---- tests/components/test_canary.py | 6 ++- 6 files changed, 72 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/canary.py b/homeassistant/components/canary.py index 03825bf48a9..4d0fbe617b2 100644 --- a/homeassistant/components/canary.py +++ b/homeassistant/components/canary.py @@ -15,7 +15,7 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ['py-canary==0.4.1'] +REQUIREMENTS = ['py-canary==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py index ded8f36203e..51fe1d4dd7a 100644 --- a/homeassistant/components/sensor/canary.py +++ b/homeassistant/components/sensor/canary.py @@ -8,6 +8,7 @@ https://home-assistant.io/components/sensor.canary/ from homeassistant.components.canary import DATA_CANARY from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['canary'] @@ -17,9 +18,11 @@ ATTR_AIR_QUALITY = "air_quality" # Sensor types are defined like so: # sensor type name, unit_of_measurement, icon SENSOR_TYPES = [ - ["temperature", TEMP_CELSIUS, "mdi:thermometer"], - ["humidity", "%", "mdi:water-percent"], - ["air_quality", None, "mdi:weather-windy"], + ["temperature", TEMP_CELSIUS, "mdi:thermometer", ["Canary"]], + ["humidity", "%", "mdi:water-percent", ["Canary"]], + ["air_quality", None, "mdi:weather-windy", ["Canary"]], + ["wifi", "dBm", "mdi:wifi", ["Canary Flex"]], + ["battery", "%", "mdi:battery-50", ["Canary Flex"]], ] STATE_AIR_QUALITY_NORMAL = "normal" @@ -35,9 +38,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for location in data.locations: for device in location.devices: if device.is_online: + device_type = device.device_type for sensor_type in SENSOR_TYPES: - devices.append(CanarySensor(data, sensor_type, location, - device)) + if device_type.get("name") in sensor_type[3]: + devices.append(CanarySensor(data, sensor_type, + location, device)) add_devices(devices, True) @@ -80,6 +85,9 @@ class CanarySensor(Entity): @property def icon(self): """Icon for the sensor.""" + if self.state is not None and self._sensor_type[0] == "battery": + return icon_for_battery_level(battery_level=self.state) + return self._sensor_type[2] @property @@ -113,6 +121,10 @@ class CanarySensor(Entity): canary_sensor_type = SensorType.TEMPERATURE elif self._sensor_type[0] == "humidity": canary_sensor_type = SensorType.HUMIDITY + elif self._sensor_type[0] == "wifi": + canary_sensor_type = SensorType.WIFI + elif self._sensor_type[0] == "battery": + canary_sensor_type = SensorType.BATTERY value = self._data.get_reading(self._device_id, canary_sensor_type) diff --git a/requirements_all.txt b/requirements_all.txt index bd0a55d7c43..ccdb5fc5669 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -639,7 +639,7 @@ pwmled==1.2.1 py-august==0.4.0 # homeassistant.components.canary -py-canary==0.4.1 +py-canary==0.5.0 # homeassistant.components.sensor.cpuspeed py-cpuinfo==3.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6b655e95f8..ef28e3a25e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -127,7 +127,7 @@ prometheus_client==0.1.0 pushbullet.py==0.11.0 # homeassistant.components.canary -py-canary==0.4.1 +py-canary==0.5.0 # homeassistant.components.deconz pydeconz==35 diff --git a/tests/components/sensor/test_canary.py b/tests/components/sensor/test_canary.py index 79e2bf4ee35..346929a4685 100644 --- a/tests/components/sensor/test_canary.py +++ b/tests/components/sensor/test_canary.py @@ -40,9 +40,9 @@ class TestCanarySensorSetup(unittest.TestCase): def test_setup_sensors(self): """Test the sensor setup.""" - online_device_at_home = mock_device(20, "Dining Room", True) - offline_device_at_home = mock_device(21, "Front Yard", False) - online_device_at_work = mock_device(22, "Office", True) + online_device_at_home = mock_device(20, "Dining Room", True, "Canary") + offline_device_at_home = mock_device(21, "Front Yard", False, "Canary") + online_device_at_work = mock_device(22, "Office", True, "Canary") self.hass.data[DATA_CANARY] = Mock() self.hass.data[DATA_CANARY].locations = [ @@ -57,7 +57,7 @@ class TestCanarySensorSetup(unittest.TestCase): def test_temperature_sensor(self): """Test temperature sensor with fahrenheit.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home", False) data = Mock() @@ -69,10 +69,11 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Temperature", sensor.name) self.assertEqual("°C", sensor.unit_of_measurement) self.assertEqual(21.12, sensor.state) + self.assertEqual("mdi:thermometer", sensor.icon) def test_temperature_sensor_with_none_sensor_value(self): """Test temperature sensor with fahrenheit.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home", False) data = Mock() @@ -85,7 +86,7 @@ class TestCanarySensorSetup(unittest.TestCase): def test_humidity_sensor(self): """Test humidity sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -97,10 +98,11 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Humidity", sensor.name) self.assertEqual("%", sensor.unit_of_measurement) self.assertEqual(50.46, sensor.state) + self.assertEqual("mdi:water-percent", sensor.icon) def test_air_quality_sensor_with_very_abnormal_reading(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -112,13 +114,14 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Air Quality", sensor.name) self.assertEqual(None, sensor.unit_of_measurement) self.assertEqual(0.4, sensor.state) + self.assertEqual("mdi:weather-windy", sensor.icon) air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] self.assertEqual(STATE_AIR_QUALITY_VERY_ABNORMAL, air_quality) def test_air_quality_sensor_with_abnormal_reading(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -130,13 +133,14 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Air Quality", sensor.name) self.assertEqual(None, sensor.unit_of_measurement) self.assertEqual(0.59, sensor.state) + self.assertEqual("mdi:weather-windy", sensor.icon) air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] self.assertEqual(STATE_AIR_QUALITY_ABNORMAL, air_quality) def test_air_quality_sensor_with_normal_reading(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -148,13 +152,14 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual("Home Family Room Air Quality", sensor.name) self.assertEqual(None, sensor.unit_of_measurement) self.assertEqual(1.0, sensor.state) + self.assertEqual("mdi:weather-windy", sensor.icon) air_quality = sensor.device_state_attributes[ATTR_AIR_QUALITY] self.assertEqual(STATE_AIR_QUALITY_NORMAL, air_quality) def test_air_quality_sensor_with_none_sensor_value(self): """Test air quality sensor.""" - device = mock_device(10, "Family Room") + device = mock_device(10, "Family Room", "Canary") location = mock_location("Home") data = Mock() @@ -165,3 +170,35 @@ class TestCanarySensorSetup(unittest.TestCase): self.assertEqual(None, sensor.state) self.assertEqual(None, sensor.device_state_attributes) + + def test_battery_sensor(self): + """Test battery sensor.""" + device = mock_device(10, "Family Room", "Canary Flex") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = 70.4567 + + sensor = CanarySensor(data, SENSOR_TYPES[4], location, device) + sensor.update() + + self.assertEqual("Home Family Room Battery", sensor.name) + self.assertEqual("%", sensor.unit_of_measurement) + self.assertEqual(70.46, sensor.state) + self.assertEqual("mdi:battery-70", sensor.icon) + + def test_wifi_sensor(self): + """Test battery sensor.""" + device = mock_device(10, "Family Room", "Canary Flex") + location = mock_location("Home") + + data = Mock() + data.get_reading.return_value = -57 + + sensor = CanarySensor(data, SENSOR_TYPES[3], location, device) + sensor.update() + + self.assertEqual("Home Family Room Wifi", sensor.name) + self.assertEqual("dBm", sensor.unit_of_measurement) + self.assertEqual(-57, sensor.state) + self.assertEqual("mdi:wifi", sensor.icon) diff --git a/tests/components/test_canary.py b/tests/components/test_canary.py index 2c496c26e11..310f3be9f05 100644 --- a/tests/components/test_canary.py +++ b/tests/components/test_canary.py @@ -8,12 +8,16 @@ from tests.common import ( get_test_home_assistant) -def mock_device(device_id, name, is_online=True): +def mock_device(device_id, name, is_online=True, device_type_name=None): """Mock Canary Device class.""" device = MagicMock() type(device).device_id = PropertyMock(return_value=device_id) type(device).name = PropertyMock(return_value=name) type(device).is_online = PropertyMock(return_value=is_online) + type(device).device_type = PropertyMock(return_value={ + "id": 1, + "name": device_type_name, + }) return device From bf5894568045a2934d7e7584e6ce61ac7a4afbe6 Mon Sep 17 00:00:00 2001 From: PhracturedBlue Date: Fri, 30 Mar 2018 15:48:31 -0700 Subject: [PATCH 237/924] Fixes #12758. Try other cameras even if one fails to initialize (#13276) --- homeassistant/components/amcrest.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index b91f1fae565..90331a3014e 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -10,6 +10,7 @@ from datetime import timedelta import aiohttp import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectionError as ConnectError from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, @@ -93,14 +94,15 @@ def setup(hass, config): amcrest_cams = config[DOMAIN] for device in amcrest_cams: - camera = AmcrestCamera(device.get(CONF_HOST), - device.get(CONF_PORT), - device.get(CONF_USERNAME), - device.get(CONF_PASSWORD)).camera try: + camera = AmcrestCamera(device.get(CONF_HOST), + device.get(CONF_PORT), + device.get(CONF_USERNAME), + device.get(CONF_PASSWORD)).camera + # pylint: disable=pointless-statement camera.current_time - except (ConnectTimeout, HTTPError) as ex: + except (ConnectError, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Amcrest camera: %s", str(ex)) hass.components.persistent_notification.create( 'Error: {}
' @@ -108,7 +110,7 @@ def setup(hass, config): ''.format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - return False + continue ffmpeg_arguments = device.get(CONF_FFMPEG_ARGUMENTS) name = device.get(CONF_NAME) From bf44dc422ceb546cc2f29b2c50b4277f480b4cd1 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Fri, 30 Mar 2018 20:22:48 -0400 Subject: [PATCH 238/924] Added HassOpenCover and HassCloseCover intents (#13372) * Added intents to cover * Added test for cover intents * Style fixes * Reverted reversions * Async fixes * Woof * Added conditional loading * Added conditional loading * Added conditional loading * Moved tests, fixed logic * Moved tests, fixed logic * Pylint * Pylint * Refactored componenet registration * Refactored componenet registration * Lint --- homeassistant/components/conversation.py | 32 ++++- homeassistant/components/cover/__init__.py | 10 ++ tests/components/cover/test_init.py | 49 ++++++++ tests/components/test_conversation.py | 130 ++++++++++++--------- tests/components/test_init.py | 47 ++++---- 5 files changed, 185 insertions(+), 83 deletions(-) create mode 100755 tests/components/cover/test_init.py diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation.py index e96694ce0a3..ddd96c99177 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation.py @@ -13,10 +13,14 @@ from homeassistant import core from homeassistant.components import http from homeassistant.components.http.data_validator import ( RequestDataValidator) +from homeassistant.components.cover import (INTENT_OPEN_COVER, + INTENT_CLOSE_COVER) +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers import intent - from homeassistant.loader import bind_hass +from homeassistant.setup import (ATTR_COMPONENT) _LOGGER = logging.getLogger(__name__) @@ -28,6 +32,13 @@ DOMAIN = 'conversation' REGEX_TURN_COMMAND = re.compile(r'turn (?P(?: |\w)+) (?P\w+)') REGEX_TYPE = type(re.compile('')) +UTTERANCES = { + 'cover': { + INTENT_OPEN_COVER: ['Open [the] [a] [an] {name}[s]'], + INTENT_CLOSE_COVER: ['Close [the] [a] [an] {name}[s]'] + } +} + SERVICE_PROCESS = 'process' SERVICE_PROCESS_SCHEMA = vol.Schema({ @@ -112,6 +123,25 @@ async def async_setup(hass, config): '[the] [a] [an] {name}[s] toggle', ]) + @callback + def register_utterances(component): + """Register utterances for a component.""" + if component not in UTTERANCES: + return + for intent_type, sentences in UTTERANCES[component].items(): + async_register(hass, intent_type, sentences) + + @callback + def component_loaded(event): + """Handle a new component loaded.""" + register_utterances(event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + + # Check already loaded components. + for component in hass.config.components: + register_utterances(component) + return True diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index b24361d8293..e4c8f5634cf 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa import homeassistant.helpers.config_validation as cv from homeassistant.components import group +from homeassistant.helpers import intent from homeassistant.const import ( SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, SERVICE_OPEN_COVER_TILT, SERVICE_CLOSE_COVER_TILT, @@ -55,6 +56,9 @@ ATTR_CURRENT_TILT_POSITION = 'current_tilt_position' ATTR_POSITION = 'position' ATTR_TILT_POSITION = 'tilt_position' +INTENT_OPEN_COVER = 'HassOpenCover' +INTENT_CLOSE_COVER = 'HassCloseCover' + COVER_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -181,6 +185,12 @@ async def async_setup(hass, config): hass.services.async_register( DOMAIN, service_name, async_handle_cover_service, schema=schema) + hass.helpers.intent.async_register(intent.ServiceIntentHandler( + INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, + "Opened {}")) + hass.helpers.intent.async_register(intent.ServiceIntentHandler( + INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, + "Closed {}")) return True diff --git a/tests/components/cover/test_init.py b/tests/components/cover/test_init.py new file mode 100755 index 00000000000..5df492d3d47 --- /dev/null +++ b/tests/components/cover/test_init.py @@ -0,0 +1,49 @@ +"""The tests for the cover platform.""" + +from homeassistant.components.cover import (SERVICE_OPEN_COVER, + SERVICE_CLOSE_COVER) +from homeassistant.components import intent +import homeassistant.components as comps +from tests.common import async_mock_service + + +async def test_open_cover_intent(hass): + """Test HassOpenCover intent.""" + result = await comps.cover.async_setup(hass, {}) + assert result + + hass.states.async_set('cover.garage_door', 'closed') + calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}} + ) + await hass.async_block_till_done() + + assert response.speech['plain']['speech'] == 'Opened garage door' + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'cover' + assert call.service == 'open_cover' + assert call.data == {'entity_id': 'cover.garage_door'} + + +async def test_close_cover_intent(hass): + """Test HassCloseCover intent.""" + result = await comps.cover.async_setup(hass, {}) + assert result + + hass.states.async_set('cover.garage_door', 'open') + calls = async_mock_service(hass, 'cover', SERVICE_CLOSE_COVER) + + response = await intent.async_handle( + hass, 'test', 'HassCloseCover', {'name': {'value': 'garage door'}} + ) + await hass.async_block_till_done() + + assert response.speech['plain']['speech'] == 'Closed garage door' + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'cover' + assert call.service == 'close_cover' + assert call.data == {'entity_id': 'cover.garage_door'} diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index bde00e10928..d9c29cdae83 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -1,26 +1,24 @@ """The tests for the Conversation component.""" # pylint: disable=protected-access -import asyncio - import pytest from homeassistant.setup import async_setup_component from homeassistant.components import conversation import homeassistant.components as component +from homeassistant.components.cover import (SERVICE_OPEN_COVER) from homeassistant.helpers import intent from tests.common import async_mock_intent, async_mock_service -@asyncio.coroutine -def test_calling_intent(hass): +async def test_calling_intent(hass): """Test calling an intent from a conversation.""" intents = async_mock_intent(hass, 'OrderBeer') - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', { + result = await async_setup_component(hass, 'conversation', { 'conversation': { 'intents': { 'OrderBeer': [ @@ -31,11 +29,11 @@ def test_calling_intent(hass): }) assert result - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: 'I would like the Grolsch beer' }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -45,8 +43,7 @@ def test_calling_intent(hass): assert intent.text_input == 'I would like the Grolsch beer' -@asyncio.coroutine -def test_register_before_setup(hass): +async def test_register_before_setup(hass): """Test calling an intent from a conversation.""" intents = async_mock_intent(hass, 'OrderBeer') @@ -54,7 +51,7 @@ def test_register_before_setup(hass): 'A {type} beer, please' ]) - result = yield from async_setup_component(hass, 'conversation', { + result = await async_setup_component(hass, 'conversation', { 'conversation': { 'intents': { 'OrderBeer': [ @@ -65,11 +62,11 @@ def test_register_before_setup(hass): }) assert result - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: 'A Grolsch beer, please' }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -78,11 +75,11 @@ def test_register_before_setup(hass): assert intent.slots == {'type': {'value': 'Grolsch'}} assert intent.text_input == 'A Grolsch beer, please' - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: 'I would like the Grolsch beer' }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 2 intent = intents[1] @@ -92,14 +89,14 @@ def test_register_before_setup(hass): assert intent.text_input == 'I would like the Grolsch beer' -@asyncio.coroutine -def test_http_processing_intent(hass, aiohttp_client): +async def test_http_processing_intent(hass, test_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): + """Test Intent Handler.""" + intent_type = 'OrderBeer' - @asyncio.coroutine - def async_handle(self, intent): + async def async_handle(self, intent): """Handle the intent.""" response = intent.create_response() response.async_set_speech( @@ -111,7 +108,7 @@ def test_http_processing_intent(hass, aiohttp_client): intent.async_register(hass, TestIntentHandler()) - result = yield from async_setup_component(hass, 'conversation', { + result = await async_setup_component(hass, 'conversation', { 'conversation': { 'intents': { 'OrderBeer': [ @@ -122,13 +119,13 @@ def test_http_processing_intent(hass, aiohttp_client): }) assert result - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.post('/api/conversation/process', json={ + client = await test_client(hass.http.app) + resp = await client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data == { 'card': { @@ -145,24 +142,23 @@ def test_http_processing_intent(hass, aiohttp_client): } -@asyncio.coroutine @pytest.mark.parametrize('sentence', ('turn on kitchen', 'turn kitchen on')) -def test_turn_on_intent(hass, sentence): +async def test_turn_on_intent(hass, sentence): """Test calling the turn on intent.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: sentence }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] @@ -171,24 +167,49 @@ def test_turn_on_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine -@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off')) -def test_turn_off_intent(hass, sentence): - """Test calling the turn on intent.""" - result = yield from component.async_setup(hass, {}) +async def test_cover_intents_loading(hass): + """Test Cover Intents Loading.""" + with pytest.raises(intent.UnknownIntent): + await intent.async_handle( + hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}} + ) + + result = await async_setup_component(hass, 'cover', {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + hass.states.async_set('cover.garage_door', 'closed') + calls = async_mock_service(hass, 'cover', SERVICE_OPEN_COVER) + + response = await intent.async_handle( + hass, 'test', 'HassOpenCover', {'name': {'value': 'garage door'}} + ) + await hass.async_block_till_done() + + assert response.speech['plain']['speech'] == 'Opened garage door' + assert len(calls) == 1 + call = calls[0] + assert call.domain == 'cover' + assert call.service == 'open_cover' + assert call.data == {'entity_id': 'cover.garage_door'} + + +@pytest.mark.parametrize('sentence', ('turn off kitchen', 'turn kitchen off')) +async def test_turn_off_intent(hass, sentence): + """Test calling the turn on intent.""" + result = await component.async_setup(hass, {}) + assert result + + result = await async_setup_component(hass, 'conversation', {}) assert result hass.states.async_set('light.kitchen', 'on') calls = async_mock_service(hass, 'homeassistant', 'turn_off') - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: sentence }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] @@ -197,24 +218,23 @@ def test_turn_off_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine @pytest.mark.parametrize('sentence', ('toggle kitchen', 'kitchen toggle')) -def test_toggle_intent(hass, sentence): +async def test_toggle_intent(hass, sentence): """Test calling the turn on intent.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result hass.states.async_set('light.kitchen', 'on') calls = async_mock_service(hass, 'homeassistant', 'toggle') - yield from hass.services.async_call( + await hass.services.async_call( 'conversation', 'process', { conversation.ATTR_TEXT: sentence }) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(calls) == 1 call = calls[0] @@ -223,20 +243,19 @@ def test_toggle_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine -def test_http_api(hass, aiohttp_client): +async def test_http_api(hass, test_client): """Test the HTTP conversation API.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result - client = yield from aiohttp_client(hass.http.app) + client = await test_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') - resp = yield from client.post('/api/conversation/process', json={ + resp = await client.post('/api/conversation/process', json={ 'text': 'Turn the kitchen on' }) assert resp.status == 200 @@ -248,23 +267,22 @@ def test_http_api(hass, aiohttp_client): assert call.data == {'entity_id': 'light.kitchen'} -@asyncio.coroutine -def test_http_api_wrong_data(hass, aiohttp_client): +async def test_http_api_wrong_data(hass, test_client): """Test the HTTP conversation API.""" - result = yield from component.async_setup(hass, {}) + result = await component.async_setup(hass, {}) assert result - result = yield from async_setup_component(hass, 'conversation', {}) + result = await async_setup_component(hass, 'conversation', {}) assert result - client = yield from aiohttp_client(hass.http.app) + client = await test_client(hass.http.app) - resp = yield from client.post('/api/conversation/process', json={ + resp = await client.post('/api/conversation/process', json={ 'text': 123 }) assert resp.status == 400 - resp = yield from client.post('/api/conversation/process', json={ + resp = await client.post('/api/conversation/process', json={ }) assert resp.status == 400 diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 991982af9b2..c8c7e0d809b 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -1,6 +1,5 @@ """The tests for Core components.""" # pylint: disable=protected-access -import asyncio import unittest from unittest.mock import patch, Mock @@ -75,9 +74,9 @@ class TestComponentsCore(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(calls)) - @asyncio.coroutine @patch('homeassistant.core.ServiceRegistry.call') - def test_turn_on_to_not_block_for_domains_without_service(self, mock_call): + async def test_turn_on_to_not_block_for_domains_without_service(self, + mock_call): """Test if turn_on is blocking domain with no service.""" async_mock_service(self.hass, 'light', SERVICE_TURN_ON) @@ -88,7 +87,7 @@ class TestComponentsCore(unittest.TestCase): 'entity_id': ['light.test', 'sensor.bla', 'light.bla'] }) service = self.hass.services._services['homeassistant']['turn_on'] - yield from service.func(service_call) + await service.func(service_call) self.assertEqual(2, mock_call.call_count) self.assertEqual( @@ -130,8 +129,8 @@ class TestComponentsCore(unittest.TestCase): comps.reload_core_config(self.hass) self.hass.block_till_done() - assert 10 == self.hass.config.latitude - assert 20 == self.hass.config.longitude + assert self.hass.config.latitude == 10 + assert self.hass.config.longitude == 20 ent.schedule_update_ha_state() self.hass.block_till_done() @@ -198,19 +197,18 @@ class TestComponentsCore(unittest.TestCase): assert not mock_stop.called -@asyncio.coroutine -def test_turn_on_intent(hass): +async def test_turn_on_intent(hass): """Test HassTurnOn intent.""" - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'off') calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassTurnOn', {'name': {'value': 'test light'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Turned test light on' assert len(calls) == 1 @@ -220,19 +218,18 @@ def test_turn_on_intent(hass): assert call.data == {'entity_id': ['light.test_light']} -@asyncio.coroutine -def test_turn_off_intent(hass): +async def test_turn_off_intent(hass): """Test HassTurnOff intent.""" - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'on') calls = async_mock_service(hass, 'light', SERVICE_TURN_OFF) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassTurnOff', {'name': {'value': 'test light'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Turned test light off' assert len(calls) == 1 @@ -242,19 +239,18 @@ def test_turn_off_intent(hass): assert call.data == {'entity_id': ['light.test_light']} -@asyncio.coroutine -def test_toggle_intent(hass): +async def test_toggle_intent(hass): """Test HassToggle intent.""" - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'off') calls = async_mock_service(hass, 'light', SERVICE_TOGGLE) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassToggle', {'name': {'value': 'test light'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Toggled test light' assert len(calls) == 1 @@ -264,13 +260,12 @@ def test_toggle_intent(hass): assert call.data == {'entity_id': ['light.test_light']} -@asyncio.coroutine -def test_turn_on_multiple_intent(hass): +async def test_turn_on_multiple_intent(hass): """Test HassTurnOn intent with multiple similar entities. This tests that matching finds the proper entity among similar names. """ - result = yield from comps.async_setup(hass, {}) + result = await comps.async_setup(hass, {}) assert result hass.states.async_set('light.test_light', 'off') @@ -278,10 +273,10 @@ def test_turn_on_multiple_intent(hass): hass.states.async_set('light.test_lighter', 'off') calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - response = yield from intent.async_handle( + response = await intent.async_handle( hass, 'test', 'HassTurnOn', {'name': {'value': 'test lights'}} ) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert response.speech['plain']['speech'] == 'Turned test lights 2 on' assert len(calls) == 1 From 72fb64695ed75e0eb675b15e97b34d823cc8b13b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 31 Mar 2018 15:07:29 +0200 Subject: [PATCH 239/924] Fix mysensors sensor type lookup (#13574) * Always return a safe default. --- homeassistant/components/sensor/mysensors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 3876b260dfc..66c36a8d9b1 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -81,5 +81,6 @@ class MySensorsSensor(mysensors.MySensorsEntity): TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) if isinstance(sensor_type, dict): - sensor_type = sensor_type.get(pres(self.child_type).name) + sensor_type = sensor_type.get( + pres(self.child_type).name, [None, None]) return sensor_type From 273a43be02715380cff7e73528137b0ea0ed410c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 31 Mar 2018 15:08:04 +0200 Subject: [PATCH 240/924] rfxtrx lib 0.22.0 (#13576) --- homeassistant/components/rfxtrx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index e7301836d7e..d6873a0bd91 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.21.1'] +REQUIREMENTS = ['pyRFXtrx==0.22.0'] DOMAIN = 'rfxtrx' diff --git a/requirements_all.txt b/requirements_all.txt index ccdb5fc5669..17ce0349ed2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -658,7 +658,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.21.1 +pyRFXtrx==0.22.0 # homeassistant.components.sensor.tibber pyTibber==0.4.0 From 25185875344f55226653c8ed5bbbf8c737b5a19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 31 Mar 2018 15:08:35 +0200 Subject: [PATCH 241/924] xiaomi lib upgrade (#13577) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 244605a7b97..48c54cdecff 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.8.3'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 17ce0349ed2..113f9bfbceb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.8.3 +PyXiaomiGateway==0.9.0 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 3b4faa74a02a6bdd4a9cf283722ccca0db2f6c7c Mon Sep 17 00:00:00 2001 From: Rene Nulsch <33263735+ReneNulschDE@users.noreply.github.com> Date: Sat, 31 Mar 2018 15:10:56 +0200 Subject: [PATCH 242/924] Remove MercedesME component (#13538) --- .coveragerc | 5 +- .../components/binary_sensor/mercedesme.py | 97 ----------- .../components/device_tracker/mercedesme.py | 74 --------- homeassistant/components/mercedesme.py | 156 ------------------ homeassistant/components/sensor/mercedesme.py | 87 ---------- requirements_all.txt | 3 - 6 files changed, 1 insertion(+), 421 deletions(-) delete mode 100644 homeassistant/components/binary_sensor/mercedesme.py delete mode 100644 homeassistant/components/device_tracker/mercedesme.py delete mode 100644 homeassistant/components/mercedesme.py delete mode 100644 homeassistant/components/sensor/mercedesme.py diff --git a/.coveragerc b/.coveragerc index 65f29767673..72bfb1269f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -159,10 +159,7 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py - - homeassistant/components/mercedesme.py - homeassistant/components/*/mercedesme.py - + homeassistant/components/mochad.py homeassistant/components/*/mochad.py diff --git a/homeassistant/components/binary_sensor/mercedesme.py b/homeassistant/components/binary_sensor/mercedesme.py deleted file mode 100644 index fcf2d7122e2..00000000000 --- a/homeassistant/components/binary_sensor/mercedesme.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/binary_sensor.mercedesme/ -""" -import logging -import datetime - -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.mercedesme import ( - DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, BINARY_SENSORS) - -DEPENDENCIES = ['mercedesme'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" - data = hass.data[DATA_MME].data - - if not data.cars: - _LOGGER.error("No cars found. Check component log.") - return - - devices = [] - for car in data.cars: - for key, value in sorted(BINARY_SENSORS.items()): - if car['availabilities'].get(key, 'INVALID') == 'VALID': - devices.append(MercedesMEBinarySensor( - data, key, value[0], car["vin"], None)) - else: - _LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"]) - - add_devices(devices, True) - - -class MercedesMEBinarySensor(MercedesMeEntity, BinarySensorDevice): - """Representation of a Sensor.""" - - @property - def is_on(self): - """Return the state of the binary sensor.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._internal_name == "windowsClosed": - return { - "window_front_left": self._car["windowStatusFrontLeft"], - "window_front_right": self._car["windowStatusFrontRight"], - "window_rear_left": self._car["windowStatusRearLeft"], - "window_rear_right": self._car["windowStatusRearRight"], - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - elif self._internal_name == "tireWarningLight": - return { - "front_right_tire_pressure_kpa": - self._car["frontRightTirePressureKpa"], - "front_left_tire_pressure_kpa": - self._car["frontLeftTirePressureKpa"], - "rear_right_tire_pressure_kpa": - self._car["rearRightTirePressureKpa"], - "rear_left_tire_pressure_kpa": - self._car["rearLeftTirePressureKpa"], - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"], - } - return { - "original_value": self._car[self._internal_name], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - - def update(self): - """Fetch new state data for the sensor.""" - self._car = next( - car for car in self._data.cars if car["vin"] == self._vin) - - if self._internal_name == "windowsClosed": - self._state = bool(self._car[self._internal_name] == "CLOSED") - elif self._internal_name == "tireWarningLight": - self._state = bool(self._car[self._internal_name] != "INACTIVE") - else: - self._state = self._car[self._internal_name] is True - - _LOGGER.debug("Updated %s Value: %s IsOn: %s", - self._internal_name, self._state, self.is_on) diff --git a/homeassistant/components/device_tracker/mercedesme.py b/homeassistant/components/device_tracker/mercedesme.py deleted file mode 100644 index dcc9e3ab2ec..00000000000 --- a/homeassistant/components/device_tracker/mercedesme.py +++ /dev/null @@ -1,74 +0,0 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/device_tracker.mercedesme/ -""" -import logging -from datetime import timedelta - -from homeassistant.components.mercedesme import DATA_MME -from homeassistant.helpers.event import track_time_interval -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -DEPENDENCIES = ['mercedesme'] - -MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Mercedes ME tracker.""" - if discovery_info is None: - return False - - data = hass.data[DATA_MME].data - - if not data.cars: - return False - - MercedesMEDeviceTracker(hass, config, see, data) - - return True - - -class MercedesMEDeviceTracker(object): - """A class representing a Mercedes ME device tracker.""" - - def __init__(self, hass, config, see, data): - """Initialize the Mercedes ME device tracker.""" - self.see = see - self.data = data - self.update_info() - - track_time_interval( - hass, self.update_info, MIN_TIME_BETWEEN_SCANS) - - @Throttle(MIN_TIME_BETWEEN_SCANS) - def update_info(self, now=None): - """Update the device info.""" - for device in self.data.cars: - if not device['services'].get('VEHICLE_FINDER', False): - continue - - location = self.data.get_location(device["vin"]) - if location is None: - continue - - dev_id = device["vin"] - name = device["license"] - - lat = location['positionLat']['value'] - lon = location['positionLong']['value'] - attrs = { - 'trackr_id': dev_id, - 'id': dev_id, - 'name': name - } - self.see( - dev_id=dev_id, host_name=name, - gps=(lat, lon), attributes=attrs - ) - - return True diff --git a/homeassistant/components/mercedesme.py b/homeassistant/components/mercedesme.py deleted file mode 100644 index b809e46ec64..00000000000 --- a/homeassistant/components/mercedesme.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Support for MercedesME System. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/mercedesme/ -""" -import asyncio -import logging -from datetime import timedelta - -import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, LENGTH_KILOMETERS) -from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval - -REQUIREMENTS = ['mercedesmejsonpy==0.1.2'] - -_LOGGER = logging.getLogger(__name__) - -BINARY_SENSORS = { - 'doorsClosed': ['Doors closed'], - 'windowsClosed': ['Windows closed'], - 'locked': ['Doors locked'], - 'tireWarningLight': ['Tire Warning'] -} - -SENSORS = { - 'fuelLevelPercent': ['Fuel Level', '%'], - 'fuelRangeKm': ['Fuel Range', LENGTH_KILOMETERS], - 'latestTrip': ['Latest Trip', None], - 'odometerKm': ['Odometer', LENGTH_KILOMETERS], - 'serviceIntervalDays': ['Next Service', 'days'] -} - -DATA_MME = 'mercedesme' -DOMAIN = 'mercedesme' - -FEATURE_NOT_AVAILABLE = "The feature %s is not available for your car %s" - -NOTIFICATION_ID = 'mercedesme_integration_notification' -NOTIFICATION_TITLE = 'Mercedes me integration setup' - -SIGNAL_UPDATE_MERCEDESME = "mercedesme_update" - - -CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=30): - vol.All(cv.positive_int, vol.Clamp(min=10)) - }) -}, extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up MercedesMe System.""" - from mercedesmejsonpy.controller import Controller - from mercedesmejsonpy import Exceptions - - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - scan_interval = conf.get(CONF_SCAN_INTERVAL) - - try: - mercedesme_api = Controller(username, password, scan_interval) - if not mercedesme_api.is_valid_session: - raise Exceptions.MercedesMeException(500) - hass.data[DATA_MME] = MercedesMeHub(mercedesme_api) - except Exceptions.MercedesMeException as ex: - if ex.code == 401: - hass.components.persistent_notification.create( - "Error:
Please check username and password." - "You will need to restart Home Assistant after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - else: - hass.components.persistent_notification.create( - "Error:
Can't communicate with Mercedes me API.
" - "Error code: {} Reason: {}" - "You will need to restart Home Assistant after fixing." - "".format(ex.code, ex.message), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - - _LOGGER.error("Unable to communicate with Mercedes me API: %s", - ex.message) - return False - - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'device_tracker', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - - def hub_refresh(event_time): - """Call Mercedes me API to refresh information.""" - _LOGGER.info("Updating Mercedes me component.") - hass.data[DATA_MME].data.update() - dispatcher_send(hass, SIGNAL_UPDATE_MERCEDESME) - - track_time_interval( - hass, - hub_refresh, - timedelta(seconds=scan_interval)) - - return True - - -class MercedesMeHub(object): - """Representation of a base MercedesMe device.""" - - def __init__(self, data): - """Initialize the entity.""" - self.data = data - - -class MercedesMeEntity(Entity): - """Entity class for MercedesMe devices.""" - - def __init__(self, data, internal_name, sensor_name, vin, unit): - """Initialize the MercedesMe entity.""" - self._car = None - self._data = data - self._state = False - self._name = sensor_name - self._internal_name = internal_name - self._unit = unit - self._vin = vin - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @asyncio.coroutine - def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, SIGNAL_UPDATE_MERCEDESME, self._update_callback) - - def _update_callback(self): - """Callback update method.""" - # If the method is made a callback this should be changed - # to the async version. Check core.callback - self.schedule_update_ha_state(True) - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self._unit diff --git a/homeassistant/components/sensor/mercedesme.py b/homeassistant/components/sensor/mercedesme.py deleted file mode 100644 index bb7212678a7..00000000000 --- a/homeassistant/components/sensor/mercedesme.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -Support for Mercedes cars with Mercedes ME. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/sensor.mercedesme/ -""" -import logging -import datetime - -from homeassistant.components.mercedesme import ( - DATA_MME, FEATURE_NOT_AVAILABLE, MercedesMeEntity, SENSORS) - - -DEPENDENCIES = ['mercedesme'] - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" - if discovery_info is None: - return - - data = hass.data[DATA_MME].data - - if not data.cars: - return - - devices = [] - for car in data.cars: - for key, value in sorted(SENSORS.items()): - if car['availabilities'].get(key, 'INVALID') == 'VALID': - devices.append( - MercedesMESensor( - data, key, value[0], car["vin"], value[1])) - else: - _LOGGER.warning(FEATURE_NOT_AVAILABLE, key, car["license"]) - - add_devices(devices, True) - - -class MercedesMESensor(MercedesMeEntity): - """Representation of a Sensor.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Get the latest data and updates the states.""" - _LOGGER.debug("Updating %s", self._internal_name) - - self._car = next( - car for car in self._data.cars if car["vin"] == self._vin) - - if self._internal_name == "latestTrip": - self._state = self._car["latestTrip"]["id"] - else: - self._state = self._car[self._internal_name] - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._internal_name == "latestTrip": - return { - "duration_seconds": - self._car["latestTrip"]["durationSeconds"], - "distance_traveled_km": - self._car["latestTrip"]["distanceTraveledKm"], - "started_at": datetime.datetime.fromtimestamp( - self._car["latestTrip"]["startedAt"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "average_speed_km_per_hr": - self._car["latestTrip"]["averageSpeedKmPerHr"], - "finished": self._car["latestTrip"]["finished"], - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"] - ).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } - - return { - "last_update": datetime.datetime.fromtimestamp( - self._car["lastUpdate"]).strftime('%Y-%m-%d %H:%M:%S'), - "car": self._car["license"] - } diff --git a/requirements_all.txt b/requirements_all.txt index 113f9bfbceb..761cb7d1eed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -495,9 +495,6 @@ matrix-client==0.0.6 # homeassistant.components.maxcube maxcube-api==0.1.0 -# homeassistant.components.mercedesme -mercedesmejsonpy==0.1.2 - # homeassistant.components.notify.message_bird messagebird==1.2.0 From 7bf8d4ab12fc3ec43aed18bb029225020d10ddab Mon Sep 17 00:00:00 2001 From: Myrddyn Date: Sat, 31 Mar 2018 17:01:07 -0400 Subject: [PATCH 243/924] Added Waze travel time sensor (#12387) * Added Waze travel time sensor * Update according PR comments and simplification --- .../components/sensor/waze_travel_time.py | 136 ++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 139 insertions(+) create mode 100644 homeassistant/components/sensor/waze_travel_time.py diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py new file mode 100644 index 00000000000..47589f33530 --- /dev/null +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -0,0 +1,136 @@ +""" +Support for Waze travel time sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.waze_travel_time/ +""" +from datetime import timedelta +import logging + +import requests +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_REGION +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['WazeRouteCalculator==0.5'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_DISTANCE = 'distance' +ATTR_ROUTE = 'route' + +CONF_ATTRIBUTION = "Data provided by the Waze.com" +CONF_DESTINATION = 'destination' +CONF_ORIGIN = 'origin' + +DEFAULT_NAME = 'Waze Travel Time' + +ICON = 'mdi:car' + +REGIONS = ['US', 'NA', 'EU', 'IL'] + +SCAN_INTERVAL = timedelta(minutes=5) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ORIGIN): cv.string, + vol.Required(CONF_DESTINATION): cv.string, + vol.Required(CONF_REGION): vol.In(REGIONS), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Waze travel time sensor platform.""" + destination = config.get(CONF_DESTINATION) + name = config.get(CONF_NAME) + origin = config.get(CONF_ORIGIN) + region = config.get(CONF_REGION) + + try: + waze_data = WazeRouteData(origin, destination, region) + except requests.exceptions.HTTPError as error: + _LOGGER.error("%s", error) + return + + add_devices([WazeTravelTime(waze_data, name)], True) + + +class WazeTravelTime(Entity): + """Representation of a Waze travel time sensor.""" + + def __init__(self, waze_data, name): + """Initialize the Waze travel time sensor.""" + self._name = name + self._state = None + self.waze_data = waze_data + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return round(self._state['duration']) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return 'min' + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @property + def device_state_attributes(self): + """Return the state attributes of the last update.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_DISTANCE: round(self._state['distance']), + ATTR_ROUTE: self._state['route'], + } + + def update(self): + """Fetch new state data for the sensor.""" + try: + self.waze_data.update() + self._state = self.waze_data.data + except KeyError: + _LOGGER.error("Error retrieving data from server") + + +class WazeRouteData(object): + """Get data from Waze.""" + + def __init__(self, origin, destination, region): + """Initialize the data object.""" + self._destination = destination + self._origin = origin + self._region = region + self.data = {} + + @Throttle(SCAN_INTERVAL) + def update(self): + """Fetch latest data from Waze.""" + import WazeRouteCalculator + _LOGGER.debug("Update in progress...") + try: + params = WazeRouteCalculator.WazeRouteCalculator( + self._origin, self._destination, self._region, None) + results = params.calc_all_routes_info() + best_route = next(iter(results)) + (duration, distance) = results[best_route] + best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') + self.data['duration'] = duration + self.data['distance'] = distance + self.data['route'] = best_route_str + except WazeRouteCalculator.WRCError as exp: + _LOGGER.error("Error on retrieving data: %s", exp) + return diff --git a/requirements_all.txt b/requirements_all.txt index 761cb7d1eed..7ddac50245c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,6 +54,9 @@ TravisPy==0.3.5 # homeassistant.components.notify.twitter TwitterAPI==2.5.0 +# homeassistant.components.sensor.waze_travel_time +WazeRouteCalculator==0.5 + # homeassistant.components.notify.yessssms YesssSMS==0.1.1b3 From 477f7ec01e49126c8fd85a90059940937a2507c6 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 31 Mar 2018 23:15:25 +0200 Subject: [PATCH 244/924] Added switch component to Amcrest IP Camera. (#12992) * Added switch component to Amcrest IP Camera. * Fixes to new switch component after review * Removed redundant branching, as well as requirement declaration. * Changes to requirements after rerunning generation script * Minor changes --- homeassistant/components/amcrest.py | 20 ++++- homeassistant/components/switch/amcrest.py | 92 ++++++++++++++++++++++ requirements_all.txt | 2 +- 3 files changed, 111 insertions(+), 3 deletions(-) create mode 100755 homeassistant/components/switch/amcrest.py diff --git a/homeassistant/components/amcrest.py b/homeassistant/components/amcrest.py index 90331a3014e..d0e470e3f8e 100644 --- a/homeassistant/components/amcrest.py +++ b/homeassistant/components/amcrest.py @@ -14,11 +14,11 @@ from requests.exceptions import ConnectionError as ConnectError from homeassistant.const import ( CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_SENSORS, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) + CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, HTTP_BASIC_AUTHENTICATION) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['amcrest==1.2.1'] +REQUIREMENTS = ['amcrest==1.2.2'] DEPENDENCIES = ['ffmpeg'] _LOGGER = logging.getLogger(__name__) @@ -64,6 +64,12 @@ SENSORS = { 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], } +# Switch types are defined like: Name, icon +SWITCHES = { + 'motion_detection': ['Motion Detection', 'mdi:run-fast'], + 'motion_recording': ['Motion Recording', 'mdi:record-rec'] +} + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ vol.Required(CONF_HOST): cv.string, @@ -82,6 +88,8 @@ CONFIG_SCHEMA = vol.Schema({ cv.time_period, vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), })]) }, extra=vol.ALLOW_EXTRA) @@ -116,6 +124,7 @@ def setup(hass, config): name = device.get(CONF_NAME) resolution = RESOLUTION_LIST[device.get(CONF_RESOLUTION)] sensors = device.get(CONF_SENSORS) + switches = device.get(CONF_SWITCHES) stream_source = STREAM_SOURCE_LIST[device.get(CONF_STREAM_SOURCE)] username = device.get(CONF_USERNAME) @@ -145,6 +154,13 @@ def setup(hass, config): CONF_SENSORS: sensors, }, config) + if switches: + discovery.load_platform( + hass, 'switch', DOMAIN, { + CONF_NAME: name, + CONF_SWITCHES: switches + }, config) + return True diff --git a/homeassistant/components/switch/amcrest.py b/homeassistant/components/switch/amcrest.py new file mode 100755 index 00000000000..0b93bc98b10 --- /dev/null +++ b/homeassistant/components/switch/amcrest.py @@ -0,0 +1,92 @@ +""" +Support for toggling Amcrest IP camera settings. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.amcrest/ +""" +import asyncio +import logging + +from homeassistant.components.amcrest import DATA_AMCREST, SWITCHES +from homeassistant.const import ( + CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON) +from homeassistant.helpers.entity import ToggleEntity + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['amcrest'] + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the IP Amcrest camera switch platform.""" + if discovery_info is None: + return + + name = discovery_info[CONF_NAME] + switches = discovery_info[CONF_SWITCHES] + camera = hass.data[DATA_AMCREST][name].device + + all_switches = [] + + for setting in switches: + all_switches.append(AmcrestSwitch(setting, camera)) + + async_add_devices(all_switches, True) + + +class AmcrestSwitch(ToggleEntity): + """Representation of an Amcrest IP camera switch.""" + + def __init__(self, setting, camera): + """Initialize the Amcrest switch.""" + self._setting = setting + self._camera = camera + self._name = SWITCHES[setting][0] + self._icon = SWITCHES[setting][1] + self._state = None + + @property + def name(self): + """Return the name of the switch if any.""" + return self._name + + @property + def state(self): + """Return the state of the switch.""" + return self._state + + @property + def is_on(self): + """Return true if switch is on.""" + return self._state == STATE_ON + + def turn_on(self, **kwargs): + """Turn setting on.""" + if self._setting == 'motion_detection': + self._camera.motion_detection = 'true' + elif self._setting == 'motion_recording': + self._camera.motion_recording = 'true' + + def turn_off(self, **kwargs): + """Turn setting off.""" + if self._setting == 'motion_detection': + self._camera.motion_detection = 'false' + elif self._setting == 'motion_recording': + self._camera.motion_recording = 'false' + + def update(self): + """Update setting state.""" + _LOGGER.debug("Polling state for setting: %s ", self._name) + + if self._setting == 'motion_detection': + detection = self._camera.is_motion_detector_on() + elif self._setting == 'motion_recording': + detection = self._camera.is_record_on_motion_detection() + + self._state = STATE_ON if detection else STATE_OFF + + @property + def icon(self): + """Return the icon for the switch.""" + return self._icon diff --git a/requirements_all.txt b/requirements_all.txt index 7ddac50245c..77b57e23769 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -98,7 +98,7 @@ alarmdecoder==1.13.2 alpha_vantage==1.9.0 # homeassistant.components.amcrest -amcrest==1.2.1 +amcrest==1.2.2 # homeassistant.components.media_player.anthemav anthemav==1.1.8 From 12affa1469df6b404e3b4c64e425ae720fcd782a Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Sat, 31 Mar 2018 17:16:47 -0400 Subject: [PATCH 245/924] Upgrade pyhydroquebec 2.2.1 (#13586) --- homeassistant/components/sensor/hydroquebec.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 3678ac9268f..9129ee17d80 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyhydroquebec==2.1.0'] +REQUIREMENTS = ['pyhydroquebec==2.2.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 77b57e23769..94271e5e23e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ pyhiveapi==0.2.11 pyhomematic==0.1.40 # homeassistant.components.sensor.hydroquebec -pyhydroquebec==2.1.0 +pyhydroquebec==2.2.1 # homeassistant.components.alarm_control_panel.ialarm pyialarm==0.2 From 7b3d17bae41b224101cb0a5e61e0827957ee1234 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 31 Mar 2018 23:20:58 +0200 Subject: [PATCH 246/924] Add mastodon (#13441) * Add mastodon * Move login * Revert "Move login" This reverts commit 2c8446f62950f91c0ebfc0b4825e87421ac653fc. --- .coveragerc | 3 +- homeassistant/components/notify/mastodon.py | 70 +++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/notify/mastodon.py diff --git a/.coveragerc b/.coveragerc index 72bfb1269f5..828da909a06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -507,6 +507,7 @@ omit = homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py + homeassistant/components/notify/mastodon.py homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py @@ -522,8 +523,8 @@ omit = homeassistant/components/notify/sendgrid.py homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py - homeassistant/components/notify/stride.py homeassistant/components/notify/smtp.py + homeassistant/components/notify/stride.py homeassistant/components/notify/synology_chat.py homeassistant/components/notify/syslog.py homeassistant/components/notify/telegram.py diff --git a/homeassistant/components/notify/mastodon.py b/homeassistant/components/notify/mastodon.py new file mode 100644 index 00000000000..3ba95407fec --- /dev/null +++ b/homeassistant/components/notify/mastodon.py @@ -0,0 +1,70 @@ +""" +Mastodon platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.mastodon/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ACCESS_TOKEN +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['Mastodon.py==1.2.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_BASE_URL = 'base_url' +CONF_CLIENT_ID = 'client_id' +CONF_CLIENT_SECRET = 'client_secret' + +DEFAULT_URL = 'https://mastodon.social' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_BASE_URL, default=DEFAULT_URL): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Mastodon notification service.""" + from mastodon import Mastodon + from mastodon.Mastodon import MastodonUnauthorizedError + + client_id = config.get(CONF_CLIENT_ID) + client_secret = config.get(CONF_CLIENT_SECRET) + access_token = config.get(CONF_ACCESS_TOKEN) + base_url = config.get(CONF_BASE_URL) + + try: + mastodon = Mastodon( + client_id=client_id, client_secret=client_secret, + access_token=access_token, api_base_url=base_url) + mastodon.account_verify_credentials() + except MastodonUnauthorizedError: + _LOGGER.warning("Authentication failed") + return None + + return MastodonNotificationService(mastodon) + + +class MastodonNotificationService(BaseNotificationService): + """Implement the notification service for Mastodon.""" + + def __init__(self, api): + """Initialize the service.""" + self._api = api + + def send_message(self, message="", **kwargs): + """Send a message to a user.""" + from mastodon.Mastodon import MastodonAPIError + + try: + self._api.toot(message) + except MastodonAPIError: + _LOGGER.error("Unable to send message") diff --git a/requirements_all.txt b/requirements_all.txt index 94271e5e23e..7eeed2d1ef2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,6 +24,9 @@ DoorBirdPy==0.1.3 # homeassistant.components.homekit HAP-python==1.1.7 +# homeassistant.components.notify.mastodon +Mastodon.py==1.2.2 + # homeassistant.components.isy994 PyISY==1.1.0 From 7c99567b65a5124c202d37f85a71b475282a303b Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Sat, 31 Mar 2018 23:22:54 +0200 Subject: [PATCH 247/924] Added support for requesting RSSI values from Bluetooth devices (#12458) * Added support for requesting RSSI values from Bluetooth devices * Moved Bluetooth RSSI code to separate library and imported it * Cleaned up tuple issues * Changed concatination of mac addresses * Changed string formatting to use new style * Ran gen_requirements_all.py --- .../device_tracker/bluetooth_tracker.py | 32 +++++++++++++------ requirements_all.txt | 3 ++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 9d41611d9a2..807f6c0d0a4 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -17,12 +17,15 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['pybluez==0.22'] +REQUIREMENTS = ['pybluez==0.22', 'bt_proximity==0.1.2'] BT_PREFIX = 'BT_' +CONF_REQUEST_RSSI = 'request_rssi' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_TRACK_NEW): cv.boolean + vol.Optional(CONF_TRACK_NEW): cv.boolean, + vol.Optional(CONF_REQUEST_RSSI): cv.boolean }) @@ -30,11 +33,15 @@ def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth Scanner.""" # pylint: disable=import-error import bluetooth + from bt_proximity import BluetoothRSSI - def see_device(device): + def see_device(mac, name, rssi=None): """Mark a device as seen.""" - see(mac=BT_PREFIX + device[0], host_name=device[1], - source_type=SOURCE_TYPE_BLUETOOTH) + attributes = {} + if rssi is not None: + attributes['rssi'] = rssi + see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name, + attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH) def discover_devices(): """Discover Bluetooth devices.""" @@ -64,27 +71,32 @@ def setup_scanner(hass, config, see, discovery_info=None): if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and \ - dev[0] not in devs_donot_track: + dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) - see_device(dev) + see_device(dev[0], dev[1]) interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + request_rssi = config.get(CONF_REQUEST_RSSI, False) + def update_bluetooth(now): """Lookup Bluetooth device and update status.""" try: if track_new: for dev in discover_devices(): if dev[0] not in devs_to_track and \ - dev[0] not in devs_donot_track: + dev[0] not in devs_donot_track: devs_to_track.append(dev[0]) for mac in devs_to_track: _LOGGER.debug("Scanning %s", mac) result = bluetooth.lookup_name(mac, timeout=5) - if not result: + rssi = None + if request_rssi: + rssi = BluetoothRSSI(mac).request_rssi() + if result is None: # Could not lookup device name continue - see_device((mac, result)) + see_device(mac, result, rssi) except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") track_point_in_utc_time( diff --git a/requirements_all.txt b/requirements_all.txt index 7eeed2d1ef2..b2098b37f63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,6 +172,9 @@ boto3==1.4.7 # homeassistant.scripts.credstash botocore==1.7.34 +# homeassistant.components.device_tracker.bluetooth_tracker +bt_proximity==0.1.2 + # homeassistant.components.sensor.buienradar # homeassistant.components.weather.buienradar buienradar==0.91 From 3e082b5ce65a513874a29193e7a7b9232276ae79 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Mar 2018 17:43:41 -0700 Subject: [PATCH 248/924] Bump frontend to 20180401.0 --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b2f50148bd3..1fbfe94bb0d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180330.0'] +REQUIREMENTS = ['home-assistant-frontend==20180401.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] From 8fbef5b0029c33cf3f6e5a1694676f852a297dc0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Mar 2018 17:44:01 -0700 Subject: [PATCH 249/924] Version bump to 0.66.1b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3dce8882015..ea3dc327c46 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '0' +PATCH_VERSION = '1b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5fce2e2b47da639f28aa61db0ddfa7521d1bd742 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 1 Apr 2018 02:45:50 +0200 Subject: [PATCH 250/924] Fix mysensors update callback (#13602) * Add callback annotation to mysensors dispatch callback. --- homeassistant/components/mysensors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index a560b49648f..74df860a0fc 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -19,6 +19,7 @@ from homeassistant.components.mqtt import ( from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) +from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -301,9 +302,9 @@ def setup(hass, config): """Call MQTT publish function.""" mqtt.publish(hass, topic, payload, qos, retain) - def sub_callback(topic, callback, qos): + def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(hass, topic, callback, qos) + mqtt.subscribe(hass, topic, sub_cb, qos) gateway = mysensors.MQTTGateway( pub_callback, sub_callback, event_callback=None, persistence=persistence, @@ -627,6 +628,7 @@ class MySensorsEntity(MySensorsDevice, Entity): """Return true if entity is available.""" return self.value_type in self._values + @callback def _async_update_callback(self): """Update the entity.""" self.async_schedule_update_ha_state(True) From 9f0f7394fbb6bc5fc1cb93a555283cc9de39888c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 31 Mar 2018 18:02:43 -0700 Subject: [PATCH 251/924] Version bump frontend done right --- 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 7ac9bd5fd7e..b2232eeb9e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -366,7 +366,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180330.0 +home-assistant-frontend==20180401.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33527a913a5..8c01400f79e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180330.0 +home-assistant-frontend==20180401.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 45ef34ff81ed5f63a56253a0ddced8db4849b028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 1 Apr 2018 10:09:16 +0200 Subject: [PATCH 252/924] Broadlink (#13585) * Update broadlink lib * Update broadlink lib * requirements --- homeassistant/components/sensor/broadlink.py | 6 ++---- homeassistant/components/switch/broadlink.py | 12 +++++------- requirements_all.txt | 8 ++++---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 47cefe50aec..044b77ebfe8 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,9 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = [ - 'https://github.com/balloob/python-broadlink/archive/' - '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1'] +REQUIREMENTS = ['broadlink==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -108,7 +106,7 @@ class BroadlinkData(object): """Initialize the data object.""" import broadlink self.data = None - self._device = broadlink.a1((ip_addr, 80), mac_addr) + self._device = broadlink.a1((ip_addr, 80), mac_addr, None) self._device.timeout = timeout self._schema = vol.Schema({ vol.Optional('temperature'): vol.Range(min=-50, max=150), diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 38888733ba6..3828758fe6e 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -22,9 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from homeassistant.util.dt import utcnow -REQUIREMENTS = [ - 'https://github.com/balloob/python-broadlink/archive/' - '3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1'] +REQUIREMENTS = ['broadlink==0.8.0'] _LOGGER = logging.getLogger(__name__) @@ -142,7 +140,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return slots['slot_{}'.format(slot)] if switch_type in RM_TYPES: - broadlink_device = broadlink.rm((ip_addr, 80), mac_addr) + broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) hass.services.register(DOMAIN, SERVICE_LEARN + '_' + ip_addr.replace('.', '_'), _learn_command) hass.services.register(DOMAIN, SERVICE_SEND + '_' + @@ -159,14 +157,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ) ) elif switch_type in SP1_TYPES: - broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr) + broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr, None) switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] elif switch_type in SP2_TYPES: - broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr) + broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None) switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] elif switch_type in MP1_TYPES: switches = [] - broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr) + broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr, None) parent_device = BroadlinkMP1Switch(broadlink_device) for i in range(1, 5): slot = BroadlinkMP1Slot( diff --git a/requirements_all.txt b/requirements_all.txt index 4a21044e6ab..2b7967f4981 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -172,6 +172,10 @@ boto3==1.4.7 # homeassistant.scripts.credstash botocore==1.7.34 +# homeassistant.components.sensor.broadlink +# homeassistant.components.switch.broadlink +broadlink==0.8.0 + # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 @@ -392,10 +396,6 @@ httplib2==0.10.3 # homeassistant.components.media_player.braviatv https://github.com/aparraga/braviarc/archive/0.3.7.zip#braviarc==0.3.7 -# homeassistant.components.sensor.broadlink -# homeassistant.components.switch.broadlink -https://github.com/balloob/python-broadlink/archive/3580ff2eaccd267846f14246d6ede6e30671f7c6.zip#broadlink==0.5.1 - # homeassistant.components.media_player.spotify https://github.com/happyleavesaoc/spotipy/archive/544614f4b1d508201d363e84e871f86c90aa26b2.zip#spotipy==2.4.4 From a8fdd76f44aa29482ab522e4d5f02c474a0f69a8 Mon Sep 17 00:00:00 2001 From: Zhao Date: Sun, 1 Apr 2018 20:17:26 +1000 Subject: [PATCH 253/924] Fix IMAP email message_data (#13606) --- homeassistant/components/sensor/imap_email_content.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index 1f04cd606d6..c0c9bf62efd 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -87,6 +87,8 @@ class EmailReader(object): _, message_data = self.connection.uid( 'fetch', message_uid, '(RFC822)') + if message_data is None: + return None raw_email = message_data[0][1] email_message = email.message_from_bytes(raw_email) return email_message From 0c0e0c36af141cd2ed4a720783953a49b66e4657 Mon Sep 17 00:00:00 2001 From: Lewis Juggins <873275+lwis@users.noreply.github.com> Date: Sun, 1 Apr 2018 15:50:48 +0100 Subject: [PATCH 254/924] Re-add group polling as a fallback for observation (#13613) --- homeassistant/components/light/tradfri.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 227ed419aec..63468225c9d 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -76,11 +76,6 @@ class TradfriGroup(Light): """Return unique ID for this group.""" return self._unique_id - @property - def should_poll(self): - """No polling needed for tradfri group.""" - return False - @property def supported_features(self): """Flag supported features.""" @@ -149,6 +144,10 @@ class TradfriGroup(Light): self._refresh(tradfri_device) self.async_schedule_update_ha_state() + async def async_update(self): + """Fetch new state data for the group.""" + await self._group.update() + class TradfriLight(Light): """The platform class required by Home Assistant.""" From ff9f500c515163e5f775b5fba02847081e397024 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Apr 2018 08:30:14 -0700 Subject: [PATCH 255/924] Unflake folder watcher test (#13569) * Unflake folder watcher test * Fix tests * Lint --- requirements_test_all.txt | 3 - script/gen_requirements_all.py | 1 - tests/components/test_folder_watcher.py | 96 ++++++++++++------------- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 990848d5c56..d3e9ae51a60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -202,8 +202,5 @@ wakeonlan==1.0.0 # homeassistant.components.cloud warrant==0.6.1 -# homeassistant.components.folder_watcher -watchdog==0.8.3 - # homeassistant.components.sensor.yahoo_finance yahoo-finance==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index fa39c307f18..d5bb2701e9b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -91,7 +91,6 @@ TEST_REQUIREMENTS = ( 'yahoo-finance', 'pythonwhois', 'wakeonlan', - 'watchdog', 'vultr' ) diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py index 587d8b7ad6d..16ec7a58a02 100644 --- a/tests/components/test_folder_watcher.py +++ b/tests/components/test_folder_watcher.py @@ -1,64 +1,56 @@ """The tests for the folder_watcher component.""" -import unittest -from unittest.mock import MagicMock +from unittest.mock import Mock, patch import os from homeassistant.components import folder_watcher -from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant - -CWD = os.path.join(os.path.dirname(__file__)) -FILE = 'file.txt' +from homeassistant.setup import async_setup_component +from tests.common import MockDependency -class TestFolderWatcher(unittest.TestCase): - """Test the file_watcher component.""" +async def test_invalid_path_setup(hass): + """Test that a invalid path is not setup.""" + assert not await async_setup_component( + hass, folder_watcher.DOMAIN, { + folder_watcher.DOMAIN: { + folder_watcher.CONF_FOLDER: 'invalid_path' + } + }) - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.whitelist_external_dirs = set((CWD)) - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() +async def test_valid_path_setup(hass): + """Test that a valid path is setup.""" + cwd = os.path.join(os.path.dirname(__file__)) + hass.config.whitelist_external_dirs = set((cwd)) + with patch.object(folder_watcher, 'Watcher'): + assert await async_setup_component( + hass, folder_watcher.DOMAIN, { + folder_watcher.DOMAIN: {folder_watcher.CONF_FOLDER: cwd} + }) - def test_invalid_path_setup(self): - """Test that a invalid path is not setup.""" - config = { - folder_watcher.DOMAIN: [{ - folder_watcher.CONF_FOLDER: 'invalid_path' - }] - } - self.assertFalse( - setup_component(self.hass, folder_watcher.DOMAIN, config)) - def test_valid_path_setup(self): - """Test that a valid path is setup.""" - config = { - folder_watcher.DOMAIN: [{folder_watcher.CONF_FOLDER: CWD}] - } +@MockDependency('watchdog', 'events') +def test_event(mock_watchdog): + """Check that HASS events are fired correctly on watchdog event.""" + class MockPatternMatchingEventHandler: + """Mock base class for the pattern matcher event handler.""" - self.assertTrue(setup_component( - self.hass, folder_watcher.DOMAIN, config)) + def __init__(self, patterns): + pass - def test_event(self): - """Check that HASS events are fired correctly on watchdog event.""" - from watchdog.events import FileModifiedEvent - - # Cant use setup_component as need to retrieve Watcher object. - w = folder_watcher.Watcher(CWD, - folder_watcher.DEFAULT_PATTERN, - self.hass) - w.startup(None) - - self.hass.bus.fire = MagicMock() - - # Trigger a fake filesystem event through the Watcher Observer emitter. - (emitter,) = w._observer.emitters - emitter.queue_event(FileModifiedEvent(FILE)) - - # Wait for the event to propagate. - self.hass.block_till_done() - - assert self.hass.bus.fire.called + mock_watchdog.events.PatternMatchingEventHandler = \ + MockPatternMatchingEventHandler + hass = Mock() + handler = folder_watcher.create_event_handler(['*'], hass) + handler.on_created(Mock( + is_directory=False, + src_path='/hello/world.txt', + event_type='created' + )) + assert hass.bus.fire.called + assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN + assert hass.bus.fire.mock_calls[0][1][1] == { + 'event_type': 'created', + 'path': '/hello/world.txt', + 'file': 'world.txt', + 'folder': '/hello', + } From c8f2810fac989d2a8275bfb92603a5877113beed Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 1 Apr 2018 17:36:26 +0200 Subject: [PATCH 256/924] Make mysensors updates and platform setup async (#13603) * Use async updates but keep methods that interact with mysensors gateway thread, eg turn_on and turn_off, non async. * Use Python 3.5 async syntax. --- .../components/binary_sensor/mysensors.py | 7 +++-- homeassistant/components/climate/mysensors.py | 12 ++++--- homeassistant/components/cover/mysensors.py | 8 +++-- .../components/device_tracker/mysensors.py | 20 ++++++------ homeassistant/components/light/mysensors.py | 31 ++++++++++--------- homeassistant/components/mysensors.py | 19 ++++++------ homeassistant/components/notify/mysensors.py | 4 +-- homeassistant/components/sensor/mysensors.py | 6 ++-- homeassistant/components/switch/mysensors.py | 15 ++++----- 9 files changed, 65 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/binary_sensor/mysensors.py b/homeassistant/components/binary_sensor/mysensors.py index 1e9359b6902..21443021193 100644 --- a/homeassistant/components/binary_sensor/mysensors.py +++ b/homeassistant/components/binary_sensor/mysensors.py @@ -21,11 +21,12 @@ SENSORS = { } -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for binary sensors.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors platform for binary sensors.""" mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsBinarySensor, - add_devices=add_devices) + async_add_devices=async_add_devices) class MySensorsBinarySensor(mysensors.MySensorsEntity, BinarySensorDevice): diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index b526d8b066c..2545094ceec 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -31,10 +31,12 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_OPERATION_MODE) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors climate.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors climate.""" mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsHVAC, add_devices=add_devices) + hass, DOMAIN, discovery_info, MySensorsHVAC, + async_add_devices=async_add_devices) class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): @@ -163,8 +165,8 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): self._values[self.value_type] = operation_mode self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() + await super().async_update() self._values[self.value_type] = DICT_MYS_TO_HA[ self._values[self.value_type]] diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 391d2a22bda..669a7ce6723 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -9,10 +9,12 @@ from homeassistant.components.cover import ATTR_POSITION, DOMAIN, CoverDevice from homeassistant.const import STATE_OFF, STATE_ON -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for covers.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors platform for covers.""" mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsCover, add_devices=add_devices) + hass, DOMAIN, discovery_info, MySensorsCover, + async_add_devices=async_add_devices) class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): diff --git a/homeassistant/components/device_tracker/mysensors.py b/homeassistant/components/device_tracker/mysensors.py index f68eb361ca0..b0d29bf0566 100644 --- a/homeassistant/components/device_tracker/mysensors.py +++ b/homeassistant/components/device_tracker/mysensors.py @@ -6,15 +6,15 @@ https://home-assistant.io/components/device_tracker.mysensors/ """ from homeassistant.components import mysensors from homeassistant.components.device_tracker import DOMAIN -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import slugify -def setup_scanner(hass, config, see, discovery_info=None): +async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MySensors device scanner.""" new_devices = mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsDeviceScanner, - device_args=(see, )) + device_args=(async_see, )) if not new_devices: return False @@ -22,9 +22,9 @@ def setup_scanner(hass, config, see, discovery_info=None): dev_id = ( id(device.gateway), device.node_id, device.child_id, device.value_type) - dispatcher_connect( + async_dispatcher_connect( hass, mysensors.SIGNAL_CALLBACK.format(*dev_id), - device.update_callback) + device.async_update_callback) return True @@ -32,20 +32,20 @@ def setup_scanner(hass, config, see, discovery_info=None): class MySensorsDeviceScanner(mysensors.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, see, *args): + def __init__(self, async_see, *args): """Set up instance.""" super().__init__(*args) - self.see = see + self.async_see = async_see - def update_callback(self): + async def async_update_callback(self): """Update the device.""" - self.update() + await self.async_update() node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] position = child.values[self.value_type] latitude, longitude, _ = position.split(',') - self.see( + await self.async_see( dev_id=slugify(self.name), host_name=self.name, gps=(latitude, longitude), diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 7aa1e754c43..6e41e0f5693 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -15,8 +15,9 @@ import homeassistant.util.color as color_util SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the MySensors platform for lights.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the mysensors platform for lights.""" device_class_map = { 'S_DIMMER': MySensorsLightDimmer, 'S_RGB_LIGHT': MySensorsLightRGB, @@ -24,7 +25,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, device_class_map, - add_devices=add_devices) + async_add_devices=async_add_devices) class MySensorsLight(mysensors.MySensorsEntity, Light): @@ -140,12 +141,12 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): self._values[value_type] = STATE_OFF self.schedule_update_ha_state() - def _update_light(self): + def _async_update_light(self): """Update the controller with values from light child.""" value_type = self.gateway.const.SetReq.V_LIGHT self._state = self._values[value_type] == STATE_ON - def _update_dimmer(self): + def _async_update_dimmer(self): """Update the controller with values from dimmer child.""" value_type = self.gateway.const.SetReq.V_DIMMER if value_type in self._values: @@ -153,7 +154,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): if self._brightness == 0: self._state = False - def _update_rgb_or_w(self): + def _async_update_rgb_or_w(self): """Update the controller with values from RGB or RGBW child.""" value = self._values[self.value_type] color_list = rgb_hex_to_rgb_list(value) @@ -177,11 +178,11 @@ class MySensorsLightDimmer(MySensorsLight): if self.gateway.optimistic: self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() - self._update_light() - self._update_dimmer() + await super().async_update() + self._async_update_light() + self._async_update_dimmer() class MySensorsLightRGB(MySensorsLight): @@ -203,12 +204,12 @@ class MySensorsLightRGB(MySensorsLight): if self.gateway.optimistic: self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() - self._update_light() - self._update_dimmer() - self._update_rgb_or_w() + await super().async_update() + self._async_update_light() + self._async_update_dimmer() + self._async_update_rgb_or_w() class MySensorsLightRGBW(MySensorsLightRGB): diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 74df860a0fc..17c9129a31d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,7 +4,6 @@ Connect to a MySensors gateway via pymysensors API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mysensors/ """ -import asyncio from collections import defaultdict import logging import os @@ -519,11 +518,12 @@ def get_mysensors_gateway(hass, gateway_id): return gateways.get(gateway_id) +@callback def setup_mysensors_platform( hass, domain, discovery_info, device_class, device_args=None, - add_devices=None): + async_add_devices=None): """Set up a MySensors platform.""" - # Only act if called via MySensors by discovery event. + # Only act if called via mysensors by discovery event. # Otherwise gateway is not setup. if not discovery_info: return @@ -552,8 +552,8 @@ def setup_mysensors_platform( new_devices.append(devices[dev_id]) if new_devices: _LOGGER.info("Adding new devices: %s", new_devices) - if add_devices is not None: - add_devices(new_devices, True) + if async_add_devices is not None: + async_add_devices(new_devices, True) return new_devices @@ -596,7 +596,7 @@ class MySensorsDevice(object): return attr - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] @@ -629,14 +629,13 @@ class MySensorsEntity(MySensorsDevice, Entity): return self.value_type in self._values @callback - def _async_update_callback(self): + def async_update_callback(self): """Update the entity.""" self.async_schedule_update_ha_state(True) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Register update callback.""" dev_id = id(self.gateway), self.node_id, self.child_id, self.value_type async_dispatcher_connect( self.hass, SIGNAL_CALLBACK.format(*dev_id), - self._async_update_callback) + self.async_update_callback) diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 8ae697048f5..257b5995446 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -9,12 +9,12 @@ from homeassistant.components.notify import ( ATTR_TARGET, DOMAIN, BaseNotificationService) -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Get the MySensors notification service.""" new_devices = mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, MySensorsNotificationDevice) if not new_devices: - return + return None return MySensorsNotificationService(hass) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 66c36a8d9b1..669ef3998de 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -34,10 +34,12 @@ SENSORS = { } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the MySensors platform for sensors.""" mysensors.setup_mysensors_platform( - hass, DOMAIN, discovery_info, MySensorsSensor, add_devices=add_devices) + hass, DOMAIN, discovery_info, MySensorsSensor, + async_add_devices=async_add_devices) class MySensorsSensor(mysensors.MySensorsEntity): diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index b4a1dcde3e6..c0f45cad861 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -20,7 +20,8 @@ SEND_IR_CODE_SERVICE_SCHEMA = vol.Schema({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the mysensors platform for switches.""" device_class_map = { 'S_DOOR': MySensorsSwitch, @@ -39,7 +40,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): } mysensors.setup_mysensors_platform( hass, DOMAIN, discovery_info, device_class_map, - add_devices=add_devices) + async_add_devices=async_add_devices) def send_ir_code_service(service): """Set IR code as device state attribute.""" @@ -59,9 +60,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in _devices: device.turn_on(**kwargs) - hass.services.register(DOMAIN, SERVICE_SEND_IR_CODE, - send_ir_code_service, - schema=SEND_IR_CODE_SERVICE_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service, + schema=SEND_IR_CODE_SERVICE_SCHEMA) class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): @@ -143,7 +144,7 @@ class MySensorsIRSwitch(MySensorsSwitch): self._values[set_req.V_LIGHT] = STATE_OFF self.schedule_update_ha_state() - def update(self): + async def async_update(self): """Update the controller with the latest value from a sensor.""" - super().update() + await super().async_update() self._ir_code = self._values.get(self.value_type) From dee47d50ecf4f0e80a127d624027c1688d34b504 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Mon, 2 Apr 2018 01:37:03 +1000 Subject: [PATCH 257/924] Use 0/1 for raspberry pi cover GPIO writes rather than true/false (#13610) * Use 0/1 for GPIO writes rather than true/false GPIO pins don't appear to respond to true/false writes, and this is reflected in code elsewhere. For example, in `\components\switch\rpio_gpio.py` the following code is used: ``` def turn_on(self, **kwargs): """Turn the device on.""" rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) self._state = True self.schedule_update_ha_state() ``` This code works. Hence this PR uses 0/1 in the raspberry pi GPIO cover, instead of true/false. * Update rpi_gpio.py --- homeassistant/components/cover/rpi_gpio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 77cd0b0f7e2..49666139330 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -87,7 +87,7 @@ class RPiGPIOCover(CoverDevice): self._invert_relay = invert_relay rpi_gpio.setup_output(self._relay_pin) rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) - rpi_gpio.write_output(self._relay_pin, not self._invert_relay) + rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) @property def name(self): @@ -105,9 +105,9 @@ class RPiGPIOCover(CoverDevice): def _trigger(self): """Trigger the cover.""" - rpi_gpio.write_output(self._relay_pin, self._invert_relay) + rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0) sleep(self._relay_time) - rpi_gpio.write_output(self._relay_pin, not self._invert_relay) + rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) def close_cover(self, **kwargs): """Close the cover.""" From cd96d7b2ecf36beb065e19211578dee95265e5d1 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Apr 2018 17:38:29 +0200 Subject: [PATCH 258/924] Add pincode fallback (#13587) * Add pincode log statement * Moved msg to show_setup_msg --- homeassistant/components/homekit/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 2fa2ebd396a..af2c74d9c3c 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -36,6 +36,7 @@ def validate_entity_config(values): def show_setup_message(bridge, hass): """Display persistent notification with setup information.""" pin = bridge.pincode.decode() + _LOGGER.info('Pincode: %s', pin) message = 'To setup Home Assistant in the Home App, enter the ' \ 'following code:\n### {}'.format(pin) hass.components.persistent_notification.create( From eb763764b39c8913c913c85ef81b783cef73577f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Apr 2018 09:03:01 -0700 Subject: [PATCH 259/924] Fix Hue error logging (#13616) --- homeassistant/components/hue/bridge.py | 7 ++++++- tests/components/light/test_hue.py | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 790831a4d6c..8093c84971e 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -31,9 +31,14 @@ class HueBridge(object): self.available = True self.api = None + @property + def host(self): + """Return the host of this bridge.""" + return self.config_entry.data['host'] + async def async_setup(self, tries=0): """Set up a phue bridge based on host parameter.""" - host = self.config_entry.data['host'] + host = self.host try: self.api = await get_bridge( diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index d73531b1b9a..7b6c3a21a79 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -160,7 +160,13 @@ LIGHT_RESPONSE = { @pytest.fixture def mock_bridge(hass): """Mock a Hue bridge.""" - bridge = Mock(available=True, allow_groups=False, host='1.1.1.1') + bridge = Mock( + available=True, + allow_unreachable=False, + allow_groups=False, + api=Mock(), + spec=hue.HueBridge + ) bridge.mock_requests = [] # We're using a deque so we can schedule multiple responses # and also means that `popleft()` will blow up if we get more updates From 4ad0152a44b0cdb8f121b9f6a9cf04dcbe8ac9a1 Mon Sep 17 00:00:00 2001 From: Lewis Juggins <873275+lwis@users.noreply.github.com> Date: Sun, 1 Apr 2018 18:42:47 +0100 Subject: [PATCH 260/924] Bugfix for tradfri to correctly execute Command. (#13618) --- homeassistant/components/light/tradfri.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 63468225c9d..95082bb4d19 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -146,7 +146,7 @@ class TradfriGroup(Light): async def async_update(self): """Fetch new state data for the group.""" - await self._group.update() + await self._api(self._group.update()) class TradfriLight(Light): From be43c3bcfeb771369c230006f638b2db5aa07b26 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 1 Apr 2018 14:12:55 -0400 Subject: [PATCH 261/924] Fix mqtt_json color commands (#13617) --- homeassistant/components/light/mqtt_json.py | 30 ++++++++++++--------- tests/components/light/test_mqtt_json.py | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 25212e45c60..20e49e40bae 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -129,6 +129,8 @@ class MqttJson(MqttAvailability, Light): self._retain = retain self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None self._state = False + self._rgb = rgb + self._xy = xy if brightness: self._brightness = 255 else: @@ -307,20 +309,22 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_HS_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs and (self._rgb or self._xy): hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], brightness / 255 * 100) - xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - message['color'] = { - 'r': rgb[0], - 'g': rgb[1], - 'b': rgb[2], - 'x': xy_color[0], - 'y': xy_color[1], - } + message['color'] = {} + if self._rgb: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + message['color']['r'] = rgb[0] + message['color']['g'] = rgb[1] + message['color']['b'] = rgb[2] + if self._xy: + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + message['color']['x'] = xy_color[0] + message['color']['y'] = xy_color[1] if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index cfeffc93108..a183355fbb3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -334,6 +334,33 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual('colorloop', state.attributes['effect']) self.assertEqual(170, state.attributes['white_value']) + # Test a color command + light.turn_on(self.hass, 'light.test', + brightness=50, hs_color=(125, 100)) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.async_publish.mock_calls[0][1][0]) + self.assertEqual(2, + self.mock_publish.async_publish.mock_calls[0][1][2]) + self.assertEqual(False, + self.mock_publish.async_publish.mock_calls[0][1][3]) + # Get the sent message + message_json = json.loads( + self.mock_publish.async_publish.mock_calls[1][1][1]) + self.assertEqual(50, message_json["brightness"]) + self.assertEqual({ + 'r': 0, + 'g': 50, + 'b': 4, + }, message_json["color"]) + self.assertEqual("ON", message_json["state"]) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(50, state.attributes['brightness']) + self.assertEqual((125, 100), state.attributes['hs_color']) + def test_flash_short_and_long(self): \ # pylint: disable=invalid-name """Test for flash length being sent when included.""" From ff72c5e4568bbb5ac8f4b12a1a2102617fbb53fc Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Sun, 1 Apr 2018 14:12:55 -0400 Subject: [PATCH 262/924] Fix mqtt_json color commands (#13617) --- homeassistant/components/light/mqtt_json.py | 30 ++++++++++++--------- tests/components/light/test_mqtt_json.py | 27 +++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 25212e45c60..20e49e40bae 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -129,6 +129,8 @@ class MqttJson(MqttAvailability, Light): self._retain = retain self._optimistic = optimistic or topic[CONF_STATE_TOPIC] is None self._state = False + self._rgb = rgb + self._xy = xy if brightness: self._brightness = 255 else: @@ -307,20 +309,22 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_HS_COLOR in kwargs: + if ATTR_HS_COLOR in kwargs and (self._rgb or self._xy): hs_color = kwargs[ATTR_HS_COLOR] - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255) - rgb = color_util.color_hsv_to_RGB( - hs_color[0], hs_color[1], brightness / 255 * 100) - xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - message['color'] = { - 'r': rgb[0], - 'g': rgb[1], - 'b': rgb[2], - 'x': xy_color[0], - 'y': xy_color[1], - } + message['color'] = {} + if self._rgb: + brightness = kwargs.get( + ATTR_BRIGHTNESS, + self._brightness if self._brightness else 255) + rgb = color_util.color_hsv_to_RGB( + hs_color[0], hs_color[1], brightness / 255 * 100) + message['color']['r'] = rgb[0] + message['color']['g'] = rgb[1] + message['color']['b'] = rgb[2] + if self._xy: + xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) + message['color']['x'] = xy_color[0] + message['color']['y'] = xy_color[1] if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index cfeffc93108..a183355fbb3 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -334,6 +334,33 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual('colorloop', state.attributes['effect']) self.assertEqual(170, state.attributes['white_value']) + # Test a color command + light.turn_on(self.hass, 'light.test', + brightness=50, hs_color=(125, 100)) + self.hass.block_till_done() + + self.assertEqual('test_light_rgb/set', + self.mock_publish.async_publish.mock_calls[0][1][0]) + self.assertEqual(2, + self.mock_publish.async_publish.mock_calls[0][1][2]) + self.assertEqual(False, + self.mock_publish.async_publish.mock_calls[0][1][3]) + # Get the sent message + message_json = json.loads( + self.mock_publish.async_publish.mock_calls[1][1][1]) + self.assertEqual(50, message_json["brightness"]) + self.assertEqual({ + 'r': 0, + 'g': 50, + 'b': 4, + }, message_json["color"]) + self.assertEqual("ON", message_json["state"]) + + state = self.hass.states.get('light.test') + self.assertEqual(STATE_ON, state.state) + self.assertEqual(50, state.attributes['brightness']) + self.assertEqual((125, 100), state.attributes['hs_color']) + def test_flash_short_and_long(self): \ # pylint: disable=invalid-name """Test for flash length being sent when included.""" From e687ca781ff99cbeeae9c70510b6a338e21c46ea Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 1 Apr 2018 17:38:29 +0200 Subject: [PATCH 263/924] Add pincode fallback (#13587) * Add pincode log statement * Moved msg to show_setup_msg --- homeassistant/components/homekit/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 2fa2ebd396a..af2c74d9c3c 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -36,6 +36,7 @@ def validate_entity_config(values): def show_setup_message(bridge, hass): """Display persistent notification with setup information.""" pin = bridge.pincode.decode() + _LOGGER.info('Pincode: %s', pin) message = 'To setup Home Assistant in the Home App, enter the ' \ 'following code:\n### {}'.format(pin) hass.components.persistent_notification.create( From 52d2139904b361b2d5b18b9205446ccfab7b4155 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 31 Mar 2018 15:07:29 +0200 Subject: [PATCH 264/924] Fix mysensors sensor type lookup (#13574) * Always return a safe default. --- homeassistant/components/sensor/mysensors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 3876b260dfc..66c36a8d9b1 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -81,5 +81,6 @@ class MySensorsSensor(mysensors.MySensorsEntity): TEMP_CELSIUS if self.gateway.metric else TEMP_FAHRENHEIT) sensor_type = SENSORS.get(set_req(self.value_type).name, [None, None]) if isinstance(sensor_type, dict): - sensor_type = sensor_type.get(pres(self.child_type).name) + sensor_type = sensor_type.get( + pres(self.child_type).name, [None, None]) return sensor_type From ff960c0c7a94c78bf40fd8d689ee8bf408b907fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Apr 2018 11:26:54 -0700 Subject: [PATCH 265/924] Version bump to 0.66.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ea3dc327c46..a597c25d094 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 66 -PATCH_VERSION = '1b0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9fb73c1bab113669a8d2b1cb326dd11bdfa7e727 Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Mon, 2 Apr 2018 09:45:38 +0200 Subject: [PATCH 266/924] Hue mireds value is actually 153 not 154 (#13601) --- homeassistant/components/light/__init__.py | 4 +++- tests/components/google_assistant/test_smart_home.py | 2 +- tests/components/light/test_demo.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index eea6c821fc0..39d3203795e 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -457,12 +457,14 @@ class Light(ToggleEntity): def min_mireds(self): """Return the coldest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed - return 154 + # https://developers.meethue.com/documentation/core-concepts + return 153 @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" # Default to the Philips Hue value that HA has always assumed + # https://developers.meethue.com/documentation/core-concepts return 500 @property diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index dd9373c782a..e284b026ad8 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -74,7 +74,7 @@ async def test_sync_message(hass): 'willReportState': False, 'attributes': { 'colorModel': 'rgb', - 'temperatureMinK': 6493, + 'temperatureMinK': 6535, 'temperatureMaxK': 2000, }, 'roomHint': 'Living Room' diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index ff984aff221..963cda6abc4 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -53,7 +53,7 @@ class TestDemoLight(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertEqual(400, state.attributes.get(light.ATTR_COLOR_TEMP)) - self.assertEqual(154, state.attributes.get(light.ATTR_MIN_MIREDS)) + self.assertEqual(153, state.attributes.get(light.ATTR_MIN_MIREDS)) self.assertEqual(500, state.attributes.get(light.ATTR_MAX_MIREDS)) self.assertEqual('none', state.attributes.get(light.ATTR_EFFECT)) light.turn_on(self.hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) From 53f08e313fede1c3813fdfbb2765fd6a01786c6c Mon Sep 17 00:00:00 2001 From: Wolfgang Malgadey Date: Mon, 2 Apr 2018 10:36:38 +0200 Subject: [PATCH 267/924] changed PyTado version (#13626) --- homeassistant/components/tado.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tado.py b/homeassistant/components/tado.py index cfba0a5c0c4..7c045518132 100644 --- a/homeassistant/components/tado.py +++ b/homeassistant/components/tado.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle -REQUIREMENTS = ['python-tado==0.2.2'] +REQUIREMENTS = ['python-tado==0.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2b7967f4981..95cb8c5462a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1001,7 +1001,7 @@ python-songpal==0.0.7 python-synology==0.1.0 # homeassistant.components.tado -python-tado==0.2.2 +python-tado==0.2.3 # homeassistant.components.telegram_bot python-telegram-bot==10.0.1 From 95e98925d1cc864c243bccc9a02f3897f099f959 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 2 Apr 2018 11:58:22 +0200 Subject: [PATCH 268/924] Upgrade py-cpuinfo to 4.0.0 (#13629) --- homeassistant/components/sensor/cpuspeed.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/cpuspeed.py b/homeassistant/components/sensor/cpuspeed.py index 25b7bba506c..c39ae43aef0 100644 --- a/homeassistant/components/sensor/cpuspeed.py +++ b/homeassistant/components/sensor/cpuspeed.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['py-cpuinfo==3.3.0'] +REQUIREMENTS = ['py-cpuinfo==4.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 95cb8c5462a..c1a44c8535b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -648,7 +648,7 @@ py-august==0.4.0 py-canary==0.5.0 # homeassistant.components.sensor.cpuspeed -py-cpuinfo==3.3.0 +py-cpuinfo==4.0.0 # homeassistant.components.melissa py-melissa-climate==1.0.6 From b342c43b09c54c746973afeb50389b2be7d2baef Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 2 Apr 2018 14:02:06 +0200 Subject: [PATCH 269/924] Add Switzerland (#13630) * Add Switzerland * remove space --- .../components/binary_sensor/workday.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index f5a7324d351..8935ad5115d 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -30,8 +30,8 @@ ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', - 'Sweden', 'SE', 'UnitedKingdom', 'UK', 'UnitedStates', 'US', - 'Wales'] + 'Sweden', 'SE', 'Switzerland', 'CH', 'UnitedKingdom', 'UK', + 'UnitedStates', 'US', 'Wales'] CONF_COUNTRY = 'country' CONF_PROVINCE = 'province' CONF_WORKDAYS = 'workdays' @@ -47,13 +47,13 @@ DEFAULT_OFFSET = 0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), - vol.Optional(CONF_PROVINCE): cv.string, + vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): + vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(int), + vol.Optional(CONF_PROVINCE): cv.string, vol.Optional(CONF_WORKDAYS, default=DEFAULT_WORKDAYS): vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), - vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): - vol.All(cv.ensure_list, [vol.In(ALLOWED_DAYS)]), }) @@ -74,14 +74,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if province: # 'state' and 'prov' are not interchangeable, so need to make # sure we use the right one - if (hasattr(obj_holidays, "PROVINCES") and + if (hasattr(obj_holidays, 'PROVINCES') and province in obj_holidays.PROVINCES): - obj_holidays = getattr(holidays, country)(prov=province, - years=year) - elif (hasattr(obj_holidays, "STATES") and + obj_holidays = getattr(holidays, country)( + prov=province, years=year) + elif (hasattr(obj_holidays, 'STATES') and province in obj_holidays.STATES): - obj_holidays = getattr(holidays, country)(state=province, - years=year) + obj_holidays = getattr(holidays, country)( + state=province, years=year) else: _LOGGER.error("There is no province/state %s in country %s", province, country) From 9ce4755f8ae18309dd28910ee7ff519fc90d46f1 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 2 Apr 2018 19:45:12 +0200 Subject: [PATCH 270/924] Xiaomi Mi WiFi Repeater 2 integration as device tracker (#13521) * Xiaomi Mi WiFi Repeater 2 integration as device tracker * Unused import removed * python-miio version pinned * Missing period added --- .../components/device_tracker/xiaomi_miio.py | 77 +++++++++++++++++++ requirements_all.txt | 1 + 2 files changed, 78 insertions(+) create mode 100644 homeassistant/components/device_tracker/xiaomi_miio.py diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py new file mode 100644 index 00000000000..61568892388 --- /dev/null +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -0,0 +1,77 @@ +""" +Support for Xiaomi Mi WiFi Repeater 2. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/device_tracker.xiaomi_miio/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.device_tracker import (DOMAIN, PLATFORM_SCHEMA, + DeviceScanner) +from homeassistant.const import (CONF_HOST, CONF_TOKEN) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), +}) + +REQUIREMENTS = ['python-miio==0.3.9'] + + +def get_scanner(hass, config): + """Return a Xiaomi MiIO device scanner.""" + from miio import WifiRepeater, DeviceException + + scanner = None + host = config[DOMAIN].get(CONF_HOST) + token = config[DOMAIN].get(CONF_TOKEN) + + _LOGGER.info( + "Initializing with host %s (token %s...)", host, token[:5]) + + try: + device = WifiRepeater(host, token) + device_info = device.info() + _LOGGER.info("%s %s %s detected", + device_info.model, + device_info.firmware_version, + device_info.hardware_version) + scanner = XiaomiMiioDeviceScanner(hass, device) + except DeviceException as ex: + _LOGGER.error("Device unavailable or token incorrect: %s", ex) + + return scanner + + +class XiaomiMiioDeviceScanner(DeviceScanner): + """This class queries a Xiaomi Mi WiFi Repeater.""" + + def __init__(self, hass, device): + """Initialize the scanner.""" + self.device = device + + async def async_scan_devices(self): + """Scan for devices and return a list containing found device ids.""" + from miio import DeviceException + + devices = [] + try: + station_info = await self.hass.async_add_job(self.device.status) + _LOGGER.debug("Got new station info: %s", station_info) + + for device in station_info['mat']: + devices.append(device['mac']) + + except DeviceException as ex: + _LOGGER.error("Got exception while fetching the state: %s", ex) + + return devices + + async def async_get_device_name(self, device): + """The repeater doesn't provide the name of the associated device.""" + return None diff --git a/requirements_all.txt b/requirements_all.txt index c1a44c8535b..e071049cfc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -961,6 +961,7 @@ python-juicenet==0.0.5 # homeassistant.components.lirc # python-lirc==1.2.3 +# homeassistant.components.device_tracker.xiaomi_miio # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio # homeassistant.components.remote.xiaomi_miio From 89f5a938c7940fde3d1d96cecb988a4ca5d538bb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 3 Apr 2018 18:27:08 +0200 Subject: [PATCH 271/924] Upgrade youtube_dl to 2018.04.03 (#13647) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index e10a713995b..85c569789a2 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.03.10'] +REQUIREMENTS = ['youtube_dl==2018.04.03'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e071049cfc8..a2e48d931c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1333,7 +1333,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.03.10 +youtube_dl==2018.04.03 # homeassistant.components.light.zengge zengge==0.2 From bfb49c2e58429e5c4860cc4787c92feacf3fcb80 Mon Sep 17 00:00:00 2001 From: Kevin Raddatz Date: Tue, 3 Apr 2018 18:28:42 +0200 Subject: [PATCH 272/924] Update plex.py (#13659) fixed IndexError on line 131 --- homeassistant/components/sensor/plex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/plex.py b/homeassistant/components/sensor/plex.py index 505983cb3a7..b61e1bce0da 100644 --- a/homeassistant/components/sensor/plex.py +++ b/homeassistant/components/sensor/plex.py @@ -128,7 +128,7 @@ class PlexSensor(Entity): season_title += " ({0})".format(sess.show().year) season_episode = "S{0}".format(sess.parentIndex) if sess.index is not None: - season_episode += " · E{1}".format(sess.index) + season_episode += " · E{0}".format(sess.index) episode_title = sess.title now_playing_title = "{0} - {1} - {2}".format(season_title, season_episode, From 92bd932679a9455826f36913dde88b5bd025a9f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Apr 2018 14:23:21 -0700 Subject: [PATCH 273/924] Always enable config entries & remove config_entry_example (#13663) --- homeassistant/components/config/__init__.py | 10 +- .../.translations/de.json | 24 ----- .../.translations/en.json | 24 ----- .../.translations/fi.json | 11 --- .../.translations/ko.json | 24 ----- .../.translations/nl.json | 24 ----- .../.translations/no.json | 24 ----- .../.translations/pl.json | 24 ----- .../.translations/ro.json | 15 --- .../.translations/sl.json | 24 ----- .../.translations/vi.json | 24 ----- .../.translations/zh-Hans.json | 24 ----- .../config_entry_example/__init__.py | 98 ------------------- .../config_entry_example/strings.json | 24 ----- homeassistant/config_entries.py | 3 +- tests/components/test_config_entry_example.py | 38 ------- 16 files changed, 2 insertions(+), 413 deletions(-) delete mode 100644 homeassistant/components/config_entry_example/.translations/de.json delete mode 100644 homeassistant/components/config_entry_example/.translations/en.json delete mode 100644 homeassistant/components/config_entry_example/.translations/fi.json delete mode 100644 homeassistant/components/config_entry_example/.translations/ko.json delete mode 100644 homeassistant/components/config_entry_example/.translations/nl.json delete mode 100644 homeassistant/components/config_entry_example/.translations/no.json delete mode 100644 homeassistant/components/config_entry_example/.translations/pl.json delete mode 100644 homeassistant/components/config_entry_example/.translations/ro.json delete mode 100644 homeassistant/components/config_entry_example/.translations/sl.json delete mode 100644 homeassistant/components/config_entry_example/.translations/vi.json delete mode 100644 homeassistant/components/config_entry_example/.translations/zh-Hans.json delete mode 100644 homeassistant/components/config_entry_example/__init__.py delete mode 100644 homeassistant/components/config_entry_example/strings.json delete mode 100644 tests/components/test_config_entry_example.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 601b12ffe4a..4d0295c382a 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -14,24 +14,16 @@ from homeassistant.util.yaml import load_yaml, dump DOMAIN = 'config' DEPENDENCIES = ['http'] SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script', - 'entity_registry') + 'entity_registry', 'config_entries') ON_DEMAND = ('zwave',) -FEATURE_FLAGS = ('config_entries',) @asyncio.coroutine def async_setup(hass, config): """Set up the config component.""" - global SECTIONS - yield from hass.components.frontend.async_register_built_in_panel( 'config', 'config', 'mdi:settings') - # Temporary way of allowing people to opt-in for unreleased config sections - for key, value in config.get(DOMAIN, {}).items(): - if key in FEATURE_FLAGS and value: - SECTIONS += (key,) - @asyncio.coroutine def setup_panel(panel_name): """Set up a panel.""" diff --git a/homeassistant/components/config_entry_example/.translations/de.json b/homeassistant/components/config_entry_example/.translations/de.json deleted file mode 100644 index 75b88f2f822..00000000000 --- a/homeassistant/components/config_entry_example/.translations/de.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Ung\u00fcltige Objekt-ID" - }, - "step": { - "init": { - "data": { - "object_id": "Objekt-ID" - }, - "description": "Bitte gib eine Objekt_ID f\u00fcr das Test-Entity ein.", - "title": "W\u00e4hle eine Objekt-ID" - }, - "name": { - "data": { - "name": "Name" - }, - "description": "Bitte gib einen Namen f\u00fcr das Test-Entity ein", - "title": "Name des Test-Entity" - } - }, - "title": "Beispiel Konfig-Eintrag" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/en.json b/homeassistant/components/config_entry_example/.translations/en.json deleted file mode 100644 index ec24d01ebc8..00000000000 --- a/homeassistant/components/config_entry_example/.translations/en.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Invalid object ID" - }, - "step": { - "init": { - "data": { - "object_id": "Object ID" - }, - "description": "Please enter an object_id for the test entity.", - "title": "Pick object id" - }, - "name": { - "data": { - "name": "Name" - }, - "description": "Please enter a name for the test entity.", - "title": "Name of the entity" - } - }, - "title": "Config Entry Example" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/fi.json b/homeassistant/components/config_entry_example/.translations/fi.json deleted file mode 100644 index 054a6f372bc..00000000000 --- a/homeassistant/components/config_entry_example/.translations/fi.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "name": { - "data": { - "name": "Nimi" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ko.json b/homeassistant/components/config_entry_example/.translations/ko.json deleted file mode 100644 index f12e3fc52f1..00000000000 --- a/homeassistant/components/config_entry_example/.translations/ko.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "\uc624\ube0c\uc81d\ud2b8 ID\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "init": { - "data": { - "object_id": "\uc624\ube0c\uc81d\ud2b8 ID" - }, - "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc624\ube0c\uc81d\ud2b8 ID \ub97c \uc785\ub825\ud558\uc138\uc694", - "title": "\uc624\ube0c\uc81d\ud2b8 ID \uc120\ud0dd" - }, - "name": { - "data": { - "name": "\uc774\ub984" - }, - "description": "\ud14c\uc2a4\ud2b8 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984\uc744 \uc785\ub825\ud558\uc138\uc694.", - "title": "\uad6c\uc131\uc694\uc18c \uc774\ub984" - } - }, - "title": "\uc785\ub825 \uc608\uc81c \uad6c\uc131" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/nl.json b/homeassistant/components/config_entry_example/.translations/nl.json deleted file mode 100644 index 7b52ac88cf0..00000000000 --- a/homeassistant/components/config_entry_example/.translations/nl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Ongeldig object ID" - }, - "step": { - "init": { - "data": { - "object_id": "Object ID" - }, - "description": "Voer een object_id in voor het testen van de entiteit.", - "title": "Kies object id" - }, - "name": { - "data": { - "name": "Naam" - }, - "description": "Voer een naam in voor het testen van de entiteit.", - "title": "Naam van de entiteit" - } - }, - "title": "Voorbeeld van de config vermelding" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/no.json b/homeassistant/components/config_entry_example/.translations/no.json deleted file mode 100644 index 380c539f8af..00000000000 --- a/homeassistant/components/config_entry_example/.translations/no.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Ugyldig objekt ID" - }, - "step": { - "init": { - "data": { - "object_id": "Objekt ID" - }, - "description": "Vennligst skriv inn en object_id for testenheten.", - "title": "Velg objekt ID" - }, - "name": { - "data": { - "name": "Navn" - }, - "description": "Vennligst skriv inn et navn for testenheten.", - "title": "Navn p\u00e5 enheten" - } - }, - "title": "Konfigureringseksempel" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/pl.json b/homeassistant/components/config_entry_example/.translations/pl.json deleted file mode 100644 index 35cca168249..00000000000 --- a/homeassistant/components/config_entry_example/.translations/pl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Nieprawid\u0142owy identyfikator obiektu" - }, - "step": { - "init": { - "data": { - "object_id": "Identyfikator obiektu" - }, - "description": "Prosz\u0119 wprowadzi\u0107 identyfikator obiektu (object_id) dla jednostki testowej.", - "title": "Wybierz identyfikator obiektu" - }, - "name": { - "data": { - "name": "Nazwa" - }, - "description": "Prosz\u0119 wprowadzi\u0107 nazw\u0119 dla jednostki testowej.", - "title": "Nazwa jednostki" - } - }, - "title": "Przyk\u0142ad wpisu do konfiguracji" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/ro.json b/homeassistant/components/config_entry_example/.translations/ro.json deleted file mode 100644 index 1a4cdd6bbb7..00000000000 --- a/homeassistant/components/config_entry_example/.translations/ro.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "step": { - "init": { - "description": "Introduce\u021bi un obiect_id pentru entitatea testat\u0103.", - "title": "Alege\u021bi id-ul obiectului" - }, - "name": { - "data": { - "name": "Nume" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/sl.json b/homeassistant/components/config_entry_example/.translations/sl.json deleted file mode 100644 index 11d2d3f5e80..00000000000 --- a/homeassistant/components/config_entry_example/.translations/sl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "Neveljaven ID objekta" - }, - "step": { - "init": { - "data": { - "object_id": "ID objekta" - }, - "description": "Prosimo, vnesite Id_objekta za testni subjekt.", - "title": "Izberite ID objekta" - }, - "name": { - "data": { - "name": "Ime" - }, - "description": "Vnesite ime za testni subjekt.", - "title": "Ime subjekta" - } - }, - "title": "Primer nastavitve" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/vi.json b/homeassistant/components/config_entry_example/.translations/vi.json deleted file mode 100644 index e40c4d38e9f..00000000000 --- a/homeassistant/components/config_entry_example/.translations/vi.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng kh\u00f4ng h\u1ee3p l\u1ec7" - }, - "step": { - "init": { - "data": { - "object_id": "ID \u0111\u1ed1i t\u01b0\u1ee3ng" - }, - "description": "Xin vui l\u00f2ng nh\u1eadp m\u1ed9t object_id cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", - "title": "Ch\u1ecdn id \u0111\u1ed1i t\u01b0\u1ee3ng" - }, - "name": { - "data": { - "name": "T\u00ean" - }, - "description": "Xin vui l\u00f2ng nh\u1eadp t\u00ean cho th\u1eed nghi\u1ec7m th\u1ef1c th\u1ec3.", - "title": "T\u00ean c\u1ee7a th\u1ef1c th\u1ec3" - } - }, - "title": "V\u00ed d\u1ee5 v\u1ec1 c\u1ea5u h\u00ecnh th\u1ef1c th\u1ec3" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/.translations/zh-Hans.json b/homeassistant/components/config_entry_example/.translations/zh-Hans.json deleted file mode 100644 index ee10e6d7b48..00000000000 --- a/homeassistant/components/config_entry_example/.translations/zh-Hans.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "invalid_object_id": "\u65e0\u6548\u7684\u5bf9\u8c61 ID" - }, - "step": { - "init": { - "data": { - "object_id": "\u5bf9\u8c61 ID" - }, - "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u5bf9\u8c61 ID", - "title": "\u8bf7\u9009\u62e9\u5bf9\u8c61 ID" - }, - "name": { - "data": { - "name": "\u540d\u79f0" - }, - "description": "\u8bf7\u4e3a\u6d4b\u8bd5\u8bbe\u5907\u8f93\u5165\u540d\u79f0", - "title": "\u8bbe\u5907\u540d\u79f0" - } - }, - "title": "\u6837\u4f8b\u914d\u7f6e\u6761\u76ee" - } -} \ No newline at end of file diff --git a/homeassistant/components/config_entry_example/__init__.py b/homeassistant/components/config_entry_example/__init__.py deleted file mode 100644 index 3ebfdc3a183..00000000000 --- a/homeassistant/components/config_entry_example/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Example component to show how config entries work.""" - -import asyncio - -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import ATTR_FRIENDLY_NAME -from homeassistant.util import slugify - - -DOMAIN = 'config_entry_example' - - -@asyncio.coroutine -def async_setup(hass, config): - """Setup for our example component.""" - return True - - -@asyncio.coroutine -def async_setup_entry(hass, entry): - """Initialize an entry.""" - entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id']) - hass.states.async_set(entity_id, 'loaded', { - ATTR_FRIENDLY_NAME: entry.data['name'] - }) - - # Indicate setup was successful. - return True - - -@asyncio.coroutine -def async_unload_entry(hass, entry): - """Unload an entry.""" - entity_id = '{}.{}'.format(DOMAIN, entry.data['object_id']) - hass.states.async_remove(entity_id) - - # Indicate unload was successful. - return True - - -@config_entries.HANDLERS.register(DOMAIN) -class ExampleConfigFlow(config_entries.ConfigFlowHandler): - """Handle an example configuration flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize a Hue config handler.""" - self.object_id = None - - @asyncio.coroutine - def async_step_init(self, user_input=None): - """Start config flow.""" - errors = None - if user_input is not None: - object_id = user_input['object_id'] - - if object_id != '' and object_id == slugify(object_id): - self.object_id = user_input['object_id'] - return (yield from self.async_step_name()) - - errors = { - 'object_id': 'invalid_object_id' - } - - return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - 'object_id': str - }), - errors=errors - ) - - @asyncio.coroutine - def async_step_name(self, user_input=None): - """Ask user to enter the name.""" - errors = None - if user_input is not None: - name = user_input['name'] - - if name != '': - return self.async_create_entry( - title=name, - data={ - 'name': name, - 'object_id': self.object_id, - } - ) - - return self.async_show_form( - step_id='name', - data_schema=vol.Schema({ - 'name': str - }), - errors=errors - ) diff --git a/homeassistant/components/config_entry_example/strings.json b/homeassistant/components/config_entry_example/strings.json deleted file mode 100644 index a7a8cd4025b..00000000000 --- a/homeassistant/components/config_entry_example/strings.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "title": "Config Entry Example", - "step": { - "init": { - "title": "Pick object id", - "description": "Please enter an object_id for the test entity.", - "data": { - "object_id": "Object ID" - } - }, - "name": { - "title": "Name of the entity", - "description": "Please enter a name for the test entity.", - "data": { - "name": "Name" - } - } - }, - "error": { - "invalid_object_id": "Invalid object ID" - } - } -} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6b2000b2ea6..69491af1aad 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -126,9 +126,8 @@ _LOGGER = logging.getLogger(__name__) HANDLERS = Registry() # Components that have config flows. In future we will auto-generate this list. FLOWS = [ - 'config_entry_example', - 'hue', 'deconz', + 'hue', ] SOURCE_USER = 'user' diff --git a/tests/components/test_config_entry_example.py b/tests/components/test_config_entry_example.py deleted file mode 100644 index 31084384c31..00000000000 --- a/tests/components/test_config_entry_example.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Test the config entry example component.""" -import asyncio - -from homeassistant import config_entries - - -@asyncio.coroutine -def test_flow_works(hass): - """Test that the config flow works.""" - result = yield from hass.config_entries.flow.async_init( - 'config_entry_example') - - assert result['type'] == config_entries.RESULT_TYPE_FORM - - result = yield from hass.config_entries.flow.async_configure( - result['flow_id'], { - 'object_id': 'bla' - }) - - assert result['type'] == config_entries.RESULT_TYPE_FORM - - result = yield from hass.config_entries.flow.async_configure( - result['flow_id'], { - 'name': 'Hello' - }) - - assert result['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY - state = hass.states.get('config_entry_example.bla') - assert state is not None - assert state.name == 'Hello' - assert 'config_entry_example' in hass.config.components - assert len(hass.config_entries.async_entries()) == 1 - - # Test removing entry. - entry = hass.config_entries.async_entries()[0] - yield from hass.config_entries.async_remove(entry.entry_id) - state = hass.states.get('config_entry_example.bla') - assert state is None From 568c6c16fa72580ec0fd551661700fbad07c9b04 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 3 Apr 2018 19:05:06 -0400 Subject: [PATCH 274/924] Add missing service docs for hs_color (#13667) --- homeassistant/components/light/services.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 9645e50d06e..3507c6d2cda 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -15,6 +15,9 @@ turn_on: color_name: description: A human readable color name. example: 'red' + hs_color: + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + example: '[300, 70]' xy_color: description: Color for the light in XY-format. example: '[0.52, 0.43]' From 13bda2669eaf063b75bc23248edb4db18a7b7cb7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Apr 2018 16:49:13 -0700 Subject: [PATCH 275/924] Bump frontend to 20180404.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1fbfe94bb0d..3fc3eff0a14 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180401.0'] +REQUIREMENTS = ['home-assistant-frontend==20180404.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a2e48d931c8..1cfe6df643f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -379,7 +379,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180401.0 +home-assistant-frontend==20180404.0 # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3e9ae51a60..120d2c73024 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180401.0 +home-assistant-frontend==20180404.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 032d6963d841cea577495917c614dcded1986514 Mon Sep 17 00:00:00 2001 From: mountainsandcode Date: Wed, 4 Apr 2018 15:34:01 +0200 Subject: [PATCH 276/924] Add regex functions as templating helpers (#13631) * Add regex functions as templating helpers * Add regex functions as templating helpers - Style fixed * Templating filters, third time lucky? --- homeassistant/helpers/template.py | 37 +++++++++++++++++++++ tests/helpers/test_template.py | 53 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a04023cfc4f..353fda28875 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -516,6 +516,39 @@ def forgiving_float(value): return value +def regex_match(value, find='', ignorecase=False): + """Match value using regex.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + return bool(re.match(find, value, flags)) + + +def regex_replace(value='', find='', replace='', ignorecase=False): + """Replace using regex.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + regex = re.compile(find, flags) + return regex.sub(replace, value) + + +def regex_search(value, find='', ignorecase=False): + """Search using regex.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + return bool(re.search(find, value, flags)) + + +def regex_findall_index(value, find='', index=0, ignorecase=False): + """Find all matches using regex and then pick specific match index.""" + if not isinstance(value, str): + value = str(value) + flags = re.I if ignorecase else 0 + return re.findall(find, value, flags)[index] + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -545,6 +578,10 @@ ENV.filters['is_defined'] = fail_when_undefined ENV.filters['max'] = max ENV.filters['min'] = min ENV.filters['random'] = random_every_time +ENV.filters['regex_match'] = regex_match +ENV.filters['regex_replace'] = regex_replace +ENV.filters['regex_search'] = regex_search +ENV.filters['regex_findall_index'] = regex_findall_index ENV.globals['log'] = logarithm ENV.globals['float'] = forgiving_float ENV.globals['now'] = dt_util.now diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 693c3909924..650b98509d0 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -441,6 +441,59 @@ class TestHelpersTemplate(unittest.TestCase): template.Template('{{ utcnow().isoformat() }}', self.hass).render()) + def test_regex_match(self): + """Test regex_match method.""" + tpl = template.Template(""" +{{ '123-456-7890' | regex_match('(\d{3})-(\d{3})-(\d{4})') }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" +{{ 'home assistant test' | regex_match('Home', True) }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" + {{ 'Another home assistant test' | regex_match('home') }} + """, self.hass) + self.assertEqual('False', tpl.render()) + + def test_regex_search(self): + """Test regex_search method.""" + tpl = template.Template(""" +{{ '123-456-7890' | regex_search('(\d{3})-(\d{3})-(\d{4})') }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" +{{ 'home assistant test' | regex_search('Home', True) }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + tpl = template.Template(""" + {{ 'Another home assistant test' | regex_search('home') }} + """, self.hass) + self.assertEqual('True', tpl.render()) + + def test_regex_replace(self): + """Test regex_replace method.""" + tpl = template.Template(""" +{{ 'Hello World' | regex_replace('(Hello\s)',) }} + """, self.hass) + self.assertEqual('World', tpl.render()) + + def test_regex_findall_index(self): + """Test regex_findall_index method.""" + tpl = template.Template(""" +{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }} + """, self.hass) + self.assertEqual('JFK', tpl.render()) + + tpl = template.Template(""" +{{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }} + """, self.hass) + self.assertEqual('LHR', tpl.render()) + def test_distance_function_with_1_state(self): """Test distance function with 1 state.""" self.hass.states.set('test.object', 'happy', { From 4b2fdd243a68895fa36563f9f5cb0a6d4050cf82 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Wed, 4 Apr 2018 15:37:14 +0200 Subject: [PATCH 277/924] Channel up and down for webostv (#13624) --- homeassistant/components/media_player/webostv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index acd1ffad6eb..860d69e22c3 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -357,8 +357,8 @@ class LgWebOSDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._client.fast_forward() + self._client.channel_up() def media_previous_track(self): """Send the previous track command.""" - self._client.rewind() + self._client.channel_down() From 9ce02d2717920374c9ea064004b14c866fa242e2 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 4 Apr 2018 17:35:33 +0300 Subject: [PATCH 278/924] Added headers configuration variable to notify.rest component (#13674) * Added headers configuration variable to notify.rest component * Fix code style --- homeassistant/components/notify/rest.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/notify/rest.py b/homeassistant/components/notify/rest.py index 73618c19502..40b09dc3c72 100644 --- a/homeassistant/components/notify/rest.py +++ b/homeassistant/components/notify/rest.py @@ -12,7 +12,8 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, ATTR_TITLE_DEFAULT, BaseNotificationService, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME) +from homeassistant.const import (CONF_RESOURCE, CONF_METHOD, CONF_NAME, + CONF_HEADERS) import homeassistant.helpers.config_validation as cv CONF_DATA = 'data' @@ -29,6 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ default=DEFAULT_MESSAGE_PARAM_NAME): cv.string, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(['POST', 'GET', 'POST_JSON']), + vol.Optional(CONF_HEADERS): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string, vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string, @@ -43,6 +45,7 @@ def get_service(hass, config, discovery_info=None): """Get the RESTful notification service.""" resource = config.get(CONF_RESOURCE) method = config.get(CONF_METHOD) + headers = config.get(CONF_HEADERS) message_param_name = config.get(CONF_MESSAGE_PARAMETER_NAME) title_param_name = config.get(CONF_TITLE_PARAMETER_NAME) target_param_name = config.get(CONF_TARGET_PARAMETER_NAME) @@ -50,19 +53,20 @@ def get_service(hass, config, discovery_info=None): data_template = config.get(CONF_DATA_TEMPLATE) return RestNotificationService( - hass, resource, method, message_param_name, + hass, resource, method, headers, message_param_name, title_param_name, target_param_name, data, data_template) class RestNotificationService(BaseNotificationService): """Implementation of a notification service for REST.""" - def __init__(self, hass, resource, method, message_param_name, + def __init__(self, hass, resource, method, headers, message_param_name, title_param_name, target_param_name, data, data_template): """Initialize the service.""" self._resource = resource self._hass = hass self._method = method.upper() + self._headers = headers self._message_param_name = message_param_name self._title_param_name = title_param_name self._target_param_name = target_param_name @@ -99,11 +103,14 @@ class RestNotificationService(BaseNotificationService): data.update(_data_template_creator(self._data_template)) if self._method == 'POST': - response = requests.post(self._resource, data=data, timeout=10) + response = requests.post(self._resource, headers=self._headers, + data=data, timeout=10) elif self._method == 'POST_JSON': - response = requests.post(self._resource, json=data, timeout=10) + response = requests.post(self._resource, headers=self._headers, + json=data, timeout=10) else: # default GET - response = requests.get(self._resource, params=data, timeout=10) + response = requests.get(self._resource, headers=self._headers, + params=data, timeout=10) if response.status_code not in (200, 201): _LOGGER.exception( From 415af5e2571969a9f813ee1fbcc08bd0df4c1a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 5 Apr 2018 00:30:02 +0300 Subject: [PATCH 279/924] Spelling fixes (#13681) --- homeassistant/components/alarm_control_panel/ifttt.py | 2 +- homeassistant/components/climate/nest.py | 2 +- homeassistant/components/device_tracker/ubus.py | 2 +- homeassistant/components/remote/xiaomi_miio.py | 2 +- homeassistant/components/smappee.py | 2 +- tests/components/homekit/test_accessories.py | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 5303c24876e..7bdc1ccd9d9 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -93,7 +93,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class IFTTTAlarmPanel(alarm.AlarmControlPanel): - """Representation of an alarm control panel controlled throught IFTTT.""" + """Representation of an alarm control panel controlled through IFTTT.""" def __init__(self, name, code, event_away, event_home, event_night, event_disarm, optimistic): diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index e5c21158acb..d11f6890a7b 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -179,7 +179,7 @@ class NestThermostat(ClimateDevice): try: self.device.target = temp except nest.nest.APIError: - _LOGGER.error("An error occured while setting the temperature") + _LOGGER.error("An error occurred while setting the temperature") def set_operation_mode(self, operation_mode): """Set operation mode.""" diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index c75529655f4..dd12df7b070 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -95,7 +95,7 @@ class UbusDeviceScanner(DeviceScanner): return self.last_results def _generate_mac2name(self): - """Return empty MAC to name dict. Overriden if DHCP server is set.""" + """Return empty MAC to name dict. Overridden if DHCP server is set.""" self.mac2name = dict() @_refresh_on_access_denied diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index b71eb2cb447..e731d421e69 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -138,7 +138,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): while (utcnow() - start_time) < timedelta(seconds=timeout): message = yield from hass.async_add_job( device.read, slot) - _LOGGER.debug("Message recieved from device: '%s'", message) + _LOGGER.debug("Message received from device: '%s'", message) if 'code' in message and message['code']: log_msg = "Received command is: {}".format(message['code']) diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py index 0111e0437fb..1241679770b 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee.py @@ -264,7 +264,7 @@ class Smappee(object): return True def active_power(self): - """Get sum of all instantanious active power values from local hub.""" + """Get sum of all instantaneous active power values from local hub.""" if not self.is_local_active: return diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 4d230b81686..6599ec83335 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -153,13 +153,13 @@ class TestAccessories(unittest.TestCase): def test_home_driver(self): """Test HomeDriver class.""" bridge = HomeBridge(None) - ip_adress = '127.0.0.1' + ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - HomeDriver(bridge, ip_adress, port, path) + HomeDriver(bridge, ip_address, port, path) self.assertEqual( - mock_driver.call_args, call(bridge, ip_adress, port, path)) + mock_driver.call_args, call(bridge, ip_address, port, path)) From 301077ded9dcaebf9b1e45561a8b1871d9895441 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 5 Apr 2018 00:42:00 +0200 Subject: [PATCH 280/924] Xiaomi MiIO Light: White Philips Candle Light support (#13682) --- homeassistant/components/light/xiaomi_miio.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/xiaomi_miio.py b/homeassistant/components/light/xiaomi_miio.py index 21a27c33203..24eab7ebd4a 100644 --- a/homeassistant/components/light/xiaomi_miio.py +++ b/homeassistant/components/light/xiaomi_miio.py @@ -38,6 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'philips.light.ceiling', 'philips.light.zyceiling', 'philips.light.bulb', + 'philips.light.candle', 'philips.light.candle2']), }) @@ -149,7 +150,9 @@ async def async_setup_platform(hass, config, async_add_devices, device = XiaomiPhilipsCeilingLamp(name, light, model, unique_id) devices.append(device) hass.data[DATA_KEY][host] = device - elif model in ['philips.light.bulb', 'philips.light.candle2']: + elif model in ['philips.light.bulb', + 'philips.light.candle', + 'philips.light.candle2']: from miio import PhilipsBulb light = PhilipsBulb(host, token) device = XiaomiPhilipsBulb(name, light, model, unique_id) From f263a931f7ac3806ac914b6cdc2be3bc0dbf0734 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 5 Apr 2018 00:46:27 +0200 Subject: [PATCH 281/924] Bugfixes HomeKit covers, lights (#13689) * covers -> current_position attribute * lights -> hue and saturation attribute --- .../components/homekit/type_covers.py | 19 ++++++++----------- .../components/homekit/type_lights.py | 4 +++- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 7616ef05fdf..9a526508117 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -63,14 +63,11 @@ class WindowCovering(HomeAccessory): return current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) - if current_position is None: - return - - self.current_position = int(current_position) - self.char_current_position.set_value(self.current_position) - - if self.homekit_target is None or \ - abs(self.current_position - self.homekit_target) < 6: - self.char_target_position.set_value(self.current_position) - self.char_position_state.set_value(2) - self.homekit_target = None + if isinstance(current_position, int): + self.current_position = current_position + self.char_current_position.set_value(self.current_position) + if self.homekit_target is None or \ + abs(self.current_position - self.homekit_target) < 6: + self.char_target_position.set_value(self.current_position) + self.char_position_state.set_value(2) + self.homekit_target = None diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index d88e7100131..d5b967797bb 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -146,7 +146,9 @@ class Light(HomeAccessory): hue, saturation = new_state.attributes.get( ATTR_HS_COLOR, (None, None)) if not self._flag[RGB_COLOR] and ( - hue != self._hue or saturation != self._saturation): + hue != self._hue or saturation != self._saturation) and \ + isinstance(hue, (int, float)) and \ + isinstance(saturation, (int, float)): self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) From 692b2644c7e902a9aeddc53f99cafe0543d0d89c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 5 Apr 2018 00:52:25 +0200 Subject: [PATCH 282/924] Minor style changes, cleanup (#13654) * Minor style changes, cleanup * Change 'self._entity.id' to 'self.entity_id' * Use const 'STATE_OFF' * Added CATEGORY constants * Removed *args from accessory types * Changed 'self._hass' to 'self.hass' * Added log debug msg (for added lights) --- homeassistant/components/homekit/__init__.py | 4 +- .../components/homekit/accessories.py | 10 ++--- homeassistant/components/homekit/const.py | 4 ++ .../components/homekit/type_covers.py | 20 +++++----- .../components/homekit/type_lights.py | 34 ++++++++-------- .../homekit/type_security_systems.py | 23 +++++------ .../components/homekit/type_sensors.py | 16 ++++---- .../components/homekit/type_switches.py | 18 ++++----- .../components/homekit/type_thermostats.py | 40 +++++++++---------- .../homekit/test_type_thermostats.py | 5 +-- 10 files changed, 87 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8ef8445aa70..25b54b6c723 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -102,8 +102,7 @@ def get_accessory(hass, state, aid, config): aid=aid) elif state.domain == 'alarm_control_panel': - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, - 'SecuritySystem') + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') return TYPES['SecuritySystem'](hass, state.entity_id, state.name, alarm_code=config.get(ATTR_CODE), aid=aid) @@ -120,6 +119,7 @@ def get_accessory(hass, state, aid, config): state.name, support_auto, aid=aid) elif state.domain == 'light': + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light') return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) elif state.domain == 'switch' or state.domain == 'remote' \ diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4c4409e6dfc..3e9f22ef2bc 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -65,10 +65,10 @@ class HomeAccessory(Accessory): def run(self): """Method called by accessory after driver is started.""" - state = self._hass.states.get(self._entity_id) + state = self.hass.states.get(self.entity_id) self.update_state(new_state=state) async_track_state_change( - self._hass, self._entity_id, self.update_state) + self.hass, self.entity_id, self.update_state) class HomeBridge(Bridge): @@ -79,7 +79,7 @@ class HomeBridge(Bridge): """Initialize a Bridge object.""" super().__init__(name, **kwargs) set_accessory_info(self, name, model) - self._hass = hass + self.hass = hass def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) @@ -92,12 +92,12 @@ class HomeBridge(Bridge): def add_paired_client(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" super().add_paired_client(client_uuid, client_public) - dismiss_setup_message(self._hass) + dismiss_setup_message(self.hass) def remove_paired_client(self, client_uuid): """Override super function to show setup message if unpaired.""" super().remove_paired_client(client_uuid) - show_setup_message(self, self._hass) + show_setup_message(self, self.hass) class HomeDriver(AccessoryDriver): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index a45c8298b78..c2a10f61fcb 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -24,8 +24,12 @@ BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' # #### Categories #### +CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' CATEGORY_LIGHT = 'LIGHTBULB' CATEGORY_SENSOR = 'SENSOR' +CATEGORY_SWITCH = 'SWITCH' +CATEGORY_THERMOSTAT = 'THERMOSTAT' +CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 9a526508117..3650a948f5d 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -6,8 +6,8 @@ from homeassistant.components.cover import ATTR_CURRENT_POSITION from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE) + CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, + CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE) _LOGGER = logging.getLogger(__name__) @@ -20,13 +20,13 @@ class WindowCovering(HomeAccessory): The cover entity must support: set_cover_position. """ - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, **kwargs): """Initialize a WindowCovering accessory object.""" - super().__init__(display_name, entity_id, 'WINDOW_COVERING', - *args, **kwargs) + super().__init__(display_name, entity_id, + CATEGORY_WINDOW_COVERING, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self.current_position = None self.homekit_target = None @@ -48,14 +48,14 @@ class WindowCovering(HomeAccessory): """Move cover to value if call came from HomeKit.""" self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: - _LOGGER.debug('%s: Set position to %d', self._entity_id, value) + _LOGGER.debug('%s: Set position to %d', self.entity_id, value) self.homekit_target = value if value > self.current_position: self.char_position_state.set_value(1) elif value < self.current_position: self.char_position_state.set_value(0) - self._hass.components.cover.set_cover_position( - value, self._entity_id) + self.hass.components.cover.set_cover_position( + value, self.entity_id) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update cover position after state changed.""" diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index d5b967797bb..b02aee1e714 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -23,19 +23,19 @@ class Light(HomeAccessory): Currently supports: state, brightness, rgb_color. """ - def __init__(self, hass, entity_id, name, *args, **kwargs): + def __init__(self, hass, entity_id, name, **kwargs): """Initialize a new Light accessory object.""" - super().__init__(name, entity_id, CATEGORY_LIGHT, *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: False} self._state = 0 self.chars = [] - self._features = self._hass.states.get(self._entity_id) \ + self._features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) @@ -70,29 +70,29 @@ class Light(HomeAccessory): if self._state == value: return - _LOGGER.debug('%s: Set state to %d', self._entity_id, value) + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True self.char_on.set_value(value, should_callback=False) if value == 1: - self._hass.components.light.turn_on(self._entity_id) + self.hass.components.light.turn_on(self.entity_id) elif value == 0: - self._hass.components.light.turn_off(self._entity_id) + self.hass.components.light.turn_off(self.entity_id) def set_brightness(self, value): """Set brightness if call came from HomeKit.""" - _LOGGER.debug('%s: Set brightness to %d', self._entity_id, value) + _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True self.char_brightness.set_value(value, should_callback=False) if value != 0: - self._hass.components.light.turn_on( - self._entity_id, brightness_pct=value) + self.hass.components.light.turn_on( + self.entity_id, brightness_pct=value) else: - self._hass.components.light.turn_off(self._entity_id) + self.hass.components.light.turn_off(self.entity_id) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" - _LOGGER.debug('%s: Set saturation to %d', self._entity_id, value) + _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) self._flag[CHAR_SATURATION] = True self.char_saturation.set_value(value, should_callback=False) self._saturation = value @@ -100,7 +100,7 @@ class Light(HomeAccessory): def set_hue(self, value): """Set hue if call came from HomeKit.""" - _LOGGER.debug('%s: Set hue to %d', self._entity_id, value) + _LOGGER.debug('%s: Set hue to %d', self.entity_id, value) self._flag[CHAR_HUE] = True self.char_hue.set_value(value, should_callback=False) self._hue = value @@ -112,11 +112,11 @@ class Light(HomeAccessory): if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: color = (self._hue, self._saturation) - _LOGGER.debug('%s: Set hs_color to %s', self._entity_id, color) + _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self._hass.components.light.turn_on( - self._entity_id, hs_color=color) + self.hass.components.light.turn_on( + self.entity_id, hs_color=color) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index b23522f0ea2..2cce6653db3 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -9,8 +9,8 @@ from homeassistant.const import ( from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) _LOGGER = logging.getLogger(__name__) @@ -27,14 +27,13 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, hass, entity_id, display_name, - alarm_code, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs): """Initialize a SecuritySystem accessory object.""" - super().__init__(display_name, entity_id, 'ALARM_SYSTEM', - *args, **kwargs) + super().__init__(display_name, entity_id, + CATEGORY_ALARM_SYSTEM, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._alarm_code = alarm_code self.flag_target_state = False @@ -52,16 +51,16 @@ class SecuritySystem(HomeAccessory): def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set security state to %d', - self._entity_id, value) + self.entity_id, value) self.flag_target_state = True self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] - params = {ATTR_ENTITY_ID: self._entity_id} + params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self._hass.services.call('alarm_control_panel', service, params) + self.hass.services.call('alarm_control_panel', service, params) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" @@ -76,7 +75,7 @@ class SecuritySystem(HomeAccessory): self.char_current_state.set_value(current_security_state, should_callback=False) _LOGGER.debug('%s: Updated current state to %s (%d)', - self._entity_id, hass_state, current_security_state) + self.entity_id, hass_state, current_security_state) if not self.flag_target_state: self.char_target_state.set_value(current_security_state, diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index e980ce4a316..9768c4a51d4 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -23,12 +23,12 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, name, *args, **kwargs): + def __init__(self, hass, entity_id, name, **kwargs): """Initialize a TemperatureSensor accessory object.""" - super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) + super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) @@ -47,7 +47,7 @@ class TemperatureSensor(HomeAccessory): temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature, should_callback=False) _LOGGER.debug('%s: Current temperature set to %d°C', - self._entity_id, temperature) + self.entity_id, temperature) @TYPES.register('HumiditySensor') @@ -58,8 +58,8 @@ class HumiditySensor(HomeAccessory): """Initialize a HumiditySensor accessory object.""" super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) self.char_humidity = serv_humidity \ @@ -75,4 +75,4 @@ class HumiditySensor(HomeAccessory): if humidity: self.char_humidity.set_value(humidity, should_callback=False) _LOGGER.debug('%s: Percent set to %d%%', - self._entity_id, humidity) + self.entity_id, humidity) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 1f19893d0be..689edde6f37 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -7,7 +7,7 @@ from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory, add_preload_service -from .const import SERV_SWITCH, CHAR_ON +from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON _LOGGER = logging.getLogger(__name__) @@ -16,12 +16,12 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, hass, entity_id, display_name, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, **kwargs): """Initialize a Switch accessory object to represent a remote.""" - super().__init__(display_name, entity_id, 'SWITCH', *args, **kwargs) + super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._domain = split_entity_id(entity_id)[0] self.flag_target_state = False @@ -34,12 +34,12 @@ class Switch(HomeAccessory): def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state to %s', - self._entity_id, value) + self.entity_id, value) self.flag_target_state = True self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self._hass.services.call(self._domain, service, - {ATTR_ENTITY_ID: self._entity_id}) + self.hass.services.call(self._domain, service, + {ATTR_ENTITY_ID: self.entity_id}) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update switch state after state changed.""" @@ -49,7 +49,7 @@ class Switch(HomeAccessory): current_state = (new_state.state == STATE_ON) if not self.flag_target_state: _LOGGER.debug('%s: Set current state to %s', - self._entity_id, current_state) + self.entity_id, current_state) self.char_on.set_value(current_state, should_callback=False) self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index d49c1ca626b..69b61062791 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -7,12 +7,12 @@ from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_HEAT, STATE_COOL, STATE_AUTO) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, + CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -20,7 +20,6 @@ from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) -STATE_OFF = 'off' UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1, @@ -32,14 +31,13 @@ HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, hass, entity_id, display_name, - support_auto, *args, **kwargs): + def __init__(self, hass, entity_id, display_name, support_auto, **kwargs): """Initialize a Thermostat accessory object.""" - super().__init__(display_name, entity_id, 'THERMOSTAT', - *args, **kwargs) + super().__init__(display_name, entity_id, + CATEGORY_THERMOSTAT, **kwargs) - self._hass = hass - self._entity_id = entity_id + self.hass = hass + self.entity_id = entity_id self._call_timer = None self._unit = TEMP_CELSIUS @@ -101,48 +99,48 @@ class Thermostat(HomeAccessory): """Move operation mode to value if call came from HomeKit.""" self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: - _LOGGER.debug('%s: Set heat-cool to %d', self._entity_id, value) + _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] - self._hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self._entity_id) + self.hass.components.climate.set_operation_mode( + operation_mode=hass_value, entity_id=self.entity_id) def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', - self._entity_id, value) + self.entity_id, value) self.coolingthresh_flag_target_state = True self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value low = temperature_to_states(low, self._unit) value = temperature_to_states(value, self._unit) - self._hass.components.climate.set_temperature( - entity_id=self._entity_id, target_temp_high=value, + self.hass.components.climate.set_temperature( + entity_id=self.entity_id, target_temp_high=value, target_temp_low=low) def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', - self._entity_id, value) + self.entity_id, value) self.heatingthresh_flag_target_state = True self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value high = temperature_to_states(high, self._unit) value = temperature_to_states(value, self._unit) - self._hass.components.climate.set_temperature( - entity_id=self._entity_id, target_temp_high=high, + self.hass.components.climate.set_temperature( + entity_id=self.entity_id, target_temp_high=high, target_temp_low=value) def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" _LOGGER.debug('%s: Set target temperature to %.2f°C', - self._entity_id, value) + self.entity_id, value) self.temperature_flag_target_state = True self.char_target_temp.set_value(value, should_callback=False) value = temperature_to_states(value, self._unit) - self._hass.components.climate.set_temperature( - temperature=value, entity_id=self._entity_id) + self.hass.components.climate.set_temperature( + temperature=value, entity_id=self.entity_id) def update_state(self, entity_id=None, old_state=None, new_state=None): """Update security state after state changed.""" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 011fe73377d..e1511163f2f 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -6,11 +6,10 @@ from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.type_thermostats import ( - Thermostat, STATE_OFF) +from homeassistant.components.homekit.type_thermostats import Thermostat from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant From fe56844a3aa70ad7c63d19b9562b784cfc667900 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Thu, 5 Apr 2018 11:14:15 +0200 Subject: [PATCH 283/924] Bugfix: Zwave Print_node to logfile instead of console (#13302) * Print to logfile instead of console * Review changes * Typo --- homeassistant/components/zwave/__init__.py | 6 ++---- tests/components/zwave/test_init.py | 16 +++++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a85160e8bde..02d2b574592 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -182,10 +182,8 @@ def nice_print_node(node): node_dict['values'] = {value_id: _obj_to_dict(value) for value_id, value in node.values.items()} - print("\n\n\n") - print("FOUND NODE", node.product_name) - pprint(node_dict) - print("\n\n\n") + _LOGGER.info("FOUND NODE %s \n" + "%s", node.product_name, node_dict) def get_config_value(node, value_index, tries=5): diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index bb073459b48..004e5e95ca0 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -1094,20 +1094,18 @@ class TestZWaveServices(unittest.TestCase): assert mock_logger.info.mock_calls[0][1][3] == 2345 def test_print_node(self): - """Test zwave print_config_parameter service.""" - node1 = MockNode(node_id=14) - node2 = MockNode(node_id=15) - self.zwave_network.nodes = {14: node1, 15: node2} + """Test zwave print_node_parameter service.""" + node = MockNode(node_id=14) - with patch.object(zwave, 'pprint') as mock_pprint: + self.zwave_network.nodes = {14: node} + + with self.assertLogs(level='INFO') as mock_logger: self.hass.services.call('zwave', 'print_node', { - const.ATTR_NODE_ID: 15, + const.ATTR_NODE_ID: 14 }) self.hass.block_till_done() - assert mock_pprint.called - assert len(mock_pprint.mock_calls) == 1 - assert mock_pprint.mock_calls[0][1][0]['node_id'] == 15 + self.assertIn("FOUND NODE ", mock_logger.output[1]) def test_set_wakeup(self): """Test zwave set_wakeup service.""" From 206e38a2aba3eda32a30cdac54976c6c1156678a Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 5 Apr 2018 13:20:20 +0200 Subject: [PATCH 284/924] Update HAP-python to 1.1.8 (#13563) * Bump version to HAP-python==1.1.8 * Required changes for version change * Small bugfix lights --- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/accessories.py | 14 ++------ homeassistant/components/homekit/const.py | 5 --- .../components/homekit/type_lights.py | 1 + .../components/homekit/type_sensors.py | 5 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_accessories.py | 32 ++++--------------- 8 files changed, 14 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 25b54b6c723..948e26be291 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -28,7 +28,7 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.7'] +REQUIREMENTS = ['HAP-python==1.1.8'] CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 3e9f22ef2bc..da45bee9e90 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,8 +8,8 @@ from homeassistant.helpers.event import async_track_state_change from .const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - MANUFACTURER, SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, + CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) @@ -39,15 +39,6 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) -def override_properties(char, properties=None, valid_values=None): - """Override characteristic property values and valid values.""" - if properties: - char.properties.update(properties) - - if valid_values: - char.properties['ValidValues'].update(valid_values) - - class HomeAccessory(Accessory): """Adapter class for Accessory.""" @@ -83,7 +74,6 @@ class HomeBridge(Bridge): def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) - add_preload_service(self, SERV_BRIDGING_STATE) def setup_message(self): """Prevent print of pyhap setup message to terminal.""" diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index c2a10f61fcb..676f83bf8e8 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -34,7 +34,6 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' -SERV_BRIDGING_STATE = 'BridgingState' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, # StatusLowBattery, Name @@ -47,9 +46,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### -CHAR_ACC_IDENTIFIER = 'AccessoryIdentifier' CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] -CHAR_CATEGORY = 'Category' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' @@ -58,13 +55,11 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] -CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_NAME = 'Name' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' -CHAR_REACHABLE = 'Reachable' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index b02aee1e714..45ed9405a2a 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -152,4 +152,5 @@ class Light(HomeAccessory): self.char_hue.set_value(hue, should_callback=False) self.char_saturation.set_value(saturation, should_callback=False) + self._hue, self._saturation = (hue, saturation) self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 9768c4a51d4..80521df5991 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -5,8 +5,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, override_properties) +from .accessories import HomeAccessory, add_preload_service from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) @@ -32,7 +31,7 @@ class TemperatureSensor(HomeAccessory): serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) - override_properties(self.char_temp, PROP_CELSIUS) + self.char_temp.override_properties(properties=PROP_CELSIUS) self.char_temp.value = 0 self.unit = None diff --git a/requirements_all.txt b/requirements_all.txt index 1cfe6df643f..49b23015794 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ attrs==17.4.0 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.7 +HAP-python==1.1.8 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 120d2c73024..7c5467f7608 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.7 +HAP-python==1.1.8 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 6599ec83335..a2facd826e4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -6,12 +6,12 @@ import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - add_preload_service, set_accessory_info, override_properties, + add_preload_service, set_accessory_info, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - SERV_ACCESSORY_INFO, SERV_BRIDGING_STATE, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, + CHAR_NAME, CHAR_SERIAL_NUMBER) class TestAccessories(unittest.TestCase): @@ -22,7 +22,7 @@ class TestAccessories(unittest.TestCase): acc = Mock() serv = add_preload_service(acc, 'AirPurifier') self.assertEqual(acc.mock_calls, [call.add_service(serv)]) - with self.assertRaises(AssertionError): + with self.assertRaises(ValueError): serv.get_characteristic('Name') # Test with typo in service name @@ -68,24 +68,6 @@ class TestAccessories(unittest.TestCase): self.assertEqual( serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - def test_override_properties(self): - """Test overriding property values.""" - serv = add_preload_service(Mock(), 'AirPurifier', 'RotationSpeed') - - char_active = serv.get_characteristic('Active') - char_rotation_speed = serv.get_characteristic('RotationSpeed') - - self.assertTrue( - char_active.properties['ValidValues'].get('State') is None) - self.assertEqual(char_rotation_speed.properties['maxValue'], 100) - - override_properties(char_active, valid_values={'State': 'On'}) - override_properties(char_rotation_speed, properties={'maxValue': 200}) - - self.assertFalse( - char_active.properties['ValidValues'].get('State') is None) - self.assertEqual(char_rotation_speed.properties['maxValue'], 200) - def test_home_accessory(self): """Test HomeAccessory class.""" acc = HomeAccessory() @@ -110,17 +92,15 @@ class TestAccessories(unittest.TestCase): bridge = HomeBridge(None) self.assertEqual(bridge.display_name, BRIDGE_NAME) self.assertEqual(bridge.category, 2) # Category.BRIDGE - self.assertEqual(len(bridge.services), 2) + self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) - serv = bridge.services[1] # SERV_BRIDGING_STATE - self.assertEqual(serv.display_name, SERV_BRIDGING_STATE) bridge = HomeBridge('hass', 'test_name', 'test_model') self.assertEqual(bridge.display_name, 'test_name') - self.assertEqual(len(bridge.services), 2) + self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, 'test_model') From b2d37f525703f21ff929f469bd66aec66ca40405 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 5 Apr 2018 17:54:17 +0200 Subject: [PATCH 285/924] Update ha-philips_js to 0.0.3 (#13702) * Update requirements_all.txt * Update philips_js.py --- homeassistant/components/media_player/philips_js.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index 29d336e4d7a..d526fbb0387 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.2'] +REQUIREMENTS = ['ha-philipsjs==0.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 49b23015794..1b581cd2040 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.2 +ha-philipsjs==0.0.3 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 From 1b3c3494e8e7e4d11024b549979632839cccde6e Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Thu, 5 Apr 2018 17:58:55 +0200 Subject: [PATCH 286/924] Coverage & Codeowners (#13700) --- .coveragerc | 17 ++++++----------- CODEOWNERS | 15 ++++++++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.coveragerc b/.coveragerc index 828da909a06..79b1cffa6fe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -159,7 +159,7 @@ omit = homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py - + homeassistant/components/mochad.py homeassistant/components/*/mochad.py @@ -286,11 +286,9 @@ omit = homeassistant/components/*/wink.py homeassistant/components/xiaomi_aqara.py - homeassistant/components/binary_sensor/xiaomi_aqara.py - homeassistant/components/cover/xiaomi_aqara.py - homeassistant/components/light/xiaomi_aqara.py - homeassistant/components/sensor/xiaomi_aqara.py - homeassistant/components/switch/xiaomi_aqara.py + homeassistant/components/*/xiaomi_aqara.py + + homeassistant/components/*/xiaomi_miio.py homeassistant/components/zabbix.py homeassistant/components/*/zabbix.py @@ -398,7 +396,6 @@ omit = homeassistant/components/emoncms_history.py homeassistant/components/emulated_hue/upnp.py homeassistant/components/fan/mqtt.py - homeassistant/components/fan/xiaomi_miio.py homeassistant/components/feedreader.py homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py @@ -431,7 +428,6 @@ omit = homeassistant/components/light/tplink.py homeassistant/components/light/tradfri.py homeassistant/components/light/x10.py - homeassistant/components/light/xiaomi_miio.py homeassistant/components/light/yeelight.py homeassistant/components/light/yeelightsunflower.py homeassistant/components/light/zengge.py @@ -440,6 +436,7 @@ omit = homeassistant/components/lock/nello.py homeassistant/components/lock/nuki.py homeassistant/components/lock/sesame.py + homeassistant/components/map.py homeassistant/components/media_extractor.py homeassistant/components/media_player/anthemav.py homeassistant/components/media_player/aquostv.py @@ -538,7 +535,6 @@ omit = homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote/harmony.py homeassistant/components/remote/itach.py - homeassistant/components/remote/xiaomi_miio.py homeassistant/components/scene/hunterdouglas_powerview.py homeassistant/components/scene/lifx_cloud.py homeassistant/components/sensor/airvisual.py @@ -674,6 +670,7 @@ omit = homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/viaggiatreno.py homeassistant/components/sensor/waqi.py + homeassistant/components/sensor/waze_travel_time.py homeassistant/components/sensor/whois.py homeassistant/components/sensor/worldtidesinfo.py homeassistant/components/sensor/worxlandroid.py @@ -707,7 +704,6 @@ omit = homeassistant/components/switch/tplink.py homeassistant/components/switch/transmission.py homeassistant/components/switch/vesync.py - homeassistant/components/switch/xiaomi_miio.py homeassistant/components/telegram_bot/* homeassistant/components/thingspeak.py homeassistant/components/tts/amazon_polly.py @@ -716,7 +712,6 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/vacuum/mqtt.py homeassistant/components/vacuum/roomba.py - homeassistant/components/vacuum/xiaomi_miio.py homeassistant/components/weather/bom.py homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py diff --git a/CODEOWNERS b/CODEOWNERS index 932f07573b2..67aef6a248f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -68,8 +68,9 @@ homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/pollen.py @bachya homeassistant/components/sensor/qnap.py @colinodell -homeassistant/components/sensor/sytadin.py @gautric +homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes +homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/rainmachine.py @bachya @@ -79,17 +80,17 @@ homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 homeassistant/components/*/bmw_connected_drive.py @ChristianKuehnel homeassistant/components/*/broadlink.py @danielhiversen +homeassistant/components/*/deconz.py @kane610 homeassistant/components/eight_sleep.py @mezz64 homeassistant/components/*/eight_sleep.py @mezz64 homeassistant/components/hive.py @Rendili @KJonline homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p -homeassistant/components/*/deconz.py @kane610 -homeassistant/components/*/rfxtrx.py @danielhiversen -homeassistant/components/velux.py @Julius2342 -homeassistant/components/*/velux.py @Julius2342 homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/qwikswitch.py @kellerza +homeassistant/components/*/qwikswitch.py @kellerza +homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei homeassistant/components/tesla.py @zabuldon @@ -97,5 +98,9 @@ homeassistant/components/*/tesla.py @zabuldon homeassistant/components/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tellduslive.py @molobrakos @fredrike homeassistant/components/*/tradfri.py @ggravlingen +homeassistant/components/velux.py @Julius2342 +homeassistant/components/*/velux.py @Julius2342 homeassistant/components/*/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/xiaomi_miio.py @rytilahti @syssi + +homeassistant/scripts/check_config.py @kellerza From 61a3b4ffdb7f7e40e000d4ca51b3db58811b260d Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 5 Apr 2018 11:59:32 -0400 Subject: [PATCH 287/924] Bump insteonplm to 0.8.6 to fix sensor message handling (#13691) --- homeassistant/components/insteon_plm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm.py index 6f5c5223ea0..d867f0c3d28 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.3'] +REQUIREMENTS = ['insteonplm==0.8.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1b581cd2040..9a0613211da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.3 +insteonplm==0.8.6 # homeassistant.components.verisure jsonpath==0.75 From 63820a78d95f364581f7d7f175a000eeb2030e29 Mon Sep 17 00:00:00 2001 From: shuaiger Date: Fri, 6 Apr 2018 00:00:40 +0800 Subject: [PATCH 288/924] Fix asuswrt ap mode failure (#13693) * fix asuswrt ap mode failure When using ap mode, asuswrt device_tracker does dont work properly as ip can not be retrieved from wl command. This version fixed the issue. * save 1 line code * another 2 lines saved * typo correction --- homeassistant/components/device_tracker/asuswrt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 14aea561c8e..5b8e173aba4 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -172,7 +172,7 @@ class AsusWrtDeviceScanner(DeviceScanner): ret_devices = {} for key in devices: - if devices[key].ip is not None: + if self.mode == 'ap' or devices[key].ip is not None: ret_devices[key] = devices[key] return ret_devices From bb5484edacba9f3c8818e682f54e7dc2b470abc6 Mon Sep 17 00:00:00 2001 From: Niklas Morberg Date: Thu, 5 Apr 2018 18:06:23 +0200 Subject: [PATCH 289/924] Support color temperature in Homekit (#13658) * Add support for color temperature * Add test for color temp --- homeassistant/components/homekit/const.py | 1 + .../components/homekit/type_lights.py | 39 +++++++++++++++++-- tests/components/homekit/test_type_lights.py | 26 ++++++++++++- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 676f83bf8e8..d1c3d84b517 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -47,6 +47,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 45ed9405a2a..018d3cd2e74 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -2,13 +2,14 @@ import logging from homeassistant.components.light import ( - ATTR_HS_COLOR, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, SUPPORT_COLOR) + ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, + ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( - CATEGORY_LIGHT, SERV_LIGHTBULB, + CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) _LOGGER = logging.getLogger(__name__) @@ -20,7 +21,7 @@ RGB_COLOR = 'rgb_color' class Light(HomeAccessory): """Generate a Light accessory for a light entity. - Currently supports: state, brightness, rgb_color. + Currently supports: state, brightness, color temperature, rgb_color. """ def __init__(self, hass, entity_id, name, **kwargs): @@ -31,7 +32,7 @@ class Light(HomeAccessory): self.entity_id = entity_id self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, - RGB_COLOR: False} + CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} self._state = 0 self.chars = [] @@ -39,6 +40,8 @@ class Light(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) + if self._features & SUPPORT_COLOR_TEMP: + self.chars.append(CHAR_COLOR_TEMPERATURE) if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) @@ -55,6 +58,18 @@ class Light(HomeAccessory): .get_characteristic(CHAR_BRIGHTNESS) self.char_brightness.setter_callback = self.set_brightness self.char_brightness.value = 0 + if CHAR_COLOR_TEMPERATURE in self.chars: + self.char_color_temperature = serv_light \ + .get_characteristic(CHAR_COLOR_TEMPERATURE) + self.char_color_temperature.setter_callback = \ + self.set_color_temperature + min_mireds = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_MIREDS, 153) + max_mireds = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_MIREDS, 500) + self.char_color_temperature.override_properties({ + 'minValue': min_mireds, 'maxValue': max_mireds}) + self.char_color_temperature.value = min_mireds if CHAR_HUE in self.chars: self.char_hue = serv_light.get_characteristic(CHAR_HUE) self.char_hue.setter_callback = self.set_hue @@ -90,6 +105,13 @@ class Light(HomeAccessory): else: self.hass.components.light.turn_off(self.entity_id) + def set_color_temperature(self, value): + """Set color temperature if call came from HomeKit.""" + _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) + self._flag[CHAR_COLOR_TEMPERATURE] = True + self.char_color_temperature.set_value(value, should_callback=False) + self.hass.components.light.turn_on(self.entity_id, color_temp=value) + def set_saturation(self, value): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) @@ -141,6 +163,15 @@ class Light(HomeAccessory): should_callback=False) self._flag[CHAR_BRIGHTNESS] = False + # Handle color temperature + if CHAR_COLOR_TEMPERATURE in self.chars: + color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) + if not self._flag[CHAR_COLOR_TEMPERATURE] \ + and isinstance(color_temperature, int): + self.char_color_temperature.set_value(color_temperature, + should_callback=False) + self._flag[CHAR_COLOR_TEMPERATURE] = False + # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: hue, saturation = new_state.attributes.get( diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index ee1900fd7c5..1cfb926c4ce 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,8 +4,8 @@ import unittest from homeassistant.core import callback from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR) + DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, @@ -118,6 +118,28 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) + def test_light_color_temperature(self): + """Test light with color temperature.""" + entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, + ATTR_COLOR_TEMP: 190}) + acc = Light(self.hass, entity_id, 'Light', aid=2) + self.assertEqual(acc.char_color_temperature.value, 153) + + acc.run() + self.hass.block_till_done() + self.assertEqual(acc.char_color_temperature.value, 190) + + # Set from HomeKit + acc.char_color_temperature.set_value(250) + self.hass.block_till_done() + self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) + self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA], { + ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 250}) + def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' From 0081764ddc05f6146de45db091bd5202c3946499 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 5 Apr 2018 17:07:42 +0100 Subject: [PATCH 290/924] Remove unused CONF_WATCHERS (#13678) `CONF_WATCHERS` was from an earlier version, now unused --- homeassistant/components/folder_watcher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py index 011ae892bc5..44110647632 100644 --- a/homeassistant/components/folder_watcher.py +++ b/homeassistant/components/folder_watcher.py @@ -16,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) CONF_FOLDER = 'folder' CONF_PATTERNS = 'patterns' -CONF_WATCHERS = 'watchers' DEFAULT_PATTERN = '*' DOMAIN = "folder_watcher" From edcb242b6d9f90c711b42922121592c5885960eb Mon Sep 17 00:00:00 2001 From: tadly Date: Thu, 5 Apr 2018 18:44:38 +0200 Subject: [PATCH 291/924] Add media type separation for video/movie (#13612) * added media type separation for video/movie * updated all media_player components to reflect new media type --- homeassistant/components/media_player/__init__.py | 3 ++- homeassistant/components/media_player/cast.py | 4 ++-- homeassistant/components/media_player/channels.py | 4 ++-- homeassistant/components/media_player/demo.py | 4 ++-- homeassistant/components/media_player/directv.py | 4 ++-- homeassistant/components/media_player/emby.py | 4 ++-- homeassistant/components/media_player/kodi.py | 6 +++--- homeassistant/components/media_player/plex.py | 6 +++--- homeassistant/components/media_player/roku.py | 4 ++-- 9 files changed, 20 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 37536bf5586..615c758cd1a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -83,7 +83,8 @@ ATTR_MEDIA_SHUFFLE = 'shuffle' MEDIA_TYPE_MUSIC = 'music' MEDIA_TYPE_TVSHOW = 'tvshow' -MEDIA_TYPE_VIDEO = 'movie' +MEDIA_TYPE_MOVIE = 'movie' +MEDIA_TYPE_VIDEO = 'video' MEDIA_TYPE_EPISODE = 'episode' MEDIA_TYPE_CHANNEL = 'channel' MEDIA_TYPE_PLAYLIST = 'playlist' diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 91b8d362c43..2edda0645b0 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -18,7 +18,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import (dispatcher_send, async_dispatcher_connect) from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) @@ -517,7 +517,7 @@ class CastDevice(MediaPlayerDevice): elif self.media_status.media_is_tvshow: return MEDIA_TYPE_TVSHOW elif self.media_status.media_is_movie: - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE elif self.media_status.media_is_musictrack: return MEDIA_TYPE_MUSIC return None diff --git a/homeassistant/components/media_player/channels.py b/homeassistant/components/media_player/channels.py index 480e5152c8e..6b41ace6ce2 100644 --- a/homeassistant/components/media_player/channels.py +++ b/homeassistant/components/media_player/channels.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components.media_player import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_EPISODE, - MEDIA_TYPE_VIDEO, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, + MEDIA_TYPE_MOVIE, SUPPORT_PLAY, SUPPORT_PAUSE, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, DOMAIN, PLATFORM_SCHEMA, MediaPlayerDevice) @@ -281,7 +281,7 @@ class ChannelsPlayer(MediaPlayerDevice): if media_type == MEDIA_TYPE_CHANNEL: response = self.client.play_channel(media_id) self.update_state(response) - elif media_type in [MEDIA_TYPE_VIDEO, MEDIA_TYPE_EPISODE, + elif media_type in [MEDIA_TYPE_MOVIE, MEDIA_TYPE_EPISODE, MEDIA_TYPE_TVSHOW]: response = self.client.play_recording(media_id) self.update_state(response) diff --git a/homeassistant/components/media_player/demo.py b/homeassistant/components/media_player/demo.py index 2be7ad431cf..22fe1d005f7 100644 --- a/homeassistant/components/media_player/demo.py +++ b/homeassistant/components/media_player/demo.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_CLEAR_PLAYLIST, SUPPORT_PLAY, @@ -147,7 +147,7 @@ class DemoYoutubePlayer(AbstractDemoPlayer): @property def media_content_type(self): """Return the content type of current playing media.""" - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE @property def media_duration(self): diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index fae18f03cde..25d13e3017a 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -8,7 +8,7 @@ import voluptuous as vol import requests from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_PLAY, MediaPlayerDevice) @@ -154,7 +154,7 @@ class DirecTvDevice(MediaPlayerDevice): """Return the content type of current playing media.""" if 'episodeTitle' in self._current: return MEDIA_TYPE_TVSHOW - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE @property def media_channel(self): diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 7b5658c56d9..4f9a4019268 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, + MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA) from homeassistant.const import ( @@ -231,7 +231,7 @@ class EmbyDevice(MediaPlayerDevice): if media_type == 'Episode': return MEDIA_TYPE_TVSHOW elif media_type == 'Movie': - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE elif media_type == 'Trailer': return MEDIA_TYPE_TRAILER elif media_type == 'Music': diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 33116258978..9f2a653b8ee 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -19,8 +19,8 @@ from homeassistant.components.media_player import ( SUPPORT_PLAY_MEDIA, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_PLAY, SUPPORT_VOLUME_STEP, SUPPORT_SHUFFLE_SET, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_PLAYLIST, - MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) + MEDIA_TYPE_MOVIE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_CHANNEL, + MEDIA_TYPE_PLAYLIST, MEDIA_PLAYER_SCHEMA, DOMAIN, SUPPORT_TURN_ON) from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, CONF_HOST, CONF_NAME, CONF_PORT, CONF_PROXY_SSL, CONF_USERNAME, CONF_PASSWORD, @@ -67,7 +67,7 @@ MEDIA_TYPES = { 'video': MEDIA_TYPE_VIDEO, 'set': MEDIA_TYPE_PLAYLIST, 'musicvideo': MEDIA_TYPE_VIDEO, - 'movie': MEDIA_TYPE_VIDEO, + 'movie': MEDIA_TYPE_MOVIE, 'tvshow': MEDIA_TYPE_TVSHOW, 'season': MEDIA_TYPE_TVSHOW, 'episode': MEDIA_TYPE_TVSHOW, diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index edb8aa147fb..6690382846f 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.media_player import ( - MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, PLATFORM_SCHEMA, + MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_MOVIE, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, MediaPlayerDevice) @@ -480,7 +480,7 @@ class PlexClient(MediaPlayerDevice): self._media_episode = str(self._session.index).zfill(2) elif self._session_type == 'movie': - self._media_content_type = MEDIA_TYPE_VIDEO + self._media_content_type = MEDIA_TYPE_MOVIE if self._session.year is not None and \ self._media_title is not None: self._media_title += ' (' + str(self._session.year) + ')' @@ -576,7 +576,7 @@ class PlexClient(MediaPlayerDevice): elif self._session_type == 'episode': return MEDIA_TYPE_TVSHOW elif self._session_type == 'movie': - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE elif self._session_type == 'track': return MEDIA_TYPE_MUSIC diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 15b16eec11b..87129f30db5 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -9,7 +9,7 @@ import logging import voluptuous as vol from homeassistant.components.media_player import ( - MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, + MEDIA_TYPE_MOVIE, SUPPORT_NEXT_TRACK, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import ( @@ -155,7 +155,7 @@ class RokuDevice(MediaPlayerDevice): return None elif self.current_app.name == "Roku": return None - return MEDIA_TYPE_VIDEO + return MEDIA_TYPE_MOVIE @property def media_image_url(self): From 4008bf5611406325ecea7765292d4847a54fd634 Mon Sep 17 00:00:00 2001 From: PlanetJ Date: Thu, 5 Apr 2018 12:45:09 -0400 Subject: [PATCH 292/924] Adding configration to disable ip address as a requirement Fixes: #13399 (#13692) * Adding configration to disable ip address as a requirement Fixes: #13399 * Remove whitespace --- .../components/device_tracker/asuswrt.py | 5 +++- .../components/device_tracker/test_asuswrt.py | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/asuswrt.py b/homeassistant/components/device_tracker/asuswrt.py index 5b8e173aba4..7e9b10e9241 100644 --- a/homeassistant/components/device_tracker/asuswrt.py +++ b/homeassistant/components/device_tracker/asuswrt.py @@ -25,6 +25,7 @@ _LOGGER = logging.getLogger(__name__) CONF_PUB_KEY = 'pub_key' CONF_SSH_KEY = 'ssh_key' +CONF_REQUIRE_IP = 'require_ip' DEFAULT_SSH_PORT = 22 SECRET_GROUP = 'Password or SSH Key' @@ -36,6 +37,7 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_PROTOCOL, default='ssh'): vol.In(['ssh', 'telnet']), vol.Optional(CONF_MODE, default='router'): vol.In(['router', 'ap']), vol.Optional(CONF_PORT, default=DEFAULT_SSH_PORT): cv.port, + vol.Optional(CONF_REQUIRE_IP, default=True): cv.boolean, vol.Exclusive(CONF_PASSWORD, SECRET_GROUP): cv.string, vol.Exclusive(CONF_SSH_KEY, SECRET_GROUP): cv.isfile, vol.Exclusive(CONF_PUB_KEY, SECRET_GROUP): cv.isfile @@ -115,6 +117,7 @@ class AsusWrtDeviceScanner(DeviceScanner): self.protocol = config[CONF_PROTOCOL] self.mode = config[CONF_MODE] self.port = config[CONF_PORT] + self.require_ip = config[CONF_REQUIRE_IP] if self.protocol == 'ssh': self.connection = SshConnection( @@ -172,7 +175,7 @@ class AsusWrtDeviceScanner(DeviceScanner): ret_devices = {} for key in devices: - if self.mode == 'ap' or devices[key].ip is not None: + if not self.require_ip or devices[key].ip is not None: ret_devices[key] = devices[key] return ret_devices diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index 27f28412561..d2ae8965668 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -15,7 +15,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker.asuswrt import ( CONF_PROTOCOL, CONF_MODE, CONF_PUB_KEY, DOMAIN, _ARP_REGEX, CONF_PORT, PLATFORM_SCHEMA, Device, get_scanner, AsusWrtDeviceScanner, - _parse_lines, SshConnection, TelnetConnection) + _parse_lines, SshConnection, TelnetConnection, CONF_REQUIRE_IP) from homeassistant.const import (CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST) @@ -105,6 +105,15 @@ WAKE_DEVICES_AP = { mac='08:09:10:11:12:14', ip='123.123.123.126', name=None) } +WAKE_DEVICES_NO_IP = { + '01:02:03:04:06:08': Device( + mac='01:02:03:04:06:08', ip='123.123.123.125', name=None), + '08:09:10:11:12:14': Device( + mac='08:09:10:11:12:14', ip='123.123.123.126', name=None), + '08:09:10:11:12:15': Device( + mac='08:09:10:11:12:15', ip=None, name=None) +} + def setup_module(): """Setup the test module.""" @@ -411,6 +420,21 @@ class TestComponentsDeviceTrackerASUSWRT(unittest.TestCase): scanner._get_leases.return_value = LEASES_DEVICES self.assertEqual(WAKE_DEVICES_AP, scanner.get_asuswrt_data()) + def test_get_asuswrt_data_no_ip(self): + """Test for get asuswrt_data and not requiring ip.""" + conf = VALID_CONFIG_ROUTER_SSH.copy()[DOMAIN] + conf[CONF_REQUIRE_IP] = False + scanner = AsusWrtDeviceScanner(conf) + scanner._get_wl = mock.Mock() + scanner._get_arp = mock.Mock() + scanner._get_neigh = mock.Mock() + scanner._get_leases = mock.Mock() + scanner._get_wl.return_value = WL_DEVICES + scanner._get_arp.return_value = ARP_DEVICES + scanner._get_neigh.return_value = NEIGH_DEVICES + scanner._get_leases.return_value = LEASES_DEVICES + self.assertEqual(WAKE_DEVICES_NO_IP, scanner.get_asuswrt_data()) + def test_update_info(self): """Test for update info.""" scanner = get_scanner(self.hass, VALID_CONFIG_ROUTER_SSH) From e6006e9bebbcd438ca6deb17bf52e2f332ecdfe3 Mon Sep 17 00:00:00 2001 From: ikucuze <37959812+ikucuze@users.noreply.github.com> Date: Thu, 5 Apr 2018 18:56:09 +0200 Subject: [PATCH 293/924] Tahoma switches (#13636) * Added the ability to switch Tahoma Garage door relay. Those are special switches that can only be pushed. Their state is always OFF, they react to the turn_on action, perform it, but stay OFF * fixing indents and so on * CI fix --- homeassistant/components/switch/tahoma.py | 51 +++++++++++++++++++++++ homeassistant/components/tahoma.py | 3 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switch/tahoma.py diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py new file mode 100644 index 00000000000..339a0c39386 --- /dev/null +++ b/homeassistant/components/switch/tahoma.py @@ -0,0 +1,51 @@ +""" +Support for Tahoma Switch - those are push buttons for garage door etc. + +Those buttons are implemented as switchs that are never on. They only +receive the turn_on action, perform the relay click, and stay in OFF state + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.tahoma/ +""" +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.tahoma import ( + DOMAIN as TAHOMA_DOMAIN, TahomaDevice) + +DEPENDENCIES = ['tahoma'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Tahoma switchs.""" + controller = hass.data[TAHOMA_DOMAIN]['controller'] + devices = [] + for switch in hass.data[TAHOMA_DOMAIN]['devices']['switch']: + devices.append(TahomaSwitch(switch, controller)) + add_devices(devices, True) + + +class TahomaSwitch(TahomaDevice, SwitchDevice): + """Representation a Tahoma Switch.""" + + @property + def device_class(self): + """Return the class of the device.""" + if self.tahoma_device.type == 'rts:GarageDoor4TRTSComponent': + return 'garage' + return None + + def turn_on(self, **kwargs): + """Send the on command.""" + self.toggle() + + def toggle(self, **kwargs): + """Click the switch.""" + self.apply_action('cycle') + + @property + def is_on(self): + """Get whether the switch is in on state.""" + return False diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 7c8d047fbcf..055e3f410ea 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) TAHOMA_COMPONENTS = [ - 'scene', 'sensor', 'cover' + 'scene', 'sensor', 'cover', 'switch' ] TAHOMA_TYPES = { @@ -43,6 +43,7 @@ TAHOMA_TYPES = { 'io:RollerShutterGenericIOComponent': 'cover', 'io:WindowOpenerVeluxIOComponent': 'cover', 'io:LightIOSystemSensor': 'sensor', + 'rts:GarageDoor4TRTSComponent': 'switch', } From 1a9727c75ac24e750105de3117e39374b0cf0319 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Thu, 5 Apr 2018 20:17:18 -0400 Subject: [PATCH 294/924] Send XY color for non-osram hue bulbs (#13665) * Send XY color for Philips hue bulbs * Review fixes * Comment tweaks --- homeassistant/components/light/hue.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 4a54f0a337d..1701b886b68 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -300,8 +300,14 @@ class HueLight(Light): command['transitiontime'] = int(kwargs[ATTR_TRANSITION] * 10) if ATTR_HS_COLOR in kwargs: - command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + if self.is_osram: + command['hue'] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + command['sat'] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + else: + # Philips hue bulb models respond differently to hue/sat + # requests, so we convert to XY first to ensure a consistent + # color. + command['xy'] = color.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) elif ATTR_COLOR_TEMP in kwargs: temp = kwargs[ATTR_COLOR_TEMP] command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) From b70b23ef8350a8c00b5f19b0b3e544f0fb374db9 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Thu, 5 Apr 2018 21:10:07 -0700 Subject: [PATCH 295/924] Update AbodePy version to 0.12.3 (#13709) --- homeassistant/components/abode.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index fde21a265b0..08918c77f01 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['abodepy==0.12.2'] +REQUIREMENTS = ['abodepy==0.12.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9a0613211da..7cd8f60fbd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -64,7 +64,7 @@ WazeRouteCalculator==0.5 YesssSMS==0.1.1b3 # homeassistant.components.abode -abodepy==0.12.2 +abodepy==0.12.3 # homeassistant.components.media_player.frontier_silicon afsapi==0.0.3 From 703eea0c93b6017d736c4254080f51624a0e93f5 Mon Sep 17 00:00:00 2001 From: jmtatsch Date: Fri, 6 Apr 2018 06:11:38 +0200 Subject: [PATCH 296/924] Enable autodiscovery for mqtt cameras (#13697) * Enable autodiscovery for mqtt cameras, BREAKING CHANGE: homogenisation topic->state_topic * fix line too long * fix topic->state_topic in test * image shall not be the state of entity --- homeassistant/components/camera/mqtt.py | 9 ++++++--- homeassistant/components/mqtt/discovery.py | 4 +++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/mqtt.py b/homeassistant/components/camera/mqtt.py index b7a7510e0eb..b2a27230a02 100644 --- a/homeassistant/components/camera/mqtt.py +++ b/homeassistant/components/camera/mqtt.py @@ -19,7 +19,6 @@ from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_TOPIC = 'topic' - DEFAULT_NAME = 'MQTT Camera' DEPENDENCIES = ['mqtt'] @@ -33,9 +32,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the MQTT Camera.""" - topic = config[CONF_TOPIC] + if discovery_info is not None: + config = PLATFORM_SCHEMA(discovery_info) - async_add_devices([MqttCamera(config[CONF_NAME], topic)]) + async_add_devices([MqttCamera( + config.get(CONF_NAME), + config.get(CONF_TOPIC) + )]) class MqttCamera(Camera): diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3263521f3f1..d5a3b4a2efb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -20,10 +20,12 @@ TOPIC_MATCHER = re.compile( r'(?:(?P[a-zA-Z0-9_-]+)/)?(?P[a-zA-Z0-9_-]+)/config') SUPPORTED_COMPONENTS = [ - 'binary_sensor', 'cover', 'fan', 'light', 'sensor', 'switch', 'lock'] + 'binary_sensor', 'camera', 'cover', 'fan', + 'light', 'sensor', 'switch', 'lock'] ALLOWED_PLATFORMS = { 'binary_sensor': ['mqtt'], + 'camera': ['mqtt'], 'cover': ['mqtt'], 'fan': ['mqtt'], 'light': ['mqtt', 'mqtt_json', 'mqtt_template'], From 1d7ecc22f91e2ad037ff350084760ff282f93e9e Mon Sep 17 00:00:00 2001 From: Timmo <28114703+timmo001@users.noreply.github.com> Date: Fri, 6 Apr 2018 09:59:09 +0100 Subject: [PATCH 297/924] Added ENTITY_ID_FORMAT import and set entity_id in __init__ (#13642) --- homeassistant/components/switch/broadlink.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 3828758fe6e..3e620a6a25b 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -14,7 +14,7 @@ import socket import voluptuous as vol from homeassistant.components.switch import ( - DOMAIN, PLATFORM_SCHEMA, SwitchDevice) + DOMAIN, PLATFORM_SCHEMA, SwitchDevice, ENTITY_ID_FORMAT) from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE) @@ -150,6 +150,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for object_id, device_config in devices.items(): switches.append( BroadlinkRMSwitch( + object_id, device_config.get(CONF_FRIENDLY_NAME, object_id), broadlink_device, device_config.get(CONF_COMMAND_ON), @@ -184,8 +185,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class BroadlinkRMSwitch(SwitchDevice): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device, command_on, command_off): + def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" + self.entity_id = ENTITY_ID_FORMAT.format(name) self._name = friendly_name self._state = False self._command_on = b64decode(command_on) if command_on else None From bddfe24753524ffb812a8402f9522a781ed997ea Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Fri, 6 Apr 2018 11:21:05 +0200 Subject: [PATCH 298/924] Fix #10175 (#13713) --- homeassistant/components/media_player/liveboxplaytv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/liveboxplaytv.py b/homeassistant/components/media_player/liveboxplaytv.py index 8093f0d3dbe..4fe4da5a942 100644 --- a/homeassistant/components/media_player/liveboxplaytv.py +++ b/homeassistant/components/media_player/liveboxplaytv.py @@ -22,7 +22,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.3'] +REQUIREMENTS = ['liveboxplaytv==2.0.2', 'pyteleloisirs==3.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7cd8f60fbd6..c29b78c8ded 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ pystride==0.1.7 pysyncthru==0.3.1 # homeassistant.components.media_player.liveboxplaytv -pyteleloisirs==3.3 +pyteleloisirs==3.4 # homeassistant.components.sensor.thinkingcleaner # homeassistant.components.switch.thinkingcleaner From 0a25d30ba69d836289dee429b329c925fddc351a Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Fri, 6 Apr 2018 15:34:56 +0200 Subject: [PATCH 299/924] Add support for Nanoleaf Aurora Light Panels (#13456) * Added support for Nanoleaf Aurora Light Panels * aurora light module - fixed lint errors * aurora light module - use SUPPORT_COLOR instead of SUPPORT_RGB_COLOR * nanoleaf aurora light - support_hs_color instead of rgb * review comments from armills implemented * nanoleaf aurora lights - put attributes into constructor (pylint) --- .coveragerc | 1 + homeassistant/components/light/aurora.py | 153 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 157 insertions(+) create mode 100644 homeassistant/components/light/aurora.py diff --git a/.coveragerc b/.coveragerc index 79b1cffa6fe..a84a739151c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -406,6 +406,7 @@ omit = homeassistant/components/image_processing/seven_segments.py homeassistant/components/keyboard_remote.py homeassistant/components/keyboard.py + homeassistant/components/light/aurora.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/aurora.py new file mode 100644 index 00000000000..2a9066bd55f --- /dev/null +++ b/homeassistant/components/light/aurora.py @@ -0,0 +1,153 @@ +""" +Support for Nanoleaf Aurora platform. + +Based in large parts upon Software-2's ha-aurora and fully +reliant on Software-2's nanoleaf-aurora Python Library, see +https://github.com/software-2/ha-aurora as well as +https://github.com/software-2/nanoleaf + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.nanoleaf_aurora/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, + SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, + SUPPORT_COLOR, PLATFORM_SCHEMA, Light) +from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.util import color as color_util +from homeassistant.util.color import \ + color_temperature_mired_to_kelvin as mired_to_kelvin + +REQUIREMENTS = ['nanoleaf==0.4.1'] + +SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | + SUPPORT_COLOR) + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_NAME, default='Aurora'): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup Nanoleaf Aurora device.""" + import nanoleaf + host = config.get(CONF_HOST) + name = config.get(CONF_NAME) + token = config.get(CONF_TOKEN) + aurora_light = nanoleaf.Aurora(host, token) + aurora_light.hass_name = name + + if aurora_light.on is None: + _LOGGER.error("Could not connect to \ + Nanoleaf Aurora: %s on %s", name, host) + add_devices([AuroraLight(aurora_light)], True) + + +class AuroraLight(Light): + """Representation of a Nanoleaf Aurora.""" + + def __init__(self, light): + """Initialize an Aurora.""" + self._brightness = None + self._color_temp = None + self._effect = None + self._effects_list = None + self._light = light + self._name = light.hass_name + self._hs_color = None + self._state = None + + @property + def brightness(self): + """Return the brightness of the light.""" + if self._brightness is not None: + return int(self._brightness * 2.55) + return None + + @property + def color_temp(self): + """Return the current color temperature.""" + if self._color_temp is not None: + return color_util.color_temperature_kelvin_to_mired( + self._color_temp) + return None + + @property + def effect(self): + """Return the current effect.""" + return self._effect + + @property + def effect_list(self): + """Return the list of supported effects.""" + return self._effects_list + + @property + def name(self): + """Return the display name of this light.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:triangle-outline" + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def hs_color(self): + """Return the color in HS.""" + return self._hs_color + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_AURORA + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + self._light.on = True + brightness = kwargs.get(ATTR_BRIGHTNESS) + hs_color = kwargs.get(ATTR_HS_COLOR) + color_temp_mired = kwargs.get(ATTR_COLOR_TEMP) + effect = kwargs.get(ATTR_EFFECT) + + if hs_color: + hue, saturation = hs_color + self._light.hue = int(hue) + self._light.saturation = int(saturation) + + if color_temp_mired: + self._light.color_temperature = mired_to_kelvin(color_temp_mired) + if brightness: + self._light.brightness = int(brightness / 2.55) + if effect: + self._light.effect = effect + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.on = False + + def update(self): + """Fetch new state data for this light. + + This is the only method that should fetch new data for Home Assistant. + """ + self._brightness = self._light.brightness + self._color_temp = self._light.color_temperature + self._effect = self._light.effect + self._effects_list = self._light.effects_list + self._hs_color = self._light.hue, self._light.saturation + self._state = self._light.on diff --git a/requirements_all.txt b/requirements_all.txt index c29b78c8ded..d2c56e74d97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -536,6 +536,9 @@ myusps==1.3.2 # homeassistant.components.media_player.nadtcp nad_receiver==0.0.9 +# homeassistant.components.light.aurora +nanoleaf==0.4.1 + # homeassistant.components.discovery netdisco==1.3.0 From bd51143ac193780b8d08d5058b452c8d53b60c33 Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Fri, 6 Apr 2018 23:53:00 +1000 Subject: [PATCH 300/924] Added gogogate2 cover (#13467) * Added gogogate2 cover * Hound fixes * PR feedback * Hound comments * Bump gogogate2 version * Update requirements all * Add device_class and features * Fix lint issues * Again lint * Fix imports * Fix end of file --- .coveragerc | 1 + homeassistant/components/cover/gogogate2.py | 120 ++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 124 insertions(+) create mode 100644 homeassistant/components/cover/gogogate2.py diff --git a/.coveragerc b/.coveragerc index a84a739151c..e9c69d137e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -352,6 +352,7 @@ omit = homeassistant/components/climate/touchline.py homeassistant/components/climate/venstar.py homeassistant/components/cover/garadget.py + homeassistant/components/cover/gogogate2.py homeassistant/components/cover/homematic.py homeassistant/components/cover/knx.py homeassistant/components/cover/myq.py diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py new file mode 100644 index 00000000000..c2bdc9c5472 --- /dev/null +++ b/homeassistant/components/cover/gogogate2.py @@ -0,0 +1,120 @@ +""" +Support for Gogogate2 Garage Doors. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/cover.gogogate2/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.cover import ( + CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN, + CONF_IP_ADDRESS, CONF_NAME) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pygogogate2==0.0.3'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'gogogate2' + +NOTIFICATION_ID = 'gogogate2_notification' +NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' + +COVER_SCHEMA = vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Gogogate2 component.""" + from pygogogate2 import Gogogate2API as pygogogate2 + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + ip_address = config.get(CONF_IP_ADDRESS) + name = config.get(CONF_NAME) + mygogogate2 = pygogogate2(username, password, ip_address) + + try: + devices = mygogogate2.get_devices() + if devices is False: + raise ValueError( + "Username or Password is incorrect or no devices found") + + add_devices(MyGogogate2Device( + mygogogate2, door, name) for door in devices) + return + + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return + + +class MyGogogate2Device(CoverDevice): + """Representation of a Gogogate2 cover.""" + + def __init__(self, mygogogate2, device, name): + """Initialize with API object, device id.""" + self.mygogogate2 = mygogogate2 + self.device_id = device['door'] + self._name = name or device['name'] + self._status = device['status'] + self.available = None + + @property + def name(self): + """Return the name of the garage door if any.""" + return self._name if self._name else DEFAULT_NAME + + @property + def is_closed(self): + """Return true if cover is closed, else False.""" + return self._status == STATE_CLOSED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'garage' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self.available + + def close_cover(self, **kwargs): + """Issue close command to cover.""" + self.mygogogate2.close_device(self.device_id) + self.schedule_update_ha_state(True) + + def open_cover(self, **kwargs): + """Issue open command to cover.""" + self.mygogogate2.open_device(self.device_id) + self.schedule_update_ha_state(True) + + def update(self): + """Update status of cover.""" + try: + self._status = self.mygogogate2.get_status(self.device_id) + self.available = True + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + self._status = STATE_UNKNOWN + self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index d2c56e74d97..da2373443cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -764,6 +764,9 @@ pyflexit==0.3 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.cover.gogogate2 +pygogogate2==0.0.3 + # homeassistant.components.remote.harmony pyharmony==1.0.20 From 85487612d5d0ca510e09324604c72d4c958487b0 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 6 Apr 2018 16:20:59 +0200 Subject: [PATCH 301/924] Update Homekit to 1.1.9 (#13716) * Version bump to HAP-python==1.1.9 * Updated types and tests --- homeassistant/components/homekit/__init__.py | 2 +- .../components/homekit/type_covers.py | 1 - .../components/homekit/type_lights.py | 18 +++++---------- .../homekit/type_security_systems.py | 7 ++---- .../components/homekit/type_sensors.py | 4 ++-- .../components/homekit/type_switches.py | 3 +-- .../components/homekit/type_thermostats.py | 15 ++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_type_covers.py | 4 ++-- tests/components/homekit/test_type_lights.py | 22 +++++++++---------- .../homekit/test_type_security_systems.py | 10 ++++----- .../components/homekit/test_type_switches.py | 8 +++---- .../homekit/test_type_thermostats.py | 14 ++++++------ 14 files changed, 46 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 948e26be291..8a38c01026e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -28,7 +28,7 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.8'] +REQUIREMENTS = ['HAP-python==1.1.9'] CONFIG_SCHEMA = vol.Schema({ diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3650a948f5d..781f52941fc 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -46,7 +46,6 @@ class WindowCovering(HomeAccessory): def move_cover(self, value): """Move cover to value if call came from HomeKit.""" - self.char_target_position.set_value(value, should_callback=False) if value != self.current_position: _LOGGER.debug('%s: Set position to %d', self.entity_id, value) self.homekit_target = value diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 018d3cd2e74..1110981fe10 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -87,7 +87,6 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - self.char_on.set_value(value, should_callback=False) if value == 1: self.hass.components.light.turn_on(self.entity_id) @@ -98,7 +97,6 @@ class Light(HomeAccessory): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - self.char_brightness.set_value(value, should_callback=False) if value != 0: self.hass.components.light.turn_on( self.entity_id, brightness_pct=value) @@ -109,14 +107,12 @@ class Light(HomeAccessory): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True - self.char_color_temperature.set_value(value, should_callback=False) self.hass.components.light.turn_on(self.entity_id, color_temp=value) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" _LOGGER.debug('%s: Set saturation to %d', self.entity_id, value) self._flag[CHAR_SATURATION] = True - self.char_saturation.set_value(value, should_callback=False) self._saturation = value self.set_color() @@ -124,7 +120,6 @@ class Light(HomeAccessory): """Set hue if call came from HomeKit.""" _LOGGER.debug('%s: Set hue to %d', self.entity_id, value) self._flag[CHAR_HUE] = True - self.char_hue.set_value(value, should_callback=False) self._hue = value self.set_color() @@ -150,7 +145,7 @@ class Light(HomeAccessory): if state in (STATE_ON, STATE_OFF): self._state = 1 if state == STATE_ON else 0 if not self._flag[CHAR_ON] and self.char_on.value != self._state: - self.char_on.set_value(self._state, should_callback=False) + self.char_on.set_value(self._state) self._flag[CHAR_ON] = False # Handle Brightness @@ -159,8 +154,7 @@ class Light(HomeAccessory): if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) if self.char_brightness.value != brightness: - self.char_brightness.set_value(brightness, - should_callback=False) + self.char_brightness.set_value(brightness) self._flag[CHAR_BRIGHTNESS] = False # Handle color temperature @@ -168,8 +162,7 @@ class Light(HomeAccessory): color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) if not self._flag[CHAR_COLOR_TEMPERATURE] \ and isinstance(color_temperature, int): - self.char_color_temperature.set_value(color_temperature, - should_callback=False) + self.char_color_temperature.set_value(color_temperature) self._flag[CHAR_COLOR_TEMPERATURE] = False # Handle Color @@ -180,8 +173,7 @@ class Light(HomeAccessory): hue != self._hue or saturation != self._saturation) and \ isinstance(hue, (int, float)) and \ isinstance(saturation, (int, float)): - self.char_hue.set_value(hue, should_callback=False) - self.char_saturation.set_value(saturation, - should_callback=False) + self.char_hue.set_value(hue) + self.char_saturation.set_value(saturation) self._hue, self._saturation = (hue, saturation) self._flag[RGB_COLOR] = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 2cce6653db3..235a8b22e7c 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -53,7 +53,6 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Set security state to %d', self.entity_id, value) self.flag_target_state = True - self.char_target_state.set_value(value, should_callback=False) hass_value = HOMEKIT_TO_HASS[value] service = STATE_TO_SERVICE[hass_value] @@ -72,13 +71,11 @@ class SecuritySystem(HomeAccessory): return current_security_state = HASS_TO_HOMEKIT[hass_state] - self.char_current_state.set_value(current_security_state, - should_callback=False) + self.char_current_state.set_value(current_security_state) _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) if not self.flag_target_state: - self.char_target_state.set_value(current_security_state, - should_callback=False) + self.char_target_state.set_value(current_security_state) if self.char_target_state.value == self.char_current_state.value: self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 80521df5991..393962eac21 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -44,7 +44,7 @@ class TemperatureSensor(HomeAccessory): temperature = convert_to_float(new_state.state) if temperature: temperature = temperature_to_homekit(temperature, unit) - self.char_temp.set_value(temperature, should_callback=False) + self.char_temp.set_value(temperature) _LOGGER.debug('%s: Current temperature set to %d°C', self.entity_id, temperature) @@ -72,6 +72,6 @@ class HumiditySensor(HomeAccessory): humidity = convert_to_float(new_state.state) if humidity: - self.char_humidity.set_value(humidity, should_callback=False) + self.char_humidity.set_value(humidity) _LOGGER.debug('%s: Percent set to %d%%', self.entity_id, humidity) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 689edde6f37..854cb49d181 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -36,7 +36,6 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True - self.char_on.set_value(value, should_callback=False) service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self.entity_id}) @@ -50,6 +49,6 @@ class Switch(HomeAccessory): if not self.flag_target_state: _LOGGER.debug('%s: Set current state to %s', self.entity_id, current_state) - self.char_on.set_value(current_state, should_callback=False) + self.char_on.set_value(current_state) self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 69b61062791..de8ecbdfe3e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -97,7 +97,6 @@ class Thermostat(HomeAccessory): def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" - self.char_target_heat_cool.set_value(value, should_callback=False) if value in HC_HOMEKIT_TO_HASS: _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True @@ -110,7 +109,6 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', self.entity_id, value) self.coolingthresh_flag_target_state = True - self.char_cooling_thresh_temp.set_value(value, should_callback=False) low = self.char_heating_thresh_temp.value low = temperature_to_states(low, self._unit) value = temperature_to_states(value, self._unit) @@ -123,7 +121,6 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - self.char_heating_thresh_temp.set_value(value, should_callback=False) # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value high = temperature_to_states(high, self._unit) @@ -137,7 +134,6 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - self.char_target_temp.set_value(value, should_callback=False) value = temperature_to_states(value, self._unit) self.hass.components.climate.set_temperature( temperature=value, entity_id=self.entity_id) @@ -161,8 +157,7 @@ class Thermostat(HomeAccessory): if isinstance(target_temp, (int, float)): target_temp = temperature_to_homekit(target_temp, self._unit) if not self.temperature_flag_target_state: - self.char_target_temp.set_value(target_temp, - should_callback=False) + self.char_target_temp.set_value(target_temp) self.temperature_flag_target_state = False # Update cooling threshold temperature if characteristic exists @@ -172,8 +167,7 @@ class Thermostat(HomeAccessory): cooling_thresh = temperature_to_homekit(cooling_thresh, self._unit) if not self.coolingthresh_flag_target_state: - self.char_cooling_thresh_temp.set_value( - cooling_thresh, should_callback=False) + self.char_cooling_thresh_temp.set_value(cooling_thresh) self.coolingthresh_flag_target_state = False # Update heating threshold temperature if characteristic exists @@ -183,8 +177,7 @@ class Thermostat(HomeAccessory): heating_thresh = temperature_to_homekit(heating_thresh, self._unit) if not self.heatingthresh_flag_target_state: - self.char_heating_thresh_temp.set_value( - heating_thresh, should_callback=False) + self.char_heating_thresh_temp.set_value(heating_thresh) self.heatingthresh_flag_target_state = False # Update display units @@ -197,7 +190,7 @@ class Thermostat(HomeAccessory): and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( - HC_HASS_TO_HOMEKIT[operation_mode], should_callback=False) + HC_HASS_TO_HOMEKIT[operation_mode]) self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode diff --git a/requirements_all.txt b/requirements_all.txt index da2373443cb..7af7bdb95ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ attrs==17.4.0 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.8 +HAP-python==1.1.9 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c5467f7608..645b56b9e62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.8 +HAP-python==1.1.9 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 45631a76c98..1fa1ef1728e 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -62,7 +62,7 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_position_state.value, 2) # Set from HomeKit - acc.char_target_position.set_value(25) + acc.char_target_position.client_update_value(25) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_cover_position') @@ -74,7 +74,7 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_position_state.value, 0) # Set from HomeKit - acc.char_target_position.set_value(75) + acc.char_target_position.client_update_value(75) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_cover_position') diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 1cfb926c4ce..1d18235d4a1 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -57,7 +57,7 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_on.value, 0) # Set from HomeKit - acc.char_on.set_value(1) + acc.char_on.client_update_value(1) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -65,7 +65,7 @@ class TestHomekitLights(unittest.TestCase): self.hass.states.set(entity_id, STATE_ON) self.hass.block_till_done() - acc.char_on.set_value(0) + acc.char_on.client_update_value(0) self.hass.block_till_done() self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) @@ -94,8 +94,8 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_brightness.value, 40) # Set from HomeKit - acc.char_brightness.set_value(20) - acc.char_on.set_value(1) + acc.char_brightness.client_update_value(20) + acc.char_on.client_update_value(1) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -103,8 +103,8 @@ class TestHomekitLights(unittest.TestCase): self.events[0].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) - acc.char_on.set_value(1) - acc.char_brightness.set_value(40) + acc.char_on.client_update_value(1) + acc.char_brightness.client_update_value(40) self.hass.block_till_done() self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -112,8 +112,8 @@ class TestHomekitLights(unittest.TestCase): self.events[1].data[ATTR_SERVICE_DATA], { ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) - acc.char_on.set_value(1) - acc.char_brightness.set_value(0) + acc.char_on.client_update_value(1) + acc.char_brightness.client_update_value(0) self.hass.block_till_done() self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) @@ -132,7 +132,7 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_color_temperature.value, 190) # Set from HomeKit - acc.char_color_temperature.set_value(250) + acc.char_color_temperature.client_update_value(250) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) @@ -156,8 +156,8 @@ class TestHomekitLights(unittest.TestCase): self.assertEqual(acc.char_saturation.value, 90) # Set from HomeKit - acc.char_hue.set_value(145) - acc.char_saturation.set_value(75) + acc.char_hue.client_update_value(145) + acc.char_saturation.client_update_value(75) self.hass.block_till_done() self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index c689a73bac2..46f886c4d35 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -71,7 +71,7 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_current_state.value, 3) # Set from HomeKit - acc.char_target_state.set_value(0) + acc.char_target_state.client_update_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') @@ -79,7 +79,7 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 0) - acc.char_target_state.set_value(1) + acc.char_target_state.client_update_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') @@ -87,7 +87,7 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 1) - acc.char_target_state.set_value(2) + acc.char_target_state.client_update_value(2) self.hass.block_till_done() self.assertEqual( self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') @@ -95,7 +95,7 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') self.assertEqual(acc.char_target_state.value, 2) - acc.char_target_state.set_value(3) + acc.char_target_state.client_update_value(3) self.hass.block_till_done() self.assertEqual( self.events[3].data[ATTR_SERVICE], 'alarm_disarm') @@ -112,7 +112,7 @@ class TestHomekitSecuritySystems(unittest.TestCase): acc.run() # Set from HomeKit - acc.char_target_state.set_value(0) + acc.char_target_state.client_update_value(0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 21d7583152e..7f30e457308 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -51,14 +51,14 @@ class TestHomekitSwitches(unittest.TestCase): self.assertEqual(acc.char_on.value, False) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.client_update_value(True) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_DOMAIN], domain) self.assertEqual( self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - acc.char_on.set_value(False) + acc.char_on.client_update_value(False) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_DOMAIN], domain) @@ -76,7 +76,7 @@ class TestHomekitSwitches(unittest.TestCase): self.assertEqual(acc.char_on.value, False) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.client_update_value(True) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_DOMAIN], domain) @@ -95,7 +95,7 @@ class TestHomekitSwitches(unittest.TestCase): self.assertEqual(acc.char_on.value, False) # Set from HomeKit - acc.char_on.set_value(True) + acc.char_on.client_update_value(True) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_DOMAIN], domain) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e1511163f2f..d363e26d712 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -151,7 +151,7 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_display_units.value, 0) # Set from HomeKit - acc.char_target_temp.set_value(19.0) + acc.char_target_temp.client_update_value(19.0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_temperature') @@ -159,7 +159,7 @@ class TestHomekitThermostats(unittest.TestCase): self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0) self.assertEqual(acc.char_target_temp.value, 19.0) - acc.char_target_heat_cool.set_value(1) + acc.char_target_heat_cool.client_update_value(1) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'set_operation_mode') @@ -221,7 +221,7 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_display_units.value, 0) # Set from HomeKit - acc.char_heating_thresh_temp.set_value(20.0) + acc.char_heating_thresh_temp.client_update_value(20.0) self.hass.block_till_done() self.assertEqual( self.events[0].data[ATTR_SERVICE], 'set_temperature') @@ -229,7 +229,7 @@ class TestHomekitThermostats(unittest.TestCase): self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0) self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - acc.char_cooling_thresh_temp.set_value(25.0) + acc.char_cooling_thresh_temp.client_update_value(25.0) self.hass.block_till_done() self.assertEqual( self.events[1].data[ATTR_SERVICE], 'set_temperature') @@ -260,19 +260,19 @@ class TestHomekitThermostats(unittest.TestCase): self.assertEqual(acc.char_display_units.value, 1) # Set from HomeKit - acc.char_cooling_thresh_temp.set_value(23) + acc.char_cooling_thresh_temp.client_update_value(23) self.hass.block_till_done() service_data = self.events[-1].data[ATTR_SERVICE_DATA] self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) - acc.char_heating_thresh_temp.set_value(22) + acc.char_heating_thresh_temp.client_update_value(22) self.hass.block_till_done() service_data = self.events[-1].data[ATTR_SERVICE_DATA] self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) - acc.char_target_temp.set_value(24.0) + acc.char_target_temp.client_update_value(24.0) self.hass.block_till_done() service_data = self.events[-1].data[ATTR_SERVICE_DATA] self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) From 9ae6a3402c44ad8466a9ff9b75a611672615be0e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 Apr 2018 10:26:08 -0400 Subject: [PATCH 302/924] Version bump to 0.67.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d286aa85458..815562b68c5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3394916a68122913c7b066b50d217cfe298914eb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 6 Apr 2018 18:06:47 +0200 Subject: [PATCH 303/924] Update docstrings (#13720) --- homeassistant/components/cover/opengarage.py | 47 ++++++++++--------- homeassistant/components/cover/tahoma.py | 2 +- homeassistant/components/ihc/__init__.py | 29 ++++++------ homeassistant/components/ihc/ihcdevice.py | 10 ++-- homeassistant/components/light/aurora.py | 38 +++++++-------- .../sensor/trafikverket_weatherstation.py | 20 ++++---- 6 files changed, 72 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/cover/opengarage.py b/homeassistant/components/cover/opengarage.py index d68021d7db3..028a7a0c9fc 100644 --- a/homeassistant/components/cover/opengarage.py +++ b/homeassistant/components/cover/opengarage.py @@ -18,30 +18,31 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_DISTANCE_SENSOR = "distance_sensor" -ATTR_DOOR_STATE = "door_state" -ATTR_SIGNAL_STRENGTH = "wifi_signal" +ATTR_DISTANCE_SENSOR = 'distance_sensor' +ATTR_DOOR_STATE = 'door_state' +ATTR_SIGNAL_STRENGTH = 'wifi_signal' -CONF_DEVICEKEY = "device_key" +CONF_DEVICE_ID = 'device_id' +CONF_DEVICE_KEY = 'device_key' DEFAULT_NAME = 'OpenGarage' DEFAULT_PORT = 80 -STATE_CLOSING = "closing" -STATE_OFFLINE = "offline" -STATE_OPENING = "opening" -STATE_STOPPED = "stopped" +STATE_CLOSING = 'closing' +STATE_OFFLINE = 'offline' +STATE_OPENING = 'opening' +STATE_STOPPED = 'stopped' STATES_MAP = { 0: STATE_CLOSED, - 1: STATE_OPEN + 1: STATE_OPEN, } COVER_SCHEMA = vol.Schema({ - vol.Required(CONF_DEVICEKEY): cv.string, + vol.Required(CONF_DEVICE_KEY): cv.string, vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -50,7 +51,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up OpenGarage covers.""" + """Set up the OpenGarage covers.""" covers = [] devices = config.get(CONF_COVERS) @@ -59,8 +60,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): CONF_NAME: device_config.get(CONF_NAME), CONF_HOST: device_config.get(CONF_HOST), CONF_PORT: device_config.get(CONF_PORT), - "device_id": device_config.get(CONF_DEVICE, device_id), - CONF_DEVICEKEY: device_config.get(CONF_DEVICEKEY) + CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id), + CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY) } covers.append(OpenGarageCover(hass, args)) @@ -79,8 +80,8 @@ class OpenGarageCover(CoverDevice): self.hass = hass self._name = args[CONF_NAME] self.device_id = args['device_id'] - self._devicekey = args[CONF_DEVICEKEY] - self._state = STATE_UNKNOWN + self._device_key = args[CONF_DEVICE_KEY] + self._state = None self._state_before_move = None self.dist = None self.signal = None @@ -138,8 +139,8 @@ class OpenGarageCover(CoverDevice): try: status = self._get_status() if self._name is None: - if status["name"] is not None: - self._name = status["name"] + if status['name'] is not None: + self._name = status['name'] state = STATES_MAP.get(status.get('door'), STATE_UNKNOWN) if self._state_before_move is not None: if self._state_before_move != state: @@ -152,7 +153,7 @@ class OpenGarageCover(CoverDevice): self.signal = status.get('rssi') self.dist = status.get('dist') self._available = True - except (requests.exceptions.RequestException) as ex: + except requests.exceptions.RequestException as ex: _LOGGER.error("Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex)) self._state = STATE_OFFLINE @@ -166,15 +167,15 @@ class OpenGarageCover(CoverDevice): def _push_button(self): """Send commands to API.""" url = '{}/cc?dkey={}&click=1'.format( - self.opengarage_url, self._devicekey) + self.opengarage_url, self._device_key) try: response = requests.get(url, timeout=10).json() - if response["result"] == 2: - _LOGGER.error("Unable to control %s: device_key is incorrect.", + if response['result'] == 2: + _LOGGER.error("Unable to control %s: Device key is incorrect", self._name) self._state = self._state_before_move self._state_before_move = None - except (requests.exceptions.RequestException) as ex: + except requests.exceptions.RequestException as ex: _LOGGER.error("Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex)) self._state = self._state_before_move diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 6fb8e92e051..c99076de851 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Tahoma covers.""" + """Set up the Tahoma covers.""" controller = hass.data[TAHOMA_DOMAIN]['controller'] devices = [] for device in hass.data[TAHOMA_DOMAIN]['devices']['cover']: diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 031fa263e5a..0c0100bc9f5 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -1,4 +1,5 @@ -"""IHC component. +""" +Support for IHC devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/ihc/ @@ -6,18 +7,18 @@ https://home-assistant.io/components/ihc/ import logging import os.path import xml.etree.ElementTree + import voluptuous as vol from homeassistant.components.ihc.const import ( - ATTR_IHC_ID, ATTR_VALUE, CONF_INFO, CONF_AUTOSETUP, - CONF_BINARY_SENSOR, CONF_LIGHT, CONF_SENSOR, CONF_SWITCH, - CONF_XPATH, CONF_NODE, CONF_DIMMABLE, CONF_INVERTING, - SERVICE_SET_RUNTIME_VALUE_BOOL, SERVICE_SET_RUNTIME_VALUE_INT, - SERVICE_SET_RUNTIME_VALUE_FLOAT) + ATTR_IHC_ID, ATTR_VALUE, CONF_AUTOSETUP, CONF_BINARY_SENSOR, CONF_DIMMABLE, + CONF_INFO, CONF_INVERTING, CONF_LIGHT, CONF_NODE, CONF_SENSOR, CONF_SWITCH, + CONF_XPATH, SERVICE_SET_RUNTIME_VALUE_BOOL, + SERVICE_SET_RUNTIME_VALUE_FLOAT, SERVICE_SET_RUNTIME_VALUE_INT) from homeassistant.config import load_yaml_config_file from homeassistant.const import ( - CONF_URL, CONF_USERNAME, CONF_PASSWORD, CONF_ID, CONF_NAME, - CONF_UNIT_OF_MEASUREMENT, CONF_TYPE, TEMP_CELSIUS) + CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, + CONF_URL, CONF_USERNAME, TEMP_CELSIUS) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType @@ -36,7 +37,7 @@ CONFIG_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_AUTOSETUP, default=True): cv.boolean, - vol.Optional(CONF_INFO, default=True): cv.boolean + vol.Optional(CONF_INFO, default=True): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -97,7 +98,7 @@ IHC_PLATFORMS = ('binary_sensor', 'light', 'sensor', 'switch') def setup(hass, config): - """Setup the IHC component.""" + """Set up the IHC component.""" from ihcsdk.ihccontroller import IHCController conf = config[DOMAIN] url = conf[CONF_URL] @@ -106,7 +107,7 @@ def setup(hass, config): ihc_controller = IHCController(url, username, password) if not ihc_controller.authenticate(): - _LOGGER.error("Unable to authenticate on ihc controller.") + _LOGGER.error("Unable to authenticate on IHC controller") return False if (conf[CONF_AUTOSETUP] and @@ -125,7 +126,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller): """Auto setup of IHC products from the ihc project file.""" project_xml = ihc_controller.get_project() if not project_xml: - _LOGGER.error("Unable to read project from ihc controller.") + _LOGGER.error("Unable to read project from ICH controller") return False project = xml.etree.ElementTree.fromstring(project_xml) @@ -150,7 +151,7 @@ def autosetup_ihc_products(hass: HomeAssistantType, config, ihc_controller): def get_discovery_info(component_setup, groups): - """Get discovery info for specified component.""" + """Get discovery info for specified IHC component.""" discovery_data = {} for group in groups: groupname = group.attrib['name'] @@ -173,7 +174,7 @@ def get_discovery_info(component_setup, groups): def setup_service_functions(hass: HomeAssistantType, ihc_controller): - """Setup the ihc service functions.""" + """Setup the IHC service functions.""" def set_runtime_value_bool(call): """Set a IHC runtime bool value service function.""" ihc_id = call.data[ATTR_IHC_ID] diff --git a/homeassistant/components/ihc/ihcdevice.py b/homeassistant/components/ihc/ihcdevice.py index 59f4d95f0a1..de6db875def 100644 --- a/homeassistant/components/ihc/ihcdevice.py +++ b/homeassistant/components/ihc/ihcdevice.py @@ -1,4 +1,4 @@ -"""Implements a base class for all IHC devices.""" +"""Implementation of a base class for all IHC devices.""" import asyncio from xml.etree.ElementTree import Element @@ -6,7 +6,7 @@ from homeassistant.helpers.entity import Entity class IHCDevice(Entity): - """Base class for all ihc devices. + """Base class for all IHC devices. All IHC devices have an associated IHC resource. IHCDevice handled the registration of the IHC controller callback when the IHC resource changes. @@ -31,13 +31,13 @@ class IHCDevice(Entity): @asyncio.coroutine def async_added_to_hass(self): - """Add callback for ihc changes.""" + """Add callback for IHC changes.""" self.ihc_controller.add_notify_event( self.ihc_id, self.on_ihc_change, True) @property def should_poll(self) -> bool: - """No polling needed for ihc devices.""" + """No polling needed for IHC devices.""" return False @property @@ -58,7 +58,7 @@ class IHCDevice(Entity): } def on_ihc_change(self, ihc_id, value): - """Callback when ihc resource changes. + """Callback when IHC resource changes. Derived classes must overwrite this to do device specific stuff. """ diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/aurora.py index 2a9066bd55f..99c07166037 100644 --- a/homeassistant/components/light/aurora.py +++ b/homeassistant/components/light/aurora.py @@ -1,11 +1,6 @@ """ Support for Nanoleaf Aurora platform. -Based in large parts upon Software-2's ha-aurora and fully -reliant on Software-2's nanoleaf-aurora Python Library, see -https://github.com/software-2/ha-aurora as well as -https://github.com/software-2/nanoleaf - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.nanoleaf_aurora/ """ @@ -15,9 +10,9 @@ import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - SUPPORT_EFFECT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, - SUPPORT_COLOR, PLATFORM_SCHEMA, Light) -from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_NAME + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, Light) +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN import homeassistant.helpers.config_validation as cv from homeassistant.util import color as color_util from homeassistant.util.color import \ @@ -25,20 +20,24 @@ from homeassistant.util.color import \ REQUIREMENTS = ['nanoleaf==0.4.1'] +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Aurora' + +ICON = 'mdi:triangle-outline' + SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | SUPPORT_COLOR) -_LOGGER = logging.getLogger(__name__) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_NAME, default='Aurora'): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup Nanoleaf Aurora device.""" + """Set up the Nanoleaf Aurora device.""" import nanoleaf host = config.get(CONF_HOST) name = config.get(CONF_NAME) @@ -47,8 +46,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): aurora_light.hass_name = name if aurora_light.on is None: - _LOGGER.error("Could not connect to \ - Nanoleaf Aurora: %s on %s", name, host) + _LOGGER.error( + "Could not connect to Nanoleaf Aurora: %s on %s", name, host) + return + add_devices([AuroraLight(aurora_light)], True) @@ -56,7 +57,7 @@ class AuroraLight(Light): """Representation of a Nanoleaf Aurora.""" def __init__(self, light): - """Initialize an Aurora.""" + """Initialize an Aurora light.""" self._brightness = None self._color_temp = None self._effect = None @@ -99,7 +100,7 @@ class AuroraLight(Light): @property def icon(self): """Return the icon to use in the frontend, if any.""" - return "mdi:triangle-outline" + return ICON @property def is_on(self): @@ -141,10 +142,7 @@ class AuroraLight(Light): self._light.on = False def update(self): - """Fetch new state data for this light. - - This is the only method that should fetch new data for Home Assistant. - """ + """Fetch new state data for this light.""" self._brightness = self._light.brightness self._color_temp = self._light.color_temperature self._effect = self._light.effect diff --git a/homeassistant/components/sensor/trafikverket_weatherstation.py b/homeassistant/components/sensor/trafikverket_weatherstation.py index fba16c27c7e..77a2b0e7338 100644 --- a/homeassistant/components/sensor/trafikverket_weatherstation.py +++ b/homeassistant/components/sensor/trafikverket_weatherstation.py @@ -4,17 +4,17 @@ Weather information for air and road temperature, provided by Trafikverket. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.trafikverket_weatherstation/ """ +from datetime import timedelta import json import logging -from datetime import timedelta import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, ATTR_ATTRIBUTION, TEMP_CELSIUS, CONF_API_KEY, CONF_TYPE) + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -25,6 +25,7 @@ CONF_ATTRIBUTION = "Data provided by Trafikverket API" CONF_STATION = 'station' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + SCAN_INTERVAL = timedelta(seconds=300) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -36,7 +37,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" + """Set up the Trafikverket sensor platform.""" sensor_name = config.get(CONF_NAME) sensor_api = config.get(CONF_API_KEY) sensor_station = config.get(CONF_STATION) @@ -47,10 +48,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class TrafikverketWeatherStation(Entity): - """Representation of a Sensor.""" + """Representation of a Trafikverket sensor.""" def __init__(self, sensor_name, sensor_api, sensor_station, sensor_type): - """Initialize the sensor.""" + """Initialize the Trafikverket sensor.""" self._name = sensor_name self._api = sensor_api self._station = sensor_station @@ -82,10 +83,7 @@ class TrafikverketWeatherStation(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ + """Fetch new state data for the sensor.""" url = 'http://api.trafikinfo.trafikverket.se/v1.3/data.json' if self._type == 'road': @@ -117,7 +115,7 @@ class TrafikverketWeatherStation(Entity): result = data["RESPONSE"]["RESULT"][0] final = result["WeatherStation"][0]["Measurement"] except KeyError: - _LOGGER.error("Incorrect weather station or API key.") + _LOGGER.error("Incorrect weather station or API key") return # air_vs_road contains "Air" or "Road" depending on user input. From 48fe2d18e8c752b36a9a5e40bad77c540da2665f Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 6 Apr 2018 21:48:50 +0200 Subject: [PATCH 304/924] Add option to ignore availability in google calendar events (#13714) --- homeassistant/components/calendar/google.py | 25 +++++++++++++++++---- homeassistant/components/google.py | 4 +++- tests/components/test_google.py | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/calendar/google.py b/homeassistant/components/calendar/google.py index a8763e8ca9e..6c26c65ebe7 100644 --- a/homeassistant/components/calendar/google.py +++ b/homeassistant/components/calendar/google.py @@ -11,6 +11,7 @@ from datetime import timedelta from homeassistant.components.calendar import CalendarEventDevice from homeassistant.components.google import ( CONF_CAL_ID, CONF_ENTITIES, CONF_TRACK, TOKEN_FILE, + CONF_IGNORE_AVAILABILITY, CONF_SEARCH, GoogleCalendarService) from homeassistant.util import Throttle, dt @@ -18,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_GOOGLE_SEARCH_PARAMS = { 'orderBy': 'startTime', - 'maxResults': 1, + 'maxResults': 5, 'singleEvents': True, } @@ -45,18 +46,22 @@ class GoogleCalendarEventDevice(CalendarEventDevice): def __init__(self, hass, calendar_service, calendar, data): """Create the Calendar event device.""" self.data = GoogleCalendarData(calendar_service, calendar, - data.get('search', None)) + data.get(CONF_SEARCH), + data.get(CONF_IGNORE_AVAILABILITY)) + super().__init__(hass, data) class GoogleCalendarData(object): """Class to utilize calendar service object to get next event.""" - def __init__(self, calendar_service, calendar_id, search=None): + def __init__(self, calendar_service, calendar_id, search, + ignore_availability): """Set up how we are going to search the google calendar.""" self.calendar_service = calendar_service self.calendar_id = calendar_id self.search = search + self.ignore_availability = ignore_availability self.event = None @Throttle(MIN_TIME_BETWEEN_UPDATES) @@ -80,5 +85,17 @@ class GoogleCalendarData(object): result = events.list(**params).execute() items = result.get('items', []) - self.event = items[0] if len(items) == 1 else None + + new_event = None + for item in items: + if (not self.ignore_availability + and 'transparency' in item.keys()): + if item['transparency'] == 'opaque': + new_event = item + break + else: + new_event = item + break + + self.event = new_event return True diff --git a/homeassistant/components/google.py b/homeassistant/components/google.py index 30151ee1a56..b41d4ea33a2 100644 --- a/homeassistant/components/google.py +++ b/homeassistant/components/google.py @@ -44,6 +44,7 @@ CONF_ENTITIES = 'entities' CONF_TRACK = 'track' CONF_SEARCH = 'search' CONF_OFFSET = 'offset' +CONF_IGNORE_AVAILABILITY = 'ignore_availability' DEFAULT_CONF_TRACK_NEW = True DEFAULT_CONF_OFFSET = '!!' @@ -74,8 +75,9 @@ _SINGLE_CALSEARCH_CONFIG = vol.Schema({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string, vol.Optional(CONF_TRACK): cv.boolean, - vol.Optional(CONF_SEARCH): vol.Any(cv.string, None), + vol.Optional(CONF_SEARCH): cv.string, vol.Optional(CONF_OFFSET): cv.string, + vol.Optional(CONF_IGNORE_AVAILABILITY, default=True): cv.boolean, }) DEVICE_SCHEMA = vol.Schema({ diff --git a/tests/components/test_google.py b/tests/components/test_google.py index fd45cfc59a9..0ee066fcfee 100644 --- a/tests/components/test_google.py +++ b/tests/components/test_google.py @@ -58,6 +58,7 @@ class TestGoogle(unittest.TestCase): 'device_id': 'we_are_we_are_a_test_calendar', 'name': 'We are, we are, a... Test Calendar', 'track': True, + 'ignore_availability': True, }] }) From c77d013f430b25b131843d7a72c75d6fb8aa8831 Mon Sep 17 00:00:00 2001 From: Juggels Date: Fri, 6 Apr 2018 22:23:40 +0200 Subject: [PATCH 305/924] Allow use of date_string in service call (#13256) * Allow use of date_string in service call * Add stricter validation, fix descriptions --- .../components/calendar/services.yaml | 43 +++++++++++-------- homeassistant/components/calendar/todoist.py | 20 ++++++++- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index 61ff4345fbe..ebf0c7b1591 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1,21 +1,26 @@ # Describes the format for available calendar services -todoist: - new_task: - description: Create a new task and add it to a project. - fields: - content: - description: The name of the task (Required). - example: Pick up the mail - project: - description: The name of the project this task should belong to. Defaults to Inbox (Optional). - example: Errands - labels: - description: Any labels that you want to apply to this task, separated by a comma (Optional). - example: Chores,Deliveries - priority: - description: The priority of this task, from 1 (normal) to 4 (urgent) (Optional). - example: 2 - due_date: - description: The day this task is due, in format YYYY-MM-DD (Optional). - example: "2018-04-01" +todoist_new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Deliveries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: "tomorrow" + due_date_lang: + description: The language of due_date_string. + example: "en" + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: "2018-04-01" diff --git a/homeassistant/components/calendar/todoist.py b/homeassistant/components/calendar/todoist.py index 02840c7d0ee..b70e44456db 100644 --- a/homeassistant/components/calendar/todoist.py +++ b/homeassistant/components/calendar/todoist.py @@ -41,6 +41,14 @@ CONTENT = 'content' DESCRIPTION = 'description' # Calendar Platform: Used in the '_get_date()' method DATETIME = 'dateTime' +# Service Call: When is this task due (in natural language)? +DUE_DATE_STRING = 'due_date_string' +# Service Call: The language of DUE_DATE_STRING +DUE_DATE_LANG = 'due_date_lang' +# Service Call: The available options of DUE_DATE_LANG +DUE_DATE_VALID_LANGS = ['en', 'da', 'pl', 'zh', 'ko', 'de', + 'pt', 'ja', 'it', 'fr', 'sv', 'ru', + 'es', 'nl'] # Attribute: When is this task due? # Service Call: When is this task due? DUE_DATE = 'due_date' @@ -83,7 +91,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema({ vol.Optional(PROJECT_NAME, default='inbox'): vol.All(cv.string, vol.Lower), vol.Optional(LABELS): cv.ensure_list_csv, vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)), - vol.Optional(DUE_DATE): cv.string, + + vol.Exclusive(DUE_DATE_STRING, 'due_date'): cv.string, + vol.Optional(DUE_DATE_LANG): + vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)), + vol.Exclusive(DUE_DATE, 'due_date'): cv.string, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -186,6 +198,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + if DUE_DATE_STRING in call.data: + item.update(date_string=call.data[DUE_DATE_STRING]) + + if DUE_DATE_LANG in call.data: + item.update(date_lang=call.data[DUE_DATE_LANG]) + if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) if due_date is None: From 262ea14e5a9bc63ba9b8f915874df0cf9517f412 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 6 Apr 2018 23:11:53 +0200 Subject: [PATCH 306/924] Add timeout / debounce (for brightness and others) (#13534) * Add async timeout feature * Decorator for setter methods to limit service calls to HA * Changed to async * Use async_call_later * Use lastargs, async_add_job * Use dict for lastargs * Updated tests to stop patch --- .../components/homekit/accessories.py | 51 +++++++++++++++++-- homeassistant/components/homekit/const.py | 1 + .../components/homekit/type_lights.py | 3 +- .../components/homekit/type_thermostats.py | 5 +- tests/components/homekit/test_accessories.py | 48 ++++++++++++++++- tests/components/homekit/test_homekit.py | 12 +++++ tests/components/homekit/test_type_lights.py | 24 +++++++-- .../homekit/test_type_thermostats.py | 23 +++++++-- 8 files changed, 151 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index da45bee9e90..ec2c49f5e43 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,21 +1,64 @@ """Extend the basic Accessory and Bridge functions.""" +from datetime import timedelta +from functools import wraps +from inspect import getmodule import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver -from homeassistant.helpers.event import async_track_state_change +from homeassistant.core import callback +from homeassistant.helpers.event import ( + async_track_state_change, track_point_in_utc_time) +from homeassistant.util import dt as dt_util from .const import ( - ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, - CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, + BRIDGE_NAME, MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, + CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) _LOGGER = logging.getLogger(__name__) +def debounce(func): + """Decorator function. Debounce callbacks form HomeKit.""" + @callback + def call_later_listener(*args): + """Callback listener called from call_later.""" + # pylint: disable=unsubscriptable-object + nonlocal lastargs, remove_listener + hass = lastargs['hass'] + hass.async_add_job(func, *lastargs['args']) + lastargs = remove_listener = None + + @wraps(func) + def wrapper(*args): + """Wrapper starts async timer. + + The accessory must have 'self.hass' and 'self.entity_id' as attributes. + """ + # pylint: disable=not-callable + hass = args[0].hass + nonlocal lastargs, remove_listener + if remove_listener: + remove_listener() + lastargs = remove_listener = None + lastargs = {'hass': hass, 'args': [*args]} + remove_listener = track_point_in_utc_time( + hass, call_later_listener, + dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) + logger.debug('%s: Start %s timeout', args[0].entity_id, + func.__name__.replace('set_', '')) + + remove_listener = None + lastargs = None + name = getmodule(func).__name__ + logger = logging.getLogger(name) + return wrapper + + def add_preload_service(acc, service, chars=None): """Define and return a service to be available for the accessory.""" from pyhap.loader import get_serv_loader, get_char_loader diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d1c3d84b517..18d02a89e18 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,5 +1,6 @@ """Constants used be the HomeKit component.""" # #### MISC #### +DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 1110981fe10..4fbfb995859 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -7,7 +7,7 @@ from homeassistant.components.light import ( from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, debounce from .const import ( CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) @@ -93,6 +93,7 @@ class Light(HomeAccessory): elif value == 0: self.hass.components.light.turn_off(self.entity_id) + @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index de8ecbdfe3e..daf81c51c4d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -10,7 +10,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, debounce from .const import ( CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, @@ -104,6 +104,7 @@ class Thermostat(HomeAccessory): self.hass.components.climate.set_operation_mode( operation_mode=hass_value, entity_id=self.entity_id) + @debounce def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set cooling threshold temperature to %.2f°C', @@ -116,6 +117,7 @@ class Thermostat(HomeAccessory): entity_id=self.entity_id, target_temp_high=value, target_temp_low=low) + @debounce def set_heating_threshold(self, value): """Set heating threshold temp to value if call came from HomeKit.""" _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', @@ -129,6 +131,7 @@ class Thermostat(HomeAccessory): entity_id=self.entity_id, target_temp_high=high, target_temp_low=value) + @debounce def set_target_temperature(self, value): """Set target temperature to value if call came from HomeKit.""" _LOGGER.debug('%s: Set target temperature to %.2f°C', diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index a2facd826e4..b7bf625a2d6 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -2,21 +2,67 @@ This includes tests for all mock object types. """ +from datetime import datetime, timedelta import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( add_preload_service, set_accessory_info, - HomeAccessory, HomeBridge, HomeDriver) + debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) +from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED +import homeassistant.util.dt as dt_util + +from tests.common import get_test_home_assistant + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) class TestAccessories(unittest.TestCase): """Test pyhap adapter methods.""" + def test_debounce(self): + """Test add_timeout decorator function.""" + def demo_func(*args): + nonlocal arguments, counter + counter += 1 + arguments = args + + arguments = None + counter = 0 + hass = get_test_home_assistant() + mock = Mock(hass=hass) + + debounce_demo = debounce(demo_func) + self.assertEqual(debounce_demo.__name__, 'demo_func') + now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.util.dt.utcnow', return_value=now): + debounce_demo(mock, 'value') + hass.bus.fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + hass.block_till_done() + assert counter == 1 + assert len(arguments) == 2 + + with patch('homeassistant.util.dt.utcnow', return_value=now): + debounce_demo(mock, 'value') + debounce_demo(mock, 'value') + + hass.bus.fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + hass.block_till_done() + assert counter == 2 + + hass.stop() + def test_add_preload_service(self): """Test add_preload_service without additional characteristics.""" acc = Mock() diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index c6d79545487..51a965b5817 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' @@ -22,6 +23,17 @@ PATH_HOMEKIT = 'homeassistant.components.homekit' class TestHomeKit(unittest.TestCase): """Test setup of HomeKit component and HomeKit class.""" + @classmethod + def setUpClass(cls): + """Setup debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 1d18235d4a1..af8676dfd74 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -2,7 +2,6 @@ import unittest from homeassistant.core import callback -from homeassistant.components.homekit.type_lights import Light from homeassistant.components.light import ( DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) @@ -12,11 +11,26 @@ from homeassistant.const import ( SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce class TestHomekitLights(unittest.TestCase): """Test class for all accessory types regarding lights.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_lights', + fromlist=['Light']) + cls.light_cls = _import.Light + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -38,7 +52,7 @@ class TestHomekitLights(unittest.TestCase): entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.aid, 2) self.assertEqual(acc.category, 5) # Lightbulb self.assertEqual(acc.char_on.value, 0) @@ -82,7 +96,7 @@ class TestHomekitLights(unittest.TestCase): entity_id = 'light.demo' self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_brightness.value, 0) acc.run() @@ -124,7 +138,7 @@ class TestHomekitLights(unittest.TestCase): self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_color_temperature.value, 153) acc.run() @@ -146,7 +160,7 @@ class TestHomekitLights(unittest.TestCase): self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) - acc = Light(self.hass, entity_id, 'Light', aid=2) + acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d363e26d712..feea5c0d01a 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -6,17 +6,32 @@ from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) -from homeassistant.components.homekit.type_thermostats import Thermostat from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce class TestHomekitThermostats(unittest.TestCase): """Test class for all accessory types regarding thermostats.""" + @classmethod + def setUpClass(cls): + """Setup Thermostat class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__( + 'homeassistant.components.homekit.type_thermostats', + fromlist=['Thermostat']) + cls.thermostat_cls = _import.Thermostat + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +52,7 @@ class TestHomekitThermostats(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = Thermostat(self.hass, climate, 'Climate', False, aid=2) + acc = self.thermostat_cls(self.hass, climate, 'Climate', False, aid=2) acc.run() self.assertEqual(acc.aid, 2) @@ -172,7 +187,7 @@ class TestHomekitThermostats(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = Thermostat(self.hass, climate, 'Climate', True) + acc = self.thermostat_cls(self.hass, climate, 'Climate', True) acc.run() self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) @@ -242,7 +257,7 @@ class TestHomekitThermostats(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = Thermostat(self.hass, climate, 'Climate', True) + acc = self.thermostat_cls(self.hass, climate, 'Climate', True) acc.run() self.hass.states.set(climate, STATE_AUTO, From fdf93d18298c89714e438dc5dfbba858f29de18e Mon Sep 17 00:00:00 2001 From: Henrik Nicolaisen Date: Fri, 6 Apr 2018 23:14:31 +0200 Subject: [PATCH 307/924] added support for smappee water sensors (#12831) * added support for smappee water sensors * fixed lint error and wrong location_id * fixed lint error * Use string formatting --- homeassistant/components/sensor/smappee.py | 76 ++++++++++++++++------ homeassistant/components/smappee.py | 21 ++++-- 2 files changed, 72 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/sensor/smappee.py b/homeassistant/components/sensor/smappee.py index c59798d16d7..5b84962144d 100644 --- a/homeassistant/components/sensor/smappee.py +++ b/homeassistant/components/sensor/smappee.py @@ -31,7 +31,19 @@ SENSOR_TYPES = { 'solar_today': ['Solar Today', 'mdi:white-balance-sunny', 'remote', 'kWh', 'solar'], 'power_today': - ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'] + ['Power Today', 'mdi:power-plug', 'remote', 'kWh', 'consumption'], + 'water_sensor_1': + ['Water Sensor 1', 'mdi:water', 'water', 'm3', 'value1'], + 'water_sensor_2': + ['Water Sensor 2', 'mdi:water', 'water', 'm3', 'value2'], + 'water_sensor_temperature': + ['Water Sensor Temperature', 'mdi:temperature-celsius', + 'water', '°', 'temperature'], + 'water_sensor_humidity': + ['Water Sensor Humidity', 'mdi:water-percent', 'water', + '%', 'humidity'], + 'water_sensor_battery': + ['Water Sensor Battery', 'mdi:battery', 'water', '%', 'battery'], } SCAN_INTERVAL = timedelta(seconds=30) @@ -43,36 +55,50 @@ def setup_platform(hass, config, add_devices, discovery_info=None): dev = [] if smappee.is_remote_active: - for sensor in SENSOR_TYPES: - if 'remote' in SENSOR_TYPES[sensor]: - for location_id in smappee.locations.keys(): - dev.append(SmappeeSensor(smappee, location_id, sensor)) + for location_id in smappee.locations.keys(): + for sensor in SENSOR_TYPES: + if 'remote' in SENSOR_TYPES[sensor]: + dev.append(SmappeeSensor(smappee, location_id, + sensor, + SENSOR_TYPES[sensor])) + elif 'water' in SENSOR_TYPES[sensor]: + for items in smappee.info[location_id].get('sensors'): + dev.append(SmappeeSensor( + smappee, + location_id, + '{}:{}'.format(sensor, items.get('id')), + SENSOR_TYPES[sensor])) if smappee.is_local_active: - for sensor in SENSOR_TYPES: - if 'local' in SENSOR_TYPES[sensor]: - if smappee.is_remote_active: - for location_id in smappee.locations.keys(): - dev.append(SmappeeSensor(smappee, location_id, sensor)) - else: - dev.append(SmappeeSensor(smappee, None, sensor)) + for location_id in smappee.locations.keys(): + for sensor in SENSOR_TYPES: + if 'local' in SENSOR_TYPES[sensor]: + if smappee.is_remote_active: + dev.append(SmappeeSensor(smappee, location_id, sensor, + SENSOR_TYPES[sensor])) + else: + dev.append(SmappeeSensor(smappee, None, sensor, + SENSOR_TYPES[sensor])) + add_devices(dev, True) class SmappeeSensor(Entity): """Implementation of a Smappee sensor.""" - def __init__(self, smappee, location_id, sensor): - """Initialize the sensor.""" + def __init__(self, smappee, location_id, sensor, attributes): + """Initialize the Smappee sensor.""" self._smappee = smappee self._location_id = location_id + self._attributes = attributes self._sensor = sensor self.data = None self._state = None - self._name = SENSOR_TYPES[self._sensor][0] - self._icon = SENSOR_TYPES[self._sensor][1] - self._unit_of_measurement = SENSOR_TYPES[self._sensor][3] - self._smappe_name = SENSOR_TYPES[self._sensor][4] + self._name = self._attributes[0] + self._icon = self._attributes[1] + self._type = self._attributes[2] + self._unit_of_measurement = self._attributes[3] + self._smappe_name = self._attributes[4] @property def name(self): @@ -82,9 +108,7 @@ class SmappeeSensor(Entity): else: location_name = 'Local' - return "{} {} {}".format(SENSOR_PREFIX, - location_name, - self._name) + return "{} {} {}".format(SENSOR_PREFIX, location_name, self._name) @property def icon(self): @@ -160,3 +184,13 @@ class SmappeeSensor(Entity): if i['key'].endswith('phase5ActivePower')] power = sum(value1 + value2 + value3) / 1000 self._state = round(power, 2) + elif self._type == 'water': + sensor_name, sensor_id = self._sensor.split(":") + data = self._smappee.sensor_consumption[self._location_id]\ + .get(int(sensor_id)) + if data: + consumption = data.get('records')[-1] + _LOGGER.debug("%s (%s) %s", + sensor_name, sensor_id, consumption) + value = consumption.get(self._smappe_name) + self._state = value diff --git a/homeassistant/components/smappee.py b/homeassistant/components/smappee.py index 1241679770b..b35cd8cf5a8 100644 --- a/homeassistant/components/smappee.py +++ b/homeassistant/components/smappee.py @@ -110,6 +110,7 @@ class Smappee(object): self.locations = {} self.info = {} self.consumption = {} + self.sensor_consumption = {} self.instantaneous = {} if self._remote_active or self._local_active: @@ -124,11 +125,22 @@ class Smappee(object): for location in service_locations: location_id = location.get('serviceLocationId') if location_id is not None: + self.sensor_consumption[location_id] = {} self.locations[location_id] = location.get('name') self.info[location_id] = self._smappy \ .get_service_location_info(location_id) _LOGGER.debug("Remote info %s %s", - self.locations, self.info) + self.locations, self.info[location_id]) + + for sensors in self.info[location_id].get('sensors'): + sensor_id = sensors.get('id') + self.sensor_consumption[location_id]\ + .update({sensor_id: self.get_sensor_consumption( + location_id, sensor_id, + aggregation=3, delta=1440)}) + _LOGGER.debug("Remote sensors %s %s", + self.locations, + self.sensor_consumption[location_id]) self.consumption[location_id] = self.get_consumption( location_id, aggregation=3, delta=1440) @@ -190,7 +202,8 @@ class Smappee(object): "Error getting comsumption from Smappee cloud. (%s)", error) - def get_sensor_consumption(self, location_id, sensor_id): + def get_sensor_consumption(self, location_id, sensor_id, + aggregation, delta): """Update data from Smappee.""" # Start & End accept epoch (in milliseconds), # datetime and pandas timestamps @@ -203,13 +216,13 @@ class Smappee(object): if not self.is_remote_active: return - start = datetime.utcnow() - timedelta(minutes=30) end = datetime.utcnow() + start = end - timedelta(minutes=delta) try: return self._smappy.get_sensor_consumption(location_id, sensor_id, start, - end, 1) + end, aggregation) except RequestException as error: _LOGGER.error( "Error getting comsumption from Smappee cloud. (%s)", From 286476f0d61ab0b038b1ef767fcea700a2d0d470 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 7 Apr 2018 02:59:55 +0100 Subject: [PATCH 308/924] Initialise filter_sensor with historical values (#13075) * Initialise filter with historical values Added get_last_state_changes() * fix test * Major changes to accommodate history + time_SMA # Conflicts: # homeassistant/components/sensor/filter.py * hail the hound! * lint fixed * less debug * ups * get state from the proper entity * sensible default * No defaults in get_last_state_changes * list_reverseiterator instead of list * prev_state to state * Initialise filter with historical values Added get_last_state_changes() * fix test * Major changes to accommodate history + time_SMA # Conflicts: # homeassistant/components/sensor/filter.py * hail the hound! * lint fixed * less debug * ups * get state from the proper entity * sensible default * No defaults in get_last_state_changes * list_reverseiterator instead of list * prev_state to state * update * added window_unit * replace isinstance with window_unit --- homeassistant/components/history.py | 24 ++++ homeassistant/components/sensor/filter.py | 158 +++++++++++++++++----- tests/components/sensor/test_filter.py | 68 +++++++--- tests/components/test_history.py | 33 +++++ 4 files changed, 229 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index 8ab91b08a3d..b5ac37b1451 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -118,6 +118,30 @@ def state_changes_during_period(hass, start_time, end_time=None, return states_to_json(hass, states, start_time, entity_ids) +def get_last_state_changes(hass, number_of_states, entity_id): + """Return the last number_of_states.""" + from homeassistant.components.recorder.models import States + + start_time = dt_util.utcnow() + + with session_scope(hass=hass) as session: + query = session.query(States).filter( + (States.last_changed == States.last_updated)) + + if entity_id is not None: + query = query.filter_by(entity_id=entity_id.lower()) + + entity_ids = [entity_id] if entity_id is not None else None + + states = execute( + query.order_by(States.last_updated.desc()).limit(number_of_states)) + + return states_to_json(hass, reversed(states), + start_time, + entity_ids, + include_start_time_state=False) + + def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None): """Return the states at a specific point in time.""" diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 3faf51a5f47..27730a8f63e 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -8,6 +8,9 @@ import logging import statistics from collections import deque, Counter from numbers import Number +from functools import partial +from copy import copy +from datetime import timedelta import voluptuous as vol @@ -20,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.components.history as history import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -40,6 +44,9 @@ CONF_TIME_SMA_TYPE = 'type' TIME_SMA_LAST = 'last' +WINDOW_SIZE_UNIT_NUMBER_EVENTS = 1 +WINDOW_SIZE_UNIT_TIME = 2 + DEFAULT_WINDOW_SIZE = 1 DEFAULT_PRECISION = 2 DEFAULT_FILTER_RADIUS = 2.0 @@ -123,21 +130,22 @@ class SensorFilter(Entity): async def async_added_to_hass(self): """Register callbacks.""" @callback - def filter_sensor_state_listener(entity, old_state, new_state): + def filter_sensor_state_listener(entity, old_state, new_state, + update_ha=True): """Handle device state changes.""" if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return - temp_state = new_state.state + temp_state = new_state try: for filt in self._filters: - filtered_state = filt.filter_state(temp_state) + filtered_state = filt.filter_state(copy(temp_state)) _LOGGER.debug("%s(%s=%s) -> %s", filt.name, self._entity, - temp_state, + temp_state.state, "skip" if filt.skip_processing else - filtered_state) + filtered_state.state) if filt.skip_processing: return temp_state = filtered_state @@ -146,7 +154,7 @@ class SensorFilter(Entity): self._state) return - self._state = temp_state + self._state = temp_state.state if self._icon is None: self._icon = new_state.attributes.get( @@ -156,7 +164,50 @@ class SensorFilter(Entity): self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT) - self.async_schedule_update_ha_state() + if update_ha: + self.async_schedule_update_ha_state() + + if 'recorder' in self.hass.config.components: + history_list = [] + largest_window_items = 0 + largest_window_time = timedelta(0) + + # Determine the largest window_size by type + for filt in self._filters: + if filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS\ + and largest_window_items < filt.window_size: + largest_window_items = filt.window_size + elif filt.window_unit == WINDOW_SIZE_UNIT_TIME\ + and largest_window_time < filt.window_size: + largest_window_time = filt.window_size + + # Retrieve the largest window_size of each type + if largest_window_items > 0: + filter_history = await self.hass.async_add_job(partial( + history.get_last_state_changes, self.hass, + largest_window_items, entity_id=self._entity)) + history_list.extend( + [state for state in filter_history[self._entity]]) + if largest_window_time > timedelta(seconds=0): + start = dt_util.utcnow() - largest_window_time + filter_history = await self.hass.async_add_job(partial( + history.state_changes_during_period, self.hass, + start, entity_id=self._entity)) + history_list.extend( + [state for state in filter_history[self._entity] + if state not in history_list]) + + # Sort the window states + history_list = sorted(history_list, key=lambda s: s.last_updated) + _LOGGER.debug("Loading from history: %s", + [(s.state, s.last_updated) for s in history_list]) + + # Replay history through the filter chain + prev_state = None + for state in history_list: + filter_sensor_state_listener( + self._entity, prev_state, state, False) + prev_state = state async_track_state_change( self.hass, self._entity, filter_sensor_state_listener) @@ -195,6 +246,31 @@ class SensorFilter(Entity): return state_attr +class FilterState(object): + """State abstraction for filter usage.""" + + def __init__(self, state): + """Initialize with HA State object.""" + self.timestamp = state.last_updated + try: + self.state = float(state.state) + except ValueError: + self.state = state.state + + def set_precision(self, precision): + """Set precision of Number based states.""" + if isinstance(self.state, Number): + self.state = round(float(self.state), precision) + + def __str__(self): + """Return state as the string representation of FilterState.""" + return str(self.state) + + def __repr__(self): + """Return timestamp and state as the representation of FilterState.""" + return "{} : {}".format(self.timestamp, self.state) + + class Filter(object): """Filter skeleton. @@ -207,11 +283,22 @@ class Filter(object): def __init__(self, name, window_size=1, precision=None, entity=None): """Initialize common attributes.""" - self.states = deque(maxlen=window_size) + if isinstance(window_size, int): + self.states = deque(maxlen=window_size) + self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS + else: + self.states = deque(maxlen=0) + self.window_unit = WINDOW_SIZE_UNIT_TIME self.precision = precision self._name = name self._entity = entity self._skip_processing = False + self._window_size = window_size + + @property + def window_size(self): + """Return window size.""" + return self._window_size @property def name(self): @@ -229,11 +316,11 @@ class Filter(object): def filter_state(self, new_state): """Implement a common interface for filters.""" - filtered = self._filter_state(new_state) - if isinstance(filtered, Number): - filtered = round(float(filtered), self.precision) - self.states.append(filtered) - return filtered + filtered = self._filter_state(FilterState(new_state)) + filtered.set_precision(self.precision) + self.states.append(copy(filtered)) + new_state.state = filtered.state + return new_state @FILTERS.register(FILTER_NAME_OUTLIER) @@ -254,11 +341,10 @@ class OutlierFilter(Filter): def _filter_state(self, new_state): """Implement the outlier filter.""" - new_state = float(new_state) - if (self.states and - abs(new_state - statistics.median(self.states)) - > self._radius): + abs(new_state.state - + statistics.median([s.state for s in self.states])) > + self._radius): self._stats_internal['erasures'] += 1 @@ -284,16 +370,15 @@ class LowPassFilter(Filter): def _filter_state(self, new_state): """Implement the low pass filter.""" - new_state = float(new_state) - if not self.states: return new_state new_weight = 1.0 / self._time_constant prev_weight = 1.0 - new_weight - filtered = prev_weight * self.states[-1] + new_weight * new_state + new_state.state = prev_weight * self.states[-1].state +\ + new_weight * new_state.state - return filtered + return new_state @FILTERS.register(FILTER_NAME_TIME_SMA) @@ -308,35 +393,36 @@ class TimeSMAFilter(Filter): def __init__(self, window_size, precision, entity, type): """Initialize Filter.""" - super().__init__(FILTER_NAME_TIME_SMA, 0, precision, entity) - self._time_window = int(window_size.total_seconds()) + super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) + self._time_window = window_size self.last_leak = None self.queue = deque() - def _leak(self, now): + def _leak(self, left_boundary): """Remove timeouted elements.""" while self.queue: - timestamp, _ = self.queue[0] - if timestamp + self._time_window <= now: + if self.queue[0].timestamp + self._time_window <= left_boundary: self.last_leak = self.queue.popleft() else: return def _filter_state(self, new_state): - now = int(dt_util.utcnow().timestamp()) + """Implement the Simple Moving Average filter.""" + self._leak(new_state.timestamp) + self.queue.append(copy(new_state)) - self._leak(now) - self.queue.append((now, float(new_state))) moving_sum = 0 - start = now - self._time_window - _, prev_val = self.last_leak or (0, float(new_state)) + start = new_state.timestamp - self._time_window + prev_state = self.last_leak or self.queue[0] + for state in self.queue: + moving_sum += (state.timestamp-start).total_seconds()\ + * prev_state.state + start = state.timestamp + prev_state = state - for timestamp, val in self.queue: - moving_sum += (timestamp-start)*prev_val - start, prev_val = timestamp, val - moving_sum += (now-start)*prev_val + new_state.state = moving_sum / self._time_window.total_seconds() - return moving_sum/self._time_window + return new_state @FILTERS.register(FILTER_NAME_THROTTLE) diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 0d4082731ab..8b8e7607b07 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -7,7 +7,9 @@ from homeassistant.components.sensor.filter import ( LowPassFilter, OutlierFilter, ThrottleFilter, TimeSMAFilter) import homeassistant.util.dt as dt_util from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component +import homeassistant.core as ha +from tests.common import (get_test_home_assistant, assert_setup_component, + init_recorder_component) class TestFilterSensor(unittest.TestCase): @@ -16,12 +18,24 @@ class TestFilterSensor(unittest.TestCase): def setup_method(self, method): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.values = [20, 19, 18, 21, 22, 0] + raw_values = [20, 19, 18, 21, 22, 0] + self.values = [] + + timestamp = dt_util.utcnow() + for val in raw_values: + self.values.append(ha.State('sensor.test_monitored', + val, last_updated=timestamp)) + timestamp += timedelta(minutes=1) def teardown_method(self, method): """Stop everything that was started.""" self.hass.stop() + def init_recorder(self): + """Initialize the recorder.""" + init_recorder_component(self.hass) + self.hass.start() + def test_setup_fail(self): """Test if filter doesn't exist.""" config = { @@ -36,31 +50,52 @@ class TestFilterSensor(unittest.TestCase): def test_chain(self): """Test if filter chaining works.""" + self.init_recorder() config = { + 'history': { + }, 'sensor': { 'platform': 'filter', 'name': 'test', 'entity_id': 'sensor.test_monitored', + 'history_period': '00:05', 'filters': [{ 'filter': 'outlier', + 'window_size': 10, 'radius': 4.0 }, { 'filter': 'lowpass', - 'window_size': 4, 'time_constant': 10, 'precision': 2 }] } } - with assert_setup_component(1): - assert setup_component(self.hass, 'sensor', config) + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) - for value in self.values: - self.hass.states.set(config['sensor']['entity_id'], value) - self.hass.block_till_done() + fake_states = { + 'sensor.test_monitored': [ + ha.State('sensor.test_monitored', 18.0, last_changed=t_0), + ha.State('sensor.test_monitored', 19.0, last_changed=t_1), + ha.State('sensor.test_monitored', 18.2, last_changed=t_2), + ] + } - state = self.hass.states.get('sensor.test') - self.assertEqual('20.25', state.state) + with patch('homeassistant.components.history.' + 'state_changes_during_period', return_value=fake_states): + with patch('homeassistant.components.history.' + 'get_last_state_changes', return_value=fake_states): + with assert_setup_component(1, 'sensor'): + assert setup_component(self.hass, 'sensor', config) + + for value in self.values: + self.hass.states.set( + config['sensor']['entity_id'], value.state) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + self.assertEqual('19.25', state.state) def test_outlier(self): """Test if outlier filter works.""" @@ -70,7 +105,7 @@ class TestFilterSensor(unittest.TestCase): radius=4.0) for state in self.values: filtered = filt.filter_state(state) - self.assertEqual(22, filtered) + self.assertEqual(22, filtered.state) def test_lowpass(self): """Test if lowpass filter works.""" @@ -80,7 +115,7 @@ class TestFilterSensor(unittest.TestCase): time_constant=10) for state in self.values: filtered = filt.filter_state(state) - self.assertEqual(18.05, filtered) + self.assertEqual(18.05, filtered.state) def test_throttle(self): """Test if lowpass filter works.""" @@ -92,7 +127,7 @@ class TestFilterSensor(unittest.TestCase): new_state = filt.filter_state(state) if not filt.skip_processing: filtered.append(new_state) - self.assertEqual([20, 21], filtered) + self.assertEqual([20, 21], [f.state for f in filtered]) def test_time_sma(self): """Test if time_sma filter works.""" @@ -100,9 +135,6 @@ class TestFilterSensor(unittest.TestCase): precision=2, entity=None, type='last') - past = dt_util.utcnow() - timedelta(minutes=5) for state in self.values: - with patch('homeassistant.util.dt.utcnow', return_value=past): - filtered = filt.filter_state(state) - past += timedelta(minutes=1) - self.assertEqual(21.5, filtered) + filtered = filt.filter_state(state) + self.assertEqual(21.5, filtered.state) diff --git a/tests/components/test_history.py b/tests/components/test_history.py index bea2af396cb..5d909492380 100644 --- a/tests/components/test_history.py +++ b/tests/components/test_history.py @@ -131,6 +131,39 @@ class TestComponentHistory(unittest.TestCase): self.assertEqual(states, hist[entity_id]) + def test_get_last_state_changes(self): + """Test number of state changes.""" + self.init_recorder() + entity_id = 'sensor.test' + + def set_state(state): + """Set the state.""" + self.hass.states.set(entity_id, state) + self.wait_recording_done() + return self.hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1) + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=start): + set_state('1') + + states = [] + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point): + states.append(set_state('2')) + + with patch('homeassistant.components.recorder.dt_util.utcnow', + return_value=point2): + states.append(set_state('3')) + + hist = history.get_last_state_changes( + self.hass, 2, entity_id) + + self.assertEqual(states, hist[entity_id]) + def test_get_significant_states(self): """Test that only significant states are returned. From 58f3690ef6b8664501b8f74b8e1684a03b205009 Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Sat, 7 Apr 2018 13:48:53 +1000 Subject: [PATCH 309/924] Fix Gogogate2 'available' attribute (#13728) * Fixed bug - unable to set base readaonly property * PR fixes * Added line --- homeassistant/components/cover/gogogate2.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index c2bdc9c5472..99da248b094 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN, + CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(MyGogogate2Device( mygogogate2, door, name) for door in devices) - return except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) @@ -60,7 +59,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ''.format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - return class MyGogogate2Device(CoverDevice): @@ -72,7 +70,7 @@ class MyGogogate2Device(CoverDevice): self.device_id = device['door'] self._name = name or device['name'] self._status = device['status'] - self.available = None + self._available = None @property def name(self): @@ -97,24 +95,22 @@ class MyGogogate2Device(CoverDevice): @property def available(self): """Could the device be accessed during the last update call.""" - return self.available + return self._available def close_cover(self, **kwargs): """Issue close command to cover.""" self.mygogogate2.close_device(self.device_id) - self.schedule_update_ha_state(True) def open_cover(self, **kwargs): """Issue open command to cover.""" self.mygogogate2.open_device(self.device_id) - self.schedule_update_ha_state(True) def update(self): """Update status of cover.""" try: self._status = self.mygogogate2.get_status(self.device_id) - self.available = True + self._available = True except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) - self._status = STATE_UNKNOWN - self.available = False + self._status = None + self._available = False From b0fd2342dbdf812bb1a8ec0cab07ec0f0e990f03 Mon Sep 17 00:00:00 2001 From: thrawnarn Date: Sat, 7 Apr 2018 10:09:09 +0200 Subject: [PATCH 310/924] Bluesound bugfix status 595 and await (#13727) * 595 fix * Await fixes and last 595 fix * Lint * Made internal exception class * Fix lint issue --- .../components/media_player/bluesound.py | 127 +++++++++--------- 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/media_player/bluesound.py b/homeassistant/components/media_player/bluesound.py index 1b6310d4cab..283c4af032e 100644 --- a/homeassistant/components/media_player/bluesound.py +++ b/homeassistant/components/media_player/bluesound.py @@ -37,30 +37,30 @@ REQUIREMENTS = ['xmltodict==0.11.0'] _LOGGER = logging.getLogger(__name__) -STATE_GROUPED = 'grouped' - ATTR_MASTER = 'master' -SERVICE_JOIN = 'bluesound_join' -SERVICE_UNJOIN = 'bluesound_unjoin' -SERVICE_SET_TIMER = 'bluesound_set_sleep_timer' -SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer' - DATA_BLUESOUND = 'bluesound' DEFAULT_PORT = 11000 -SYNC_STATUS_INTERVAL = timedelta(minutes=5) -UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) -UPDATE_SERVICES_INTERVAL = timedelta(minutes=30) -UPDATE_PRESETS_INTERVAL = timedelta(minutes=30) NODE_OFFLINE_CHECK_TIMEOUT = 180 NODE_RETRY_INITIATION = timedelta(minutes=3) +SERVICE_CLEAR_TIMER = 'bluesound_clear_sleep_timer' +SERVICE_JOIN = 'bluesound_join' +SERVICE_SET_TIMER = 'bluesound_set_sleep_timer' +SERVICE_UNJOIN = 'bluesound_unjoin' +STATE_GROUPED = 'grouped' +SYNC_STATUS_INTERVAL = timedelta(minutes=5) + +UPDATE_CAPTURE_INTERVAL = timedelta(minutes=30) +UPDATE_PRESETS_INTERVAL = timedelta(minutes=30) +UPDATE_SERVICES_INTERVAL = timedelta(minutes=30) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list, [{ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }]) }) @@ -131,8 +131,8 @@ def _add_player(hass, async_add_devices, host, port=None, name=None): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _init_player) -async def async_setup_platform(hass, config, async_add_devices, - discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Bluesound platforms.""" if DATA_BLUESOUND not in hass.data: hass.data[DATA_BLUESOUND] = [] @@ -202,6 +202,9 @@ class BluesoundPlayer(MediaPlayerDevice): if self.port is None: self.port = DEFAULT_PORT + class _TimeoutException(Exception): + pass + @staticmethod def _try_get_index(string, search_string): """Get the index.""" @@ -258,7 +261,8 @@ class BluesoundPlayer(MediaPlayerDevice): while True: await self.async_update_status() - except (asyncio.TimeoutError, ClientError): + except (asyncio.TimeoutError, ClientError, + BluesoundPlayer._TimeoutException): _LOGGER.info("Node %s is offline, retrying later", self._name) await asyncio.sleep( NODE_OFFLINE_CHECK_TIMEOUT, loop=self._hass.loop) @@ -293,8 +297,8 @@ class BluesoundPlayer(MediaPlayerDevice): self._retry_remove = async_track_time_interval( self._hass, self.async_init, NODE_RETRY_INITIATION) except Exception: - _LOGGER.exception("Unexpected when initiating error in %s", - self.host) + _LOGGER.exception( + "Unexpected when initiating error in %s", self.host) raise async def async_update(self): @@ -307,8 +311,8 @@ class BluesoundPlayer(MediaPlayerDevice): await self.async_update_captures() await self.async_update_services() - async def send_bluesound_command(self, method, raise_timeout=False, - allow_offline=False): + async def send_bluesound_command( + self, method, raise_timeout=False, allow_offline=False): """Send command to the player.""" import xmltodict @@ -321,6 +325,7 @@ class BluesoundPlayer(MediaPlayerDevice): _LOGGER.debug("Calling URL: %s", url) response = None + try: websession = async_get_clientsession(self._hass) with async_timeout.timeout(10, loop=self._hass.loop): @@ -332,6 +337,9 @@ class BluesoundPlayer(MediaPlayerDevice): data = None else: data = xmltodict.parse(result) + elif response.status == 595: + _LOGGER.info("Status 595 returned, treating as timeout") + raise BluesoundPlayer._TimeoutException() else: _LOGGER.error("Error %s on %s", response.status, url) return None @@ -366,13 +374,9 @@ class BluesoundPlayer(MediaPlayerDevice): with async_timeout.timeout(125, loop=self._hass.loop): response = await self._polling_session.get( - url, - headers={CONNECTION: KEEP_ALIVE}) + url, headers={CONNECTION: KEEP_ALIVE}) - if response.status != 200: - _LOGGER.error("Error %s on %s. Trying one more time.", - response.status, url) - else: + if response.status == 200: result = await response.text() self._is_online = True self._last_status_update = dt_util.utcnow() @@ -380,8 +384,8 @@ class BluesoundPlayer(MediaPlayerDevice): group_name = self._status.get('groupName', None) if group_name != self._group_name: - _LOGGER.debug('Group name change detected on device: %s', - self.host) + _LOGGER.debug( + "Group name change detected on device: %s", self.host) self._group_name = group_name # the sleep is needed to make sure that the # devices is synced @@ -398,14 +402,20 @@ class BluesoundPlayer(MediaPlayerDevice): await self.force_update_sync_status() self.async_schedule_update_ha_state() + elif response.status == 595: + _LOGGER.info("Status 595 returned, treating as timeout") + raise BluesoundPlayer._TimeoutException() + else: + _LOGGER.error("Error %s on %s. Trying one more time", + response.status, url) except (asyncio.TimeoutError, ClientError): self._is_online = False self._last_status_update = None self._status = None self.async_schedule_update_ha_state() - _LOGGER.info("Client connection error, marking %s as offline", - self._name) + _LOGGER.info( + "Client connection error, marking %s as offline", self._name) raise async def async_trigger_sync_on_all(self): @@ -416,8 +426,8 @@ class BluesoundPlayer(MediaPlayerDevice): await player.force_update_sync_status() @Throttle(SYNC_STATUS_INTERVAL) - async def async_update_sync_status(self, on_updated_cb=None, - raise_timeout=False): + async def async_update_sync_status( + self, on_updated_cb=None, raise_timeout=False): """Update sync status.""" await self.force_update_sync_status( on_updated_cb, raise_timeout=False) @@ -465,7 +475,7 @@ class BluesoundPlayer(MediaPlayerDevice): 'image': item.get('@image', ''), 'is_raw_url': True, 'url2': item.get('@url', ''), - 'url': 'Preset?id=' + item.get('@id', '') + 'url': 'Preset?id={}'.format(item.get('@id', '')) }) if 'presets' in resp and 'preset' in resp['presets']: @@ -503,11 +513,6 @@ class BluesoundPlayer(MediaPlayerDevice): return self._services_items - @property - def should_poll(self): - """No need to poll information.""" - return True - @property def media_content_type(self): """Content type of current playing media.""" @@ -803,22 +808,22 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_add_slave(self, slave_device): """Add slave to master.""" - return self.send_bluesound_command('/AddSlave?slave={}&port={}' - .format(slave_device.host, - slave_device.port)) + return await self.send_bluesound_command( + '/AddSlave?slave={}&port={}'.format( + slave_device.host, slave_device.port)) async def async_remove_slave(self, slave_device): """Remove slave to master.""" - return self.send_bluesound_command('/RemoveSlave?slave={}&port={}' - .format(slave_device.host, - slave_device.port)) + return await self.send_bluesound_command( + '/RemoveSlave?slave={}&port={}'.format( + slave_device.host, slave_device.port)) async def async_increase_timer(self): """Increase sleep time on player.""" sleep_time = await self.send_bluesound_command('/Sleep') if sleep_time is None: - _LOGGER.error('Error while increasing sleep time on player: %s', - self.host) + _LOGGER.error( + "Error while increasing sleep time on player: %s", self.host) return 0 return int(sleep_time.get('sleep', '0')) @@ -831,8 +836,9 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" - return self.send_bluesound_command('/Shuffle?state={}' - .format('1' if shuffle else '0')) + value = '1' if shuffle else '0' + return await self.send_bluesound_command( + '/Shuffle?state={}'.format(value)) async def async_select_source(self, source): """Select input source.""" @@ -856,14 +862,14 @@ class BluesoundPlayer(MediaPlayerDevice): if 'is_raw_url' in selected_source and selected_source['is_raw_url']: url = selected_source['url'] - return self.send_bluesound_command(url) + return await self.send_bluesound_command(url) async def async_clear_playlist(self): """Clear players playlist.""" if self.is_grouped and not self.is_master: return - return self.send_bluesound_command('Clear') + return await self.send_bluesound_command('Clear') async def async_media_next_track(self): """Send media_next command to media player.""" @@ -877,7 +883,7 @@ class BluesoundPlayer(MediaPlayerDevice): action['@name'] == 'skip'): cmd = action['@url'] - return self.send_bluesound_command(cmd) + return await self.send_bluesound_command(cmd) async def async_media_previous_track(self): """Send media_previous command to media player.""" @@ -891,35 +897,36 @@ class BluesoundPlayer(MediaPlayerDevice): action['@name'] == 'back'): cmd = action['@url'] - return self.send_bluesound_command(cmd) + return await self.send_bluesound_command(cmd) async def async_media_play(self): """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return - return self.send_bluesound_command('Play') + return await self.send_bluesound_command('Play') async def async_media_pause(self): """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return - return self.send_bluesound_command('Pause') + return await self.send_bluesound_command('Pause') async def async_media_stop(self): """Send stop command.""" if self.is_grouped and not self.is_master: return - return self.send_bluesound_command('Pause') + return await self.send_bluesound_command('Pause') async def async_media_seek(self, position): """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return - return self.send_bluesound_command('Play?seek=' + str(float(position))) + return await self.send_bluesound_command( + 'Play?seek={}'.format(float(position))) async def async_play_media(self, media_type, media_id, **kwargs): """ @@ -933,9 +940,9 @@ class BluesoundPlayer(MediaPlayerDevice): url = 'Play?url={}'.format(media_id) if kwargs.get(ATTR_MEDIA_ENQUEUE): - return self.send_bluesound_command(url) + return await self.send_bluesound_command(url) - return self.send_bluesound_command(url) + return await self.send_bluesound_command(url) async def async_volume_up(self): """Volume up the media player.""" @@ -957,7 +964,7 @@ class BluesoundPlayer(MediaPlayerDevice): volume = 0 elif volume > 1: volume = 1 - return self.send_bluesound_command( + return await self.send_bluesound_command( 'Volume?level=' + str(float(volume) * 100)) async def async_mute_volume(self, mute): @@ -966,7 +973,7 @@ class BluesoundPlayer(MediaPlayerDevice): volume = self.volume_level if volume > 0: self._lastvol = volume - return self.send_bluesound_command('Volume?level=0') + return await self.send_bluesound_command('Volume?level=0') else: - return self.send_bluesound_command( + return await self.send_bluesound_command( 'Volume?level=' + str(float(self._lastvol) * 100)) From fbb8a54c391336ea791be1374cc8ca6e3a0d5f4c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 Apr 2018 10:40:34 +0200 Subject: [PATCH 311/924] Upgrade aiohttp to 3.1.2 (#13732) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 85f8d5dcf12..9e21055f0c1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.1 +aiohttp==3.1.2 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index 7af7bdb95ec..b2cb5155aec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.1 +aiohttp==3.1.2 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index db4b1f8df92..602c1d19cbd 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.1.1', + 'aiohttp==3.1.2', 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', From ca3cc27e400b5791702f622198526bdcf45dc1cc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 Apr 2018 10:41:35 +0200 Subject: [PATCH 312/924] Upgrade sqlalchemy to 1.2.6 (#13733) --- homeassistant/components/recorder/__init__.py | 7 +++---- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f10e0fc75d7..64e2b85f611 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.5'] +REQUIREMENTS = ['sqlalchemy==1.2.6'] _LOGGER = logging.getLogger(__name__) @@ -47,9 +47,8 @@ ATTR_KEEP_DAYS = 'keep_days' ATTR_REPACK = 'repack' SERVICE_PURGE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_KEEP_DAYS): - vol.All(vol.Coerce(int), vol.Range(min=0)), - vol.Optional(ATTR_REPACK, default=False): cv.boolean + vol.Optional(ATTR_KEEP_DAYS): vol.All(vol.Coerce(int), vol.Range(min=0)), + vol.Optional(ATTR_REPACK, default=False): cv.boolean, }) DEFAULT_URL = 'sqlite:///{hass_config_path}' diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index af9fa233d40..eeca31fa36b 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.5'] +REQUIREMENTS = ['sqlalchemy==1.2.6'] CONF_QUERIES = 'queries' CONF_QUERY = 'query' diff --git a/requirements_all.txt b/requirements_all.txt index b2cb5155aec..e71616d6ddf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1196,7 +1196,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.5 +sqlalchemy==1.2.6 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 645b56b9e62..ce20ecfbfc6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,7 +179,7 @@ somecomfort==0.5.0 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.5 +sqlalchemy==1.2.6 # homeassistant.components.statsd statsd==3.2.1 From 2bf17cba8e3e653394f87d94b615e7555a15d1c2 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 7 Apr 2018 01:15:35 -0800 Subject: [PATCH 313/924] Brightness conversion for Abode dimmers (#13711) With AbodePy 0.12.3, dimmers will now work but a conversion of the brightness is required. Additionally, when a brightness value of 100 is sent to Abode, 99 is returned causing AbodePy to throw an error so this component will send 99 instead of 100. Keeps the brightness value sent and returned from the device response consistent. However, during initialization and when a device refresh is received, Abode can return 100 thus we'll convert that case back to 99. --- homeassistant/components/light/abode.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) mode change 100644 => 100755 homeassistant/components/light/abode.py diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py old mode 100644 new mode 100755 index bfea19fc3fa..8b7e09d86bc --- a/homeassistant/components/light/abode.py +++ b/homeassistant/components/light/abode.py @@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.abode/ """ import logging - +from math import ceil from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, @@ -51,7 +51,9 @@ class AbodeLight(AbodeDevice, Light): *kwargs[ATTR_HS_COLOR])) if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: - self._device.set_level(kwargs[ATTR_BRIGHTNESS]) + # Convert HASS brightness (0-255) to Abode brightness (0-99) + # If 100 is sent to Abode, response is 99 causing an error + self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0)) else: self._device.switch_on() @@ -68,7 +70,12 @@ class AbodeLight(AbodeDevice, Light): def brightness(self): """Return the brightness of the light.""" if self._device.is_dimmable and self._device.has_brightness: - return self._device.brightness + brightness = int(self._device.brightness) + # Abode returns 100 during device initialization and device refresh + if brightness == 100: + return 255 + # Convert Abode brightness (0-99) to HASS brightness (0-255) + return ceil(brightness * 255 / 99.0) @property def hs_color(self): From 3084ac16258a3ee856a8891bd33f10bb8da5d53c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 7 Apr 2018 12:44:08 +0100 Subject: [PATCH 314/924] Update CODEOWNERS (sensor.filter, sensor.upnp) (#13736) --- CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 67aef6a248f..528716e174d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -63,6 +63,7 @@ homeassistant/components/media_player/xiaomi_tv.py @fattdev homeassistant/components/media_player/yamaha_musiccast.py @jalmeroth homeassistant/components/plant.py @ChristianKuehnel homeassistant/components/sensor/airvisual.py @bachya +homeassistant/components/sensor/filter.py @dgomes homeassistant/components/sensor/gearbest.py @HerrHofrat homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel @@ -72,6 +73,7 @@ homeassistant/components/sensor/sma.py @kellerza homeassistant/components/sensor/sql.py @dgomes homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen +homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti From 435b49fb96c0acd3aa86c9a7dfdbc1974a719e87 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 Apr 2018 19:11:48 +0200 Subject: [PATCH 315/924] Reset permission (#13743) --- homeassistant/components/light/abode.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 homeassistant/components/light/abode.py diff --git a/homeassistant/components/light/abode.py b/homeassistant/components/light/abode.py old mode 100755 new mode 100644 From 6cd599b7dfc8382b13325b350d17ecac4475bedb Mon Sep 17 00:00:00 2001 From: dangyuluo Date: Sat, 7 Apr 2018 15:47:56 -0400 Subject: [PATCH 316/924] Throw an error when invalid device_mode is given (#13739) * Throw an error when invalid device_mode is given * Fix lint issue, typo and error msg * Fix error msg --- homeassistant/components/climate/nest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index d11f6890a7b..0a5344fdf98 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -187,6 +187,11 @@ class NestThermostat(ClimateDevice): device_mode = operation_mode elif operation_mode == STATE_AUTO: device_mode = NEST_MODE_HEAT_COOL + else: + device_mode = STATE_OFF + _LOGGER.error( + "An error occurred while setting device mode. " + "Invalid operation mode: %s", operation_mode) self.device.mode = device_mode @property From f915a1c80982164775203bcd84f37335465ea61c Mon Sep 17 00:00:00 2001 From: Kane610 Date: Sat, 7 Apr 2018 23:18:49 +0200 Subject: [PATCH 317/924] Fix so it is possible to ignore discovered config entry handlers (#13741) * Fix so it is possible to ignore discovered config entry handlers * Improve efficiency --- homeassistant/components/discovery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b2aa5b890a8..01ef36b778b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -84,7 +84,8 @@ CONF_IGNORE = 'ignore' CONFIG_SCHEMA = vol.Schema({ vol.Required(DOMAIN): vol.Schema({ vol.Optional(CONF_IGNORE, default=[]): - vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)]) + vol.All(cv.ensure_list, [ + vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]) }), }, extra=vol.ALLOW_EXTRA) From 99f4509c2b4872e04b18e6e2d2ea836941efbfc0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 7 Apr 2018 23:19:55 +0200 Subject: [PATCH 318/924] Upgrade netdisco to 1.3.1 (#13744) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 01ef36b778b..677a13d6a9d 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.3.0'] +REQUIREMENTS = ['netdisco==1.3.1'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index e71616d6ddf..532c723365b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -540,7 +540,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.3.0 +netdisco==1.3.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 81b1d08d3580257f29c74a8110421fc3d9f4f6ee Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 8 Apr 2018 04:32:09 +0200 Subject: [PATCH 319/924] Add MQTT Sensor unique_id (#13318) * Add MQTT Sensor unique_id * Add test * Update comment --- homeassistant/components/sensor/mqtt.py | 15 ++++++++++++++- tests/components/sensor/test_mqtt.py | 21 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index d191b9a22e8..c4f64e9e015 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -8,6 +8,7 @@ import asyncio import logging import json from datetime import timedelta +from typing import Optional import voluptuous as vol @@ -28,6 +29,7 @@ _LOGGER = logging.getLogger(__name__) CONF_EXPIRE_AFTER = 'expire_after' CONF_JSON_ATTRS = 'json_attributes' +CONF_UNIQUE_ID = 'unique_id' DEFAULT_NAME = 'MQTT Sensor' DEFAULT_FORCE_UPDATE = False @@ -40,6 +42,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + # Integrations shouldn't never expose unique_id through configuration + # this here is an exception because MQTT is a msg transport, not a protocol + vol.Optional(CONF_UNIQUE_ID): cv.string, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -63,6 +68,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_ICON), value_template, config.get(CONF_JSON_ATTRS), + config.get(CONF_UNIQUE_ID), config.get(CONF_AVAILABILITY_TOPIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), @@ -74,7 +80,8 @@ class MqttSensor(MqttAvailability, Entity): def __init__(self, name, state_topic, qos, unit_of_measurement, force_update, expire_after, icon, value_template, - json_attributes, availability_topic, payload_available, + json_attributes, unique_id: Optional[str], + availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" super().__init__(availability_topic, qos, payload_available, @@ -90,6 +97,7 @@ class MqttSensor(MqttAvailability, Entity): self._icon = icon self._expiration_trigger = None self._json_attributes = set(json_attributes) + self._unique_id = unique_id self._attributes = None @asyncio.coroutine @@ -174,6 +182,11 @@ class MqttSensor(MqttAvailability, Entity): """Return the state attributes.""" return self._attributes + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the icon.""" diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index b23d89e3057..88e74e11008 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -329,3 +329,24 @@ class TestSensorMQTT(unittest.TestCase): self.assertEqual('100', state.attributes.get('val')) self.assertEqual('100', state.state) + + def test_unique_id(self): + """Test unique id option only creates one sensor per unique_id.""" + assert setup_component(self.hass, sensor.DOMAIN, { + sensor.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + fire_mqtt_message(self.hass, 'test-topic', 'payload') + self.hass.block_till_done() + + assert len(self.hass.states.all()) == 1 From 40d7857f3b7a8b2b1522e4d1a7f59f7ac3617b06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 Apr 2018 23:04:50 -0400 Subject: [PATCH 320/924] Prepare entity component for config entries (#13730) * Prepare entity component for config entries * Return in time --- homeassistant/helpers/entity_component.py | 55 ++++++++++------------- homeassistant/helpers/entity_platform.py | 30 ++++++++++--- tests/common.py | 4 +- 3 files changed, 49 insertions(+), 40 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f086437c10d..6ff9b6f6571 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -40,16 +40,7 @@ class EntityComponent(object): self.config = None self._platforms = { - domain: EntityPlatform( - hass=hass, - logger=logger, - domain=domain, - platform_name=domain, - scan_interval=self.scan_interval, - parallel_updates=0, - entity_namespace=None, - async_entities_added_callback=self._async_update_group, - ) + domain: self._async_init_entity_platform(domain, None) } self.async_add_entities = self._platforms[domain].async_add_entities self.add_entities = self._platforms[domain].add_entities @@ -127,34 +118,19 @@ class EntityComponent(object): if platform is None: return - # Config > Platform > Component - scan_interval = ( - platform_config.get(CONF_SCAN_INTERVAL) or - getattr(platform, 'SCAN_INTERVAL', None) or self.scan_interval) - parallel_updates = getattr( - platform, 'PARALLEL_UPDATES', - int(not hasattr(platform, 'async_setup_platform'))) - + # Use config scan interval, fallback to platform if none set + scan_interval = platform_config.get( + CONF_SCAN_INTERVAL, getattr(platform, 'SCAN_INTERVAL', None)) entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE) key = (platform_type, scan_interval, entity_namespace) if key not in self._platforms: - entity_platform = self._platforms[key] = EntityPlatform( - hass=self.hass, - logger=self.logger, - domain=self.domain, - platform_name=platform_type, - scan_interval=scan_interval, - parallel_updates=parallel_updates, - entity_namespace=entity_namespace, - async_entities_added_callback=self._async_update_group, + self._platforms[key] = self._async_init_entity_platform( + platform_type, platform, scan_interval, entity_namespace ) - else: - entity_platform = self._platforms[key] - await entity_platform.async_setup( - platform, platform_config, discovery_info) + await self._platforms[key].async_setup(platform_config, discovery_info) @callback def _async_update_group(self): @@ -219,3 +195,20 @@ class EntityComponent(object): await self._async_reset() return conf + + def _async_init_entity_platform(self, platform_type, platform, + scan_interval=None, entity_namespace=None): + """Helper to initialize an entity platform.""" + if scan_interval is None: + scan_interval = self.scan_interval + + return EntityPlatform( + hass=self.hass, + logger=self.logger, + domain=self.domain, + platform_name=platform_type, + platform=platform, + scan_interval=scan_interval, + entity_namespace=entity_namespace, + async_entities_added_callback=self._async_update_group, + ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 501ab5057a3..3c6deaba94a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -20,8 +20,8 @@ PLATFORM_NOT_READY_RETRIES = 10 class EntityPlatform(object): """Manage the entities for a single platform.""" - def __init__(self, *, hass, logger, domain, platform_name, scan_interval, - parallel_updates, entity_namespace, + def __init__(self, *, hass, logger, domain, platform_name, platform, + scan_interval, entity_namespace, async_entities_added_callback): """Initialize the entity platform. @@ -38,8 +38,8 @@ class EntityPlatform(object): self.logger = logger self.domain = domain self.platform_name = platform_name + self.platform = platform self.scan_interval = scan_interval - self.parallel_updates = None self.entity_namespace = entity_namespace self.async_entities_added_callback = async_entities_added_callback self.entities = {} @@ -47,13 +47,30 @@ class EntityPlatform(object): self._async_unsub_polling = None self._process_updates = asyncio.Lock(loop=hass.loop) + # Platform is None for the EntityComponent "catch-all" EntityPlatform + # which powers entity_component.add_entities + if platform is None: + self.parallel_updates = None + return + + # Async platforms do all updates in parallel by default + if hasattr(platform, 'async_setup_platform'): + default_parallel_updates = 0 + else: + default_parallel_updates = 1 + + parallel_updates = getattr(platform, 'PARALLEL_UPDATES', + default_parallel_updates) + if parallel_updates: self.parallel_updates = asyncio.Semaphore( parallel_updates, loop=hass.loop) + else: + self.parallel_updates = None - async def async_setup(self, platform, platform_config, discovery_info=None, - tries=0): + async def async_setup(self, platform_config, discovery_info=None, tries=0): """Setup the platform.""" + platform = self.platform logger = self.logger hass = self.hass full_name = '{}.{}'.format(self.domain, self.platform_name) @@ -98,8 +115,7 @@ class EntityPlatform(object): 'Platform %s not ready yet. Retrying in %d seconds.', self.platform_name, wait_time) async_track_point_in_time( - hass, self.async_setup( - platform, platform_config, discovery_info, tries), + hass, self.async_setup(platform_config, discovery_info, tries), dt_util.utcnow() + timedelta(seconds=wait_time)) except asyncio.TimeoutError: logger.error( diff --git a/tests/common.py b/tests/common.py index bc84b3493a8..388898e7024 100644 --- a/tests/common.py +++ b/tests/common.py @@ -370,8 +370,8 @@ class MockEntityPlatform(entity_platform.EntityPlatform): logger=None, domain='test_domain', platform_name='test_platform', + platform=None, scan_interval=timedelta(seconds=15), - parallel_updates=0, entity_namespace=None, async_entities_added_callback=lambda: None ): @@ -381,8 +381,8 @@ class MockEntityPlatform(entity_platform.EntityPlatform): logger=logger, domain=domain, platform_name=platform_name, + platform=platform, scan_interval=scan_interval, - parallel_updates=parallel_updates, entity_namespace=entity_namespace, async_entities_added_callback=async_entities_added_callback, ) From ef16c53e4650f6001f7289ec07a18e1764c77b09 Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 8 Apr 2018 10:32:49 +0100 Subject: [PATCH 321/924] Check valid file on get_size (#13756) Addresses https://github.com/home-assistant/home-assistant/issues/13754 --- homeassistant/components/sensor/folder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/folder.py b/homeassistant/components/sensor/folder.py index a185cd1e825..2b5f3dd4309 100644 --- a/homeassistant/components/sensor/folder.py +++ b/homeassistant/components/sensor/folder.py @@ -38,7 +38,7 @@ def get_files_list(folder_path, filter_term): def get_size(files_list): """Return the sum of the size in bytes of files in the list.""" - size_list = [os.stat(f).st_size for f in files_list] + size_list = [os.stat(f).st_size for f in files_list if os.path.isfile(f)] return sum(size_list) From b01dceaff2c6188e69ee9706a9fa6c547a9f131a Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 8 Apr 2018 21:59:19 +0200 Subject: [PATCH 322/924] Qwikswitch sensors (#13622) --- .coveragerc | 4 +- homeassistant/components/light/qwikswitch.py | 4 +- homeassistant/components/qwikswitch.py | 116 +++++++++--------- homeassistant/components/sensor/qwikswitch.py | 54 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/sensor/test_qwikswitch.py | 90 ++++++++++++++ 8 files changed, 182 insertions(+), 92 deletions(-) create mode 100644 tests/components/sensor/test_qwikswitch.py diff --git a/.coveragerc b/.coveragerc index e9c69d137e2..6b1ca91a574 100644 --- a/.coveragerc +++ b/.coveragerc @@ -190,8 +190,8 @@ omit = homeassistant/components/pilight.py homeassistant/components/*/pilight.py - homeassistant/components/qwikswitch.py - homeassistant/components/*/qwikswitch.py + homeassistant/components/switch/qwikswitch.py + homeassistant/components/light/qwikswitch.py homeassistant/components/rachio.py homeassistant/components/*/rachio.py diff --git a/homeassistant/components/light/qwikswitch.py b/homeassistant/components/light/qwikswitch.py index 26741525b8f..528f4f73c53 100644 --- a/homeassistant/components/light/qwikswitch.py +++ b/homeassistant/components/light/qwikswitch.py @@ -27,9 +27,9 @@ class QSLight(QSToggleEntity, Light): @property def brightness(self): """Return the brightness of this light (0-255).""" - return self._qsusb[self.qsid, 1] if self._dim else None + return self.device.value if self.device.is_dimmer else None @property def supported_features(self): """Flag supported features.""" - return SUPPORT_BRIGHTNESS if self._dim else 0 + return SUPPORT_BRIGHTNESS if self.device.is_dimmer else 0 diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 708eff7cf11..36bd726fa2d 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.light import ATTR_BRIGHTNESS import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.6'] +REQUIREMENTS = ['pyqwikswitch==0.7'] _LOGGER = logging.getLogger(__name__) @@ -34,17 +34,48 @@ CONFIG_SCHEMA = vol.Schema({ vol.Coerce(str), vol.Optional(CONF_DIMMER_ADJUST, default=1): CV_DIM_VALUE, vol.Optional(CONF_BUTTON_EVENTS, default=[]): cv.ensure_list_csv, - vol.Optional(CONF_SENSORS, default={}): vol.Schema({cv.slug: str}), + vol.Optional(CONF_SENSORS, default=[]): vol.All( + cv.ensure_list, [vol.Schema({ + vol.Required('id'): str, + vol.Optional('channel', default=1): int, + vol.Required('name'): str, + vol.Required('type'): str, + })]), vol.Optional(CONF_SWITCHES, default=[]): vol.All( cv.ensure_list, [str]) })}, extra=vol.ALLOW_EXTRA) -class QSToggleEntity(Entity): - """Representation of a Qwikswitch Entity. +class QSEntity(Entity): + """Qwikswitch Entity base.""" - Implement base QS methods. Modeled around HA ToggleEntity[1] & should only - be used in a class that extends both QSToggleEntity *and* ToggleEntity. + def __init__(self, qsid, name): + """Initialize the QSEntity.""" + self._name = name + self.qsid = qsid + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def poll(self): + """QS sensors gets packets in update_packet.""" + return False + + def update_packet(self, packet): + """Receive update packet from QSUSB. Match dispather_send signature.""" + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Listen for updates from QSUSb via dispatcher.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + self.qsid, self.update_packet) + + +class QSToggleEntity(QSEntity): + """Representation of a Qwikswitch Toggle Entity. Implemented: - QSLight extends QSToggleEntity and Light[2] (ToggleEntity[1]) @@ -57,52 +88,28 @@ class QSToggleEntity(Entity): def __init__(self, qsid, qsusb): """Initialize the ToggleEntity.""" - from pyqwikswitch import (QS_NAME, QSDATA, QS_TYPE, QSType) - self.qsid = qsid - self._qsusb = qsusb.devices - dev = qsusb.devices[qsid] - self._dim = dev[QS_TYPE] == QSType.dimmer - self._name = dev[QSDATA][QS_NAME] - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the light.""" - return self._name + self.device = qsusb.devices[qsid] + super().__init__(qsid, self.device.name) @property def is_on(self): """Check if device is on (non-zero).""" - return self._qsusb[self.qsid, 1] > 0 + return self.device.value > 0 async def async_turn_on(self, **kwargs): """Turn the device on.""" new = kwargs.get(ATTR_BRIGHTNESS, 255) - self._qsusb.set_value(self.qsid, new) + self.hass.data[DOMAIN].devices.set_value(self.qsid, new) async def async_turn_off(self, **_): """Turn the device off.""" - self._qsusb.set_value(self.qsid, 0) - - def _update(self, _packet=None): - """Schedule an update - match dispather_send signature.""" - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Listen for updates from QSUSb via dispatcher.""" - self.hass.helpers.dispatcher.async_dispatcher_connect( - self.qsid, self._update) + self.hass.data[DOMAIN].devices.set_value(self.qsid, 0) async def async_setup(hass, config): """Qwiskswitch component setup.""" from pyqwikswitch.async_ import QSUsb - from pyqwikswitch import ( - CMD_BUTTONS, QS_CMD, QS_ID, QS_TYPE, QSType) + from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] @@ -112,8 +119,8 @@ async def async_setup(hass, config): url = config[DOMAIN][CONF_URL] dimmer_adjust = config[DOMAIN][CONF_DIMMER_ADJUST] - sensors = config[DOMAIN]['sensors'] - switches = config[DOMAIN]['switches'] + sensors = config[DOMAIN][CONF_SENSORS] + switches = config[DOMAIN][CONF_SWITCHES] def callback_value_changed(_qsd, qsid, _val): """Update entity values based on device change.""" @@ -131,17 +138,17 @@ async def async_setup(hass, config): hass.data[DOMAIN] = qsusb _new = {'switch': [], 'light': [], 'sensor': sensors} - for _id, item in qsusb.devices: - if _id in switches: - if item[QS_TYPE] != QSType.relay: + for qsid, dev in qsusb.devices.items(): + if qsid in switches: + if dev.qstype != QSType.relay: _LOGGER.warning( - "You specified a switch that is not a relay %s", _id) + "You specified a switch that is not a relay %s", qsid) continue - _new['switch'].append(_id) - elif item[QS_TYPE] in [QSType.relay, QSType.dimmer]: - _new['light'].append(_id) + _new['switch'].append(qsid) + elif dev.qstype in (QSType.relay, QSType.dimmer): + _new['light'].append(qsid) else: - _LOGGER.warning("Ignored unknown QSUSB device: %s", item) + _LOGGER.warning("Ignored unknown QSUSB device: %s", dev) continue # Load platforms @@ -149,24 +156,21 @@ async def async_setup(hass, config): if comp_conf: load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) - def callback_qs_listen(item): + def callback_qs_listen(qspacket): """Typically a button press or update signal.""" # If button pressed, fire a hass event - if QS_ID in item: - if item.get(QS_CMD, '') in cmd_buttons: + if QS_ID in qspacket: + if qspacket.get(QS_CMD, '') in cmd_buttons: hass.bus.async_fire( - 'qwikswitch.button.{}'.format(item[QS_ID]), item) + 'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket) return - # Private method due to bad __iter__ design in qsusb - # qsusb.devices returns a list of tuples - if item[QS_ID] not in \ - qsusb.devices._data: # pylint: disable=protected-access + if qspacket[QS_ID] not in qsusb.devices: # Not a standard device in, component can handle packet # i.e. sensors - _LOGGER.debug("Dispatch %s ((%s))", item[QS_ID], item) + _LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket) hass.helpers.dispatcher.async_dispatcher_send( - item[QS_ID], item) + qspacket[QS_ID], qspacket) # Update all ha_objects hass.async_add_job(qsusb.update_from_devices) diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py index 19b32e93670..98c67b7a21c 100644 --- a/homeassistant/components/sensor/qwikswitch.py +++ b/homeassistant/components/sensor/qwikswitch.py @@ -6,8 +6,7 @@ https://home-assistant.io/components/sensor.qwikswitch/ """ import logging -from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH -from homeassistant.helpers.entity import Entity +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH, QSEntity DEPENDENCIES = [QWIKSWITCH] @@ -15,55 +14,48 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, _, add_devices, discovery_info=None): - """Add lights from the main Qwikswitch component.""" + """Add sensor from the main Qwikswitch component.""" if discovery_info is None: return qsusb = hass.data[QWIKSWITCH] _LOGGER.debug("Setup qwikswitch.sensor %s, %s", qsusb, discovery_info) - devs = [QSSensor(name, qsid) - for name, qsid in discovery_info[QWIKSWITCH].items()] + devs = [QSSensor(sensor) for sensor in discovery_info[QWIKSWITCH]] add_devices(devs) -class QSSensor(Entity): +class QSSensor(QSEntity): """Sensor based on a Qwikswitch relay/dimmer module.""" - _val = {} + _val = None - def __init__(self, sensor_name, sensor_id): + def __init__(self, sensor): """Initialize the sensor.""" - self._name = sensor_name - self.qsid = sensor_id + from pyqwikswitch import SENSORS + + super().__init__(sensor['id'], sensor['name']) + self.channel = sensor['channel'] + self.sensor_type = sensor['type'] + + self._decode, self.unit = SENSORS[self.sensor_type] + if isinstance(self.unit, type): + self.unit = "{}:{}".format(self.sensor_type, self.channel) def update_packet(self, packet): """Receive update packet from QSUSB.""" - _LOGGER.debug("Update %s (%s): %s", self.entity_id, self.qsid, packet) - self._val = packet - self.async_schedule_update_ha_state() + val = self._decode(packet.get('data'), channel=self.channel) + _LOGGER.debug("Update %s (%s) decoded as %s: %s: %s", + self.entity_id, self.qsid, val, self.channel, packet) + if val is not None: + self._val = val + self.async_schedule_update_ha_state() @property def state(self): """Return the value of the sensor.""" - return self._val.get('data', 0) - - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - return self._val + return str(self._val) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return None - - @property - def poll(self): - """QS sensors gets packets in update_packet.""" - return False - - async def async_added_to_hass(self): - """Listen for updates from QSUSb via dispatcher.""" - # Part of Entity/ToggleEntity - self.hass.helpers.dispatcher.async_dispatcher_connect( - self.qsid, self.update_packet) + return self.unit diff --git a/requirements_all.txt b/requirements_all.txt index 532c723365b..a747b5c3090 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -885,7 +885,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.6 +pyqwikswitch==0.7 # homeassistant.components.rainbird pyrainbird==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce20ecfbfc6..484fd1c39f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -145,6 +145,9 @@ pymonoprice==0.3 # homeassistant.components.binary_sensor.nx584 pynx584==0.4 +# homeassistant.components.qwikswitch +pyqwikswitch==0.7 + # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky python-forecastio==1.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index d5bb2701e9b..708d9dbd30b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -73,6 +73,7 @@ TEST_REQUIREMENTS = ( 'pylitejet', 'pymonoprice', 'pynx584', + 'pyqwikswitch', 'python-forecastio', 'pyunifi', 'pywebpush', diff --git a/tests/components/sensor/test_qwikswitch.py b/tests/components/sensor/test_qwikswitch.py new file mode 100644 index 00000000000..d9799b8530e --- /dev/null +++ b/tests/components/sensor/test_qwikswitch.py @@ -0,0 +1,90 @@ +"""Test qwikswitch sensors.""" +import asyncio +import logging + +import pytest + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.bootstrap import async_setup_component +from tests.test_util.aiohttp import mock_aiohttp_client + + +_LOGGER = logging.getLogger(__name__) + + +class AiohttpClientMockResponseList(list): + """List that fires an event on empty pop, for aiohttp Mocker.""" + + def decode(self, _): + """Return next item from list.""" + try: + res = list.pop(self) + _LOGGER.debug("MockResponseList popped %s: %s", res, self) + return res + except IndexError: + _LOGGER.debug("MockResponseList empty") + return "" + + async def wait_till_empty(self, hass): + """Wait until empty.""" + while self: + await asyncio.sleep(1) + await hass.async_block_till_done() + await hass.async_block_till_done() + + +LISTEN = AiohttpClientMockResponseList() + + +@pytest.fixture +def aioclient_mock(): + """HTTP client listen and devices.""" + devices = """[ + {"id":"@000001","name":"Switch 1","type":"rel","val":"OFF", + "time":"1522777506","rssi":"51%"}, + {"id":"@000002","name":"Light 2","type":"rel","val":"ON", + "time":"1522777507","rssi":"45%"}, + {"id":"@000003","name":"Dim 3","type":"dim","val":"280c00", + "time":"1522777544","rssi":"62%"}]""" + + with mock_aiohttp_client() as mock_session: + mock_session.get("http://127.0.0.1:2020/&listen", content=LISTEN) + mock_session.get("http://127.0.0.1:2020/&device", text=devices) + yield mock_session + + +# @asyncio.coroutine +async def test_sensor_device(hass, aioclient_mock): + """Test a sensor device.""" + config = { + 'qwikswitch': { + 'sensors': { + 'name': 's1', + 'id': '@a00001', + 'channel': 1, + 'type': 'imod', + } + } + } + await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.s1') + assert state_obj + assert state_obj.state == 'None' + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + LISTEN.append( # Close + """{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""") + await hass.async_block_till_done() + state_obj = hass.states.get('sensor.s1') + assert state_obj.state == 'True' + + # Causes a 30second delay: can be uncommented when upstream library + # allows cancellation of asyncio.sleep(30) on failed packet ("") + # LISTEN.append( # Open + # """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""") + # await LISTEN.wait_till_empty(hass) + # state_obj = hass.states.get('sensor.s1') + # assert state_obj.state == 'False' From 70649dfe22c898a0efe6754465d6924b6cdef59e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 8 Apr 2018 22:00:47 +0200 Subject: [PATCH 323/924] Device type mapping introduced to avoid breaking change (#13765) --- homeassistant/components/light/yeelight.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index 7061c24aac6..d6d860cbd9e 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -24,6 +24,14 @@ REQUIREMENTS = ['yeelight==0.4.0'] _LOGGER = logging.getLogger(__name__) +LEGACY_DEVICE_TYPE_MAP = { + 'color1': 'rgb', + 'mono1': 'white', + 'strip1': 'strip', + 'bslamp1': 'bedside', + 'ceiling1': 'ceiling', +} + CONF_TRANSITION = 'transition' DEFAULT_TRANSITION = 350 @@ -122,8 +130,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if discovery_info is not None: _LOGGER.debug("Adding autodetected %s", discovery_info['hostname']) + device_type = discovery_info['device_type'] + device_type = LEGACY_DEVICE_TYPE_MAP.get(device_type, device_type) + # Not using hostname, as it seems to vary. - name = "yeelight_%s_%s" % (discovery_info['device_type'], + name = "yeelight_%s_%s" % (device_type, discovery_info['properties']['mac']) device = {'name': name, 'ipaddr': discovery_info['host']} From 8beb9c2b2890f829d51d91d97d0a639aab2e8d88 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Mon, 9 Apr 2018 06:12:46 +0200 Subject: [PATCH 324/924] Only flag media position as updated when it really has (#13737) --- homeassistant/components/media_player/squeezebox.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/squeezebox.py b/homeassistant/components/media_player/squeezebox.py index 86b4087ca81..371ad890364 100644 --- a/homeassistant/components/media_player/squeezebox.py +++ b/homeassistant/components/media_player/squeezebox.py @@ -266,6 +266,8 @@ class SqueezeBoxDevice(MediaPlayerDevice): if response is False: return + last_media_position = self.media_position + self._status = {} try: @@ -278,7 +280,11 @@ class SqueezeBoxDevice(MediaPlayerDevice): pass self._status.update(response) - self._last_update = utcnow() + + if self.media_position != last_media_position: + _LOGGER.debug('Media position updated for %s: %s', + self, self.media_position) + self._last_update = utcnow() @property def volume_level(self): From cb51553c2dd2bd6cc38beeb411f9b693f9ce4bac Mon Sep 17 00:00:00 2001 From: Yonsm Date: Mon, 9 Apr 2018 21:32:29 +0800 Subject: [PATCH 325/924] Support binary_sensor and device_tracker in HomeKit (#13735) * Support binary_sensor and device_tracker for HomeKit * Add test for get_accessory and binary sensor * Test service.display_name and char_detected.display_name * Split test to improve speed --- homeassistant/components/homekit/__init__.py | 5 ++ homeassistant/components/homekit/const.py | 23 ++++++++ .../components/homekit/type_sensors.py | 57 ++++++++++++++++++- .../homekit/test_get_accessories.py | 15 ++++- tests/components/homekit/test_type_sensors.py | 57 ++++++++++++++++++- 5 files changed, 152 insertions(+), 5 deletions(-) mode change 100644 => 100755 homeassistant/components/homekit/type_sensors.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 8a38c01026e..06258bcc97a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -92,6 +92,11 @@ def get_accessory(hass, state, aid, config): return TYPES['HumiditySensor'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'binary_sensor' or state.domain == 'device_tracker': + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'BinarySensor') + return TYPES['BinarySensor'](hass, state.entity_id, + state.name, aid=aid) + elif state.domain == 'cover': # Only add covers that support set_cover_position features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 18d02a89e18..7136852c409 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -35,11 +35,18 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' +SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' +SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' +SERV_CONTACT_SENSOR = 'ContactSensor' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, # StatusLowBattery, Name +SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_MOTION_SENSOR = 'MotionSensor' +SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_SECURITY_SYSTEM = 'SecuritySystem' +SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' @@ -48,7 +55,10 @@ SERV_WINDOW_COVERING = 'WindowCovering' # #### Characteristics #### CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' +CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' +CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' @@ -57,13 +67,17 @@ CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' +CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' +CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' +CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' @@ -72,3 +86,12 @@ CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} + +# #### Device Class #### +DEVICE_CLASS_CO2 = 'co2' +DEVICE_CLASS_GAS = 'gas' +DEVICE_CLASS_MOISTURE = 'moisture' +DEVICE_CLASS_MOTION = 'motion' +DEVICE_CLASS_OCCUPANCY = 'occupancy' +DEVICE_CLASS_OPENING = 'opening' +DEVICE_CLASS_SMOKE = 'smoke' diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py old mode 100644 new mode 100755 index 393962eac21..b25eb784d6b --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -2,19 +2,40 @@ import logging from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, + ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES from .accessories import HomeAccessory, add_preload_service from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS) + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, + DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, + DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, + CHAR_CARBON_MONOXIDE_DETECTED, + DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, + DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, + DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, + DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, + DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import convert_to_float, temperature_to_homekit _LOGGER = logging.getLogger(__name__) +BINARY_SENSOR_SERVICE_MAP = { + DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, + CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, + CHAR_CARBON_MONOXIDE_DETECTED), + DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), + DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), + DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), + DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), + DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)} + + @TYPES.register('TemperatureSensor') class TemperatureSensor(HomeAccessory): """Generate a TemperatureSensor accessory for a temperature sensor. @@ -75,3 +96,35 @@ class HumiditySensor(HomeAccessory): self.char_humidity.set_value(humidity) _LOGGER.debug('%s: Percent set to %d%%', self.entity_id, humidity) + + +@TYPES.register('BinarySensor') +class BinarySensor(HomeAccessory): + """Generate a BinarySensor accessory as binary sensor.""" + + def __init__(self, hass, entity_id, name, **kwargs): + """Initialize a BinarySensor accessory object.""" + super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) + + self.hass = hass + self.entity_id = entity_id + + device_class = hass.states.get(entity_id).attributes \ + .get(ATTR_DEVICE_CLASS) + service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \ + if device_class in BINARY_SENSOR_SERVICE_MAP \ + else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] + + service = add_preload_service(self, service_char[0]) + self.char_detected = service.get_characteristic(service_char[1]) + self.char_detected.value = 0 + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update accessory after state change.""" + if new_state is None: + return + + state = new_state.state + detected = (state == STATE_ON) or (state == STATE_HOME) + self.char_detected.set_value(detected) + _LOGGER.debug('%s: Set to %d', self.entity_id, detected) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e29ed85b5fc..e323431ac3f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,7 +9,7 @@ from homeassistant.components.climate import ( from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS) _LOGGER = logging.getLogger(__name__) @@ -63,6 +63,19 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: '%'}) get_accessory(None, state, 2, {}) + def test_binary_sensor(self): + """Test binary sensor with opening class.""" + with patch.dict(TYPES, {'BinarySensor': self.mock_type}): + state = State('binary_sensor.opening', 'on', + {ATTR_DEVICE_CLASS: 'opening'}) + get_accessory(None, state, 2, {}) + + def test_device_tracker(self): + """Test binary sensor with opening class.""" + with patch.dict(TYPES, {'BinarySensor': self.mock_type}): + state = State('device_tracker.someone', 'not_home', {}) + get_accessory(None, state, 2, {}) + def test_cover_set_position(self): """Test cover with support for set_cover_position.""" with patch.dict(TYPES, {'WindowCovering': self.mock_type}): diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index c04c250613d..a6e178bb226 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,9 +3,10 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor) + TemperatureSensor, HumiditySensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, + STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant @@ -68,3 +69,55 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) self.hass.block_till_done() self.assertEqual(acc.char_humidity.value, 20) + + def test_binary(self): + """Test if accessory is updated after state change.""" + entity_id = 'binary_sensor.opening' + + self.hass.states.set(entity_id, STATE_UNKNOWN, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + + acc = BinarySensor(self.hass, entity_id, 'Window Opening', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.set(entity_id, STATE_ON, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 1) + + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.set(entity_id, STATE_HOME, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 1) + + self.hass.states.set(entity_id, STATE_NOT_HOME, + {ATTR_DEVICE_CLASS: "opening"}) + self.hass.block_till_done() + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.remove(entity_id) + self.hass.block_till_done() + + def test_binary_device_classes(self): + """Test if services and characteristics are assigned correctly.""" + entity_id = 'binary_sensor.demo' + + for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): + self.hass.states.set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: device_class}) + self.hass.block_till_done() + + acc = BinarySensor(self.hass, entity_id, 'Binary Sensor', aid=2) + self.assertEqual(acc.get_service(service).display_name, service) + self.assertEqual(acc.char_detected.display_name, char) From 73de74941127d4d9028fc2428bb9846cc167448b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Apr 2018 10:09:08 -0400 Subject: [PATCH 326/924] Use config entry to setup platforms (#13752) * Use config entry to setup platforms * Rename to async_forward_entry * Add tests * Catch if platform not exists for entry --- homeassistant/components/hue/bridge.py | 16 ++--- homeassistant/components/light/__init__.py | 7 +- homeassistant/components/light/hue.py | 14 ++-- homeassistant/config_entries.py | 29 ++++++++- homeassistant/helpers/entity_component.py | 20 ++++++ homeassistant/helpers/entity_platform.py | 75 ++++++++++++++++------ tests/common.py | 14 +++- tests/components/hue/test_bridge.py | 7 +- tests/components/light/test_hue.py | 7 +- tests/helpers/test_entity_component.py | 45 ++++++++++++- tests/helpers/test_entity_platform.py | 46 ++++++++++++- tests/test_config_entries.py | 36 +++++++++++ 12 files changed, 271 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 8093c84971e..4693a2f4dbe 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -39,18 +39,17 @@ class HueBridge(object): async def async_setup(self, tries=0): """Set up a phue bridge based on host parameter.""" host = self.host + hass = self.hass try: self.api = await get_bridge( - self.hass, host, - self.config_entry.data['username'] - ) + hass, host, self.config_entry.data['username']) except AuthenticationRequired: # usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new # linking procedure. When linking succeeds, it will remove the # old config entry. - self.hass.async_add_job(self.hass.config_entries.flow.async_init( + hass.async_add_job(hass.config_entries.flow.async_init( DOMAIN, source='import', data={ 'host': host, } @@ -69,7 +68,7 @@ class HueBridge(object): self.config_entry.state = config_entries.ENTRY_STATE_LOADED # Unhandled edge case: cancel this if we discover bridge on new IP - self.hass.helpers.event.async_call_later(retry_delay, retry_setup) + hass.helpers.event.async_call_later(retry_delay, retry_setup) return False @@ -78,11 +77,10 @@ class HueBridge(object): host) return False - self.hass.async_add_job( - self.hass.helpers.discovery.async_load_platform( - 'light', DOMAIN, {'host': host})) + hass.async_add_job(hass.config_entries.async_forward_entry( + self.config_entry, 'light')) - self.hass.services.async_register( + hass.services.async_register( DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, schema=SCENE_SCHEMA) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 39d3203795e..d497c8f9880 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -334,7 +334,7 @@ class SetIntentHandler(intent.IntentHandler): async def async_setup(hass, config): """Expose light control via state machine and services.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS) await component.async_setup(config) @@ -388,6 +388,11 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + class Profiles: """Representation of available color profiles.""" diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 1701b886b68..6eb8de99c99 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -49,11 +49,17 @@ GROUP_MIN_API_VERSION = (1, 13, 0) async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the Hue lights.""" - if discovery_info is None: - return + """Old way of setting up Hue lights. - bridge = hass.data[hue.DOMAIN][discovery_info['host']] + Can only be called when a user accidentally mentions hue platform in their + config. But even in that case it would have been ignored. + """ + pass + + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the Hue lights from a config entry.""" + bridge = hass.data[hue.DOMAIN][config_entry.data['host']] cur_lights = {} cur_groups = {} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 69491af1aad..fc781bd62c8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -187,13 +187,17 @@ class ConfigEntry: if not isinstance(result, bool): _LOGGER.error('%s.async_config_entry did not return boolean', - self.domain) + component.DOMAIN) result = False except Exception: # pylint: disable=broad-except _LOGGER.exception('Error setting up entry %s for %s', - self.title, self.domain) + self.title, component.DOMAIN) result = False + # Only store setup result as state if it was not forwarded. + if self.domain != component.DOMAIN: + return + if result: self.state = ENTRY_STATE_LOADED else: @@ -322,6 +326,27 @@ class ConfigEntries: entries = await self.hass.async_add_job(load_json, path) self._entries = [ConfigEntry(**entry) for entry in entries] + async def async_forward_entry(self, entry, component): + """Forward the setup of an entry to a different component. + + By default an entry is setup with the component it belongs to. If that + component also has related platforms, the component will have to + forward the entry to be setup by that component. + + You don't want to await this coroutine if it is called as part of the + setup of a component, because it can cause a deadlock. + """ + # Setup Component if not set up yet + if component not in self.hass.config.components: + result = await async_setup_component( + self.hass, component, self._hass_config) + + if not result: + return False + + await entry.async_setup( + self.hass, component=getattr(self.hass.components, component)) + async def _async_add_entry(self, entry): """Add an entry.""" self._entries.append(entry) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 6ff9b6f6571..265464d548d 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -93,6 +93,26 @@ class EntityComponent(object): discovery.async_listen_platform( self.hass, self.domain, component_platform_discovered) + async def async_setup_entry(self, config_entry): + """Setup a config entry.""" + platform_type = config_entry.domain + platform = await async_prepare_setup_platform( + self.hass, self.config, self.domain, platform_type) + + if platform is None: + return False + + key = config_entry.entry_id + + if key in self._platforms: + raise ValueError('Config entry has already been setup!') + + self._platforms[key] = self._async_init_entity_platform( + platform_type, platform + ) + + return await self._platforms[key].async_setup_entry(config_entry) + @callback def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3c6deaba94a..ba8df7e01d8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,15 +1,13 @@ """Class to manage the entities for a single platform.""" import asyncio -from datetime import timedelta from homeassistant.const import DEVICE_DEFAULT_NAME from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import ( run_callback_threadsafe, run_coroutine_threadsafe) -import homeassistant.util.dt as dt_util -from .event import async_track_time_interval, async_track_point_in_time +from .event import async_track_time_interval, async_call_later from .entity_registry import async_get_registry SLOW_SETUP_WARNING = 10 @@ -42,6 +40,7 @@ class EntityPlatform(object): self.scan_interval = scan_interval self.entity_namespace = entity_namespace self.async_entities_added_callback = async_entities_added_callback + self.config_entry = None self.entities = {} self._tasks = [] self._async_unsub_polling = None @@ -68,9 +67,47 @@ class EntityPlatform(object): else: self.parallel_updates = None - async def async_setup(self, platform_config, discovery_info=None, tries=0): - """Setup the platform.""" + async def async_setup(self, platform_config, discovery_info=None): + """Setup the platform from a config file.""" platform = self.platform + hass = self.hass + + @callback + def async_create_setup_task(): + """Get task to setup platform.""" + if getattr(platform, 'async_setup_platform', None): + return platform.async_setup_platform( + hass, platform_config, + self._async_schedule_add_entities, discovery_info + ) + + # This should not be replaced with hass.async_add_job because + # we don't want to track this task in case it blocks startup. + return hass.loop.run_in_executor( + None, platform.setup_platform, hass, platform_config, + self._schedule_add_entities, discovery_info + ) + await self._async_setup_platform(async_create_setup_task) + + async def async_setup_entry(self, config_entry): + """Setup the platform from a config entry.""" + # Store it so that we can save config entry ID in entity registry + self.config_entry = config_entry + platform = self.platform + + @callback + def async_create_setup_task(): + """Get task to setup platform.""" + return platform.async_setup_entry( + self.hass, config_entry, self._async_schedule_add_entities) + + return await self._async_setup_platform(async_create_setup_task) + + async def _async_setup_platform(self, async_create_setup_task, tries=0): + """Helper to setup a platform via config file or config entry. + + async_create_setup_task creates a coroutine that sets up platform. + """ logger = self.logger hass = self.hass full_name = '{}.{}'.format(self.domain, self.platform_name) @@ -82,18 +119,8 @@ class EntityPlatform(object): self.platform_name, SLOW_SETUP_WARNING) try: - if getattr(platform, 'async_setup_platform', None): - task = platform.async_setup_platform( - hass, platform_config, - self._async_schedule_add_entities, discovery_info - ) - else: - # This should not be replaced with hass.async_add_job because - # we don't want to track this task in case it blocks startup. - task = hass.loop.run_in_executor( - None, platform.setup_platform, hass, platform_config, - self._schedule_add_entities, discovery_info - ) + task = async_create_setup_task() + await asyncio.wait_for( asyncio.shield(task, loop=hass.loop), SLOW_SETUP_MAX_WAIT, loop=hass.loop) @@ -108,23 +135,31 @@ class EntityPlatform(object): pending, loop=self.hass.loop) hass.config.components.add(full_name) + return True except PlatformNotReady: tries += 1 wait_time = min(tries, 6) * 30 logger.warning( 'Platform %s not ready yet. Retrying in %d seconds.', self.platform_name, wait_time) - async_track_point_in_time( - hass, self.async_setup(platform_config, discovery_info, tries), - dt_util.utcnow() + timedelta(seconds=wait_time)) + + async def setup_again(now): + """Run setup again.""" + await self._async_setup_platform( + async_create_setup_task, tries) + + async_call_later(hass, wait_time, setup_again) + return False except asyncio.TimeoutError: logger.error( "Setup of platform %s is taking longer than %s seconds." " Startup will proceed without waiting any longer.", self.platform_name, SLOW_SETUP_MAX_WAIT) + return False except Exception: # pylint: disable=broad-except logger.exception( "Error while setting up platform %s", self.platform_name) + return False finally: warn_task.cancel() diff --git a/tests/common.py b/tests/common.py index 388898e7024..54c214da4e9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -344,7 +344,8 @@ class MockPlatform(object): # pylint: disable=invalid-name def __init__(self, setup_platform=None, dependencies=None, - platform_schema=None, async_setup_platform=None): + platform_schema=None, async_setup_platform=None, + async_setup_entry=None): """Initialize the platform.""" self.DEPENDENCIES = dependencies or [] @@ -358,6 +359,9 @@ class MockPlatform(object): if async_setup_platform is not None: self.async_setup_platform = async_setup_platform + if async_setup_entry is not None: + self.async_setup_entry = async_setup_entry + if setup_platform is None and async_setup_platform is None: self.async_setup_platform = mock_coro_func() @@ -376,6 +380,14 @@ class MockEntityPlatform(entity_platform.EntityPlatform): async_entities_added_callback=lambda: None ): """Initialize a mock entity platform.""" + if logger is None: + logger = logging.getLogger('homeassistant.helpers.entity_platform') + + # Otherwise the constructor will blow up. + if (isinstance(platform, Mock) and + isinstance(platform.PARALLEL_UPDATES, Mock)): + platform.PARALLEL_UPDATES = 0 + super().__init__( hass=hass, logger=logger, diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 0845aa2f077..1f53d5aac14 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -18,10 +18,9 @@ async def test_bridge_setup(): assert await hue_bridge.async_setup() is True assert hue_bridge.api is api - assert len(hass.helpers.discovery.async_load_platform.mock_calls) == 1 - assert hass.helpers.discovery.async_load_platform.mock_calls[0][1][2] == { - 'host': '1.2.3.4' - } + assert len(hass.config_entries.async_forward_entry.mock_calls) == 1 + assert hass.config_entries.async_forward_entry.mock_calls[0][1] == \ + (entry, 'light') async def test_bridge_setup_invalid_username(): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 7b6c3a21a79..dee27adfe34 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -9,6 +9,7 @@ from aiohue.lights import Lights from aiohue.groups import Groups import pytest +from homeassistant import config_entries from homeassistant.components import hue import homeassistant.components.light.hue as hue_light from homeassistant.util import color @@ -196,9 +197,11 @@ async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) hass.data[hue.DOMAIN] = {'mock-host': mock_bridge} - await hass.helpers.discovery.async_load_platform('light', 'hue', { + config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', { 'host': 'mock-host' - }) + }, 'test') + await hass.config_entries.async_forward_entry(config_entry, 'light') + # To flush out the service call to update the group await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index d8dac11f6a0..f53b69274ef 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -7,6 +7,8 @@ import unittest from unittest.mock import patch, Mock from datetime import timedelta +import pytest + import homeassistant.core as ha import homeassistant.loader as loader from homeassistant.exceptions import PlatformNotReady @@ -19,7 +21,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, MockPlatform, MockModule, mock_coro, - async_fire_time_changed, MockEntity) + async_fire_time_changed, MockEntity, MockConfigEntry) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -333,3 +335,44 @@ def test_setup_dependencies_platform(hass): assert 'test_component' in hass.config.components assert 'test_component2' in hass.config.components assert 'test_domain.test_component' in hass.config.components + + +async def test_setup_entry(hass): + """Test setup entry calls async_setup_entry on platform.""" + mock_setup_entry = Mock(return_value=mock_coro(True)) + loader.set_component( + 'test_domain.entry_domain', + MockPlatform(async_setup_entry=mock_setup_entry)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='entry_domain') + + assert await component.async_setup_entry(entry) + assert len(mock_setup_entry.mock_calls) == 1 + p_hass, p_entry, p_add_entities = mock_setup_entry.mock_calls[0][1] + assert p_hass is hass + assert p_entry is entry + + +async def test_setup_entry_platform_not_exist(hass): + """Test setup entry fails if platform doesnt exist.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='non_existing') + + assert (await component.async_setup_entry(entry)) is False + + +async def test_setup_entry_fails_duplicate(hass): + """Test we don't allow setting up a config entry twice.""" + mock_setup_entry = Mock(return_value=mock_coro(True)) + loader.set_component( + 'test_domain.entry_domain', + MockPlatform(async_setup_entry=mock_setup_entry)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='entry_domain') + + assert await component.async_setup_entry(entry) + + with pytest.raises(ValueError): + await component.async_setup_entry(entry) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 8c085e4abb1..a8394ff6a49 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -5,6 +5,7 @@ import unittest from unittest.mock import patch, Mock, MagicMock from datetime import timedelta +from homeassistant.exceptions import PlatformNotReady import homeassistant.loader as loader from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import ( @@ -15,7 +16,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry, - MockEntity, MockEntityPlatform) + MockEntity, MockEntityPlatform, MockConfigEntry, mock_coro) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -511,3 +512,46 @@ async def test_entity_registry_updates(hass): state = hass.states.get('test_domain.world') assert state.name == 'after update' + + +async def test_setup_entry(hass): + """Test we can setup an entry.""" + async_setup_entry = Mock(return_value=mock_coro(True)) + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry() + entity_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + assert await entity_platform.async_setup_entry(config_entry) + + full_name = '{}.{}'.format(entity_platform.domain, config_entry.domain) + assert full_name in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + + +async def test_setup_entry_platform_not_ready(hass, caplog): + """Test when an entry is not ready yet.""" + async_setup_entry = Mock(side_effect=PlatformNotReady) + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + with patch.object(entity_platform, 'async_call_later') as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + full_name = '{}.{}'.format(ent_platform.domain, config_entry.domain) + assert full_name not in hass.config.components + assert len(async_setup_entry.mock_calls) == 1 + assert 'Platform test not ready yet' in caplog.text + assert len(mock_call_later.mock_calls) == 1 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 5b1ec3b8ec0..8bbd79a7ac7 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -389,3 +389,39 @@ def test_discovery_init_flow(manager): assert entry.title == 'hello' assert entry.data == data assert entry.source == config_entries.SOURCE_DISCOVERY + + +async def test_forward_entry_sets_up_component(hass): + """Test we setup the component entry is forwarded to.""" + entry = MockConfigEntry(domain='original') + + mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) + loader.set_component( + 'original', + MockModule('original', async_setup_entry=mock_original_setup_entry)) + + mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) + loader.set_component( + 'forwarded', + MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry)) + + await hass.config_entries.async_forward_entry(entry, 'forwarded') + assert len(mock_original_setup_entry.mock_calls) == 0 + assert len(mock_forwarded_setup_entry.mock_calls) == 1 + + +async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): + """Test we do not setup entry if component setup fails.""" + entry = MockConfigEntry(domain='original') + + mock_setup = MagicMock(return_value=mock_coro(False)) + mock_setup_entry = MagicMock() + loader.set_component('forwarded', MockModule( + 'forwarded', + async_setup=mock_setup, + async_setup_entry=mock_setup_entry, + )) + + await hass.config_entries.async_forward_entry(entry, 'forwarded') + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 From c61611d2b4b0f0fd905cafd2ef629d27c4cb97f8 Mon Sep 17 00:00:00 2001 From: Phil Kates Date: Mon, 9 Apr 2018 07:23:49 -0700 Subject: [PATCH 327/924] Add Homekit locks support (#13625) * homekit: Add locks support * Improved upgradeability --- homeassistant/components/homekit/__init__.py | 7 +- homeassistant/components/homekit/const.py | 5 ++ .../components/homekit/type_locks.py | 77 +++++++++++++++++++ tests/components/homekit/test_type_locks.py | 77 +++++++++++++++++++ 4 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/homekit/type_locks.py create mode 100644 tests/components/homekit/test_type_locks.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 06258bcc97a..22c74faf5f0 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -127,6 +127,9 @@ def get_accessory(hass, state, aid, config): _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light') return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'lock': + return TYPES['Lock'](hass, state.entity_id, state.name, aid=aid) + elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean' or state.domain == 'script': _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') @@ -186,8 +189,8 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_lights, type_security_systems, type_sensors, - type_switches, type_thermostats) + type_covers, type_lights, type_locks, type_security_systems, + type_sensors, type_switches, type_thermostats) for state in self._hass.states.all(): self.add_bridge_accessory(state) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 7136852c409..e5a4c80a430 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -27,6 +27,7 @@ MANUFACTURER = 'HomeAssistant' # #### Categories #### CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' CATEGORY_LIGHT = 'LIGHTBULB' +CATEGORY_LOCK = 'DOOR_LOCK' CATEGORY_SENSOR = 'SENSOR' CATEGORY_SWITCH = 'SWITCH' CATEGORY_THERMOSTAT = 'THERMOSTAT' @@ -43,6 +44,7 @@ SERV_HUMIDITY_SENSOR = 'HumiditySensor' # StatusLowBattery, Name SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' SERV_SECURITY_SYSTEM = 'SecuritySystem' @@ -68,6 +70,9 @@ CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LEAK_DETECTED = 'LeakDetected' +CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' +CHAR_LOCK_TARGET_STATE = 'LockTargetState' +CHAR_LINK_QUALITY = 'LinkQuality' CHAR_MANUFACTURER = 'Manufacturer' CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py new file mode 100644 index 00000000000..9df0c101eff --- /dev/null +++ b/homeassistant/components/homekit/type_locks.py @@ -0,0 +1,77 @@ +"""Class to hold all lock accessories.""" +import logging + +from homeassistant.components.lock import ( + ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + +from . import TYPES +from .accessories import HomeAccessory, add_preload_service +from .const import ( + CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) + +_LOGGER = logging.getLogger(__name__) + +HASS_TO_HOMEKIT = {STATE_UNLOCKED: 0, + STATE_LOCKED: 1, + # value 2 is Jammed which hass doesn't have a state for + STATE_UNKNOWN: 3} +HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} +STATE_TO_SERVICE = {STATE_LOCKED: 'lock', + STATE_UNLOCKED: 'unlock'} + + +@TYPES.register('Lock') +class Lock(HomeAccessory): + """Generate a Lock accessory for a lock entity. + + The lock entity must support: unlock and lock. + """ + + def __init__(self, hass, entity_id, name, **kwargs): + """Initialize a Lock accessory object.""" + super().__init__(name, entity_id, CATEGORY_LOCK, **kwargs) + + self.hass = hass + self.entity_id = entity_id + + self.flag_target_state = False + + serv_lock_mechanism = add_preload_service(self, SERV_LOCK) + self.char_current_state = serv_lock_mechanism. \ + get_characteristic(CHAR_LOCK_CURRENT_STATE) + self.char_target_state = serv_lock_mechanism. \ + get_characteristic(CHAR_LOCK_TARGET_STATE) + + self.char_current_state.value = HASS_TO_HOMEKIT[STATE_UNKNOWN] + self.char_target_state.value = HASS_TO_HOMEKIT[STATE_LOCKED] + + self.char_target_state.setter_callback = self.set_state + + def set_state(self, value): + """Set lock state to value if call came from HomeKit.""" + _LOGGER.debug("%s: Set state to %d", self.entity_id, value) + self.flag_target_state = True + + hass_value = HOMEKIT_TO_HASS.get(value) + service = STATE_TO_SERVICE[hass_value] + + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call('lock', service, params) + + def update_state(self, entity_id=None, old_state=None, new_state=None): + """Update lock after state changed.""" + if new_state is None: + return + + hass_state = new_state.state + if hass_state in HASS_TO_HOMEKIT: + current_lock_state = HASS_TO_HOMEKIT[hass_state] + self.char_current_state.set_value(current_lock_state) + _LOGGER.debug('%s: Updated current state to %s (%d)', + self.entity_id, hass_state, current_lock_state) + + # LockTargetState only supports locked and unlocked + if hass_state in (STATE_LOCKED, STATE_UNLOCKED): + if not self.flag_target_state: + self.char_target_state.set_value(current_lock_state) + self.flag_target_state = False diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py new file mode 100644 index 00000000000..d19bcdf3ec5 --- /dev/null +++ b/tests/components/homekit/test_type_locks.py @@ -0,0 +1,77 @@ +"""Test different accessory types: Locks.""" +import unittest + +from homeassistant.core import callback +from homeassistant.components.homekit.type_locks import Lock +from homeassistant.const import ( + STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED, + ATTR_SERVICE, EVENT_CALL_SERVICE) + +from tests.common import get_test_home_assistant + + +class TestHomekitSensors(unittest.TestCase): + """Test class for all accessory types regarding covers.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.events = [] + + @callback + def record_event(event): + """Track called event.""" + self.events.append(event) + + self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + def test_lock_unlock(self): + """Test if accessory and HA are updated accordingly.""" + kitchen_lock = 'lock.kitchen_door' + + acc = Lock(self.hass, kitchen_lock, 'Lock', aid=2) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 6) # DoorLock + + self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_target_state.value, 1) + + self.hass.states.set(kitchen_lock, STATE_LOCKED) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 1) + self.assertEqual(acc.char_target_state.value, 1) + + self.hass.states.set(kitchen_lock, STATE_UNLOCKED) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 0) + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.set(kitchen_lock, STATE_UNKNOWN) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_target_state.value, 0) + + # Set from HomeKit + acc.char_target_state.client_update_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'lock') + self.assertEqual(acc.char_target_state.value, 1) + + acc.char_target_state.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'unlock') + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.remove(kitchen_lock) + self.hass.block_till_done() From e593117ab6f224c8ecad9c1475610b80f1ecfc64 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Mon, 9 Apr 2018 11:46:27 -0400 Subject: [PATCH 328/924] Snips sounds (#13746) * Added feedback sound configuration * Added feedback sound configuration * Cleaned up feedback off * Cleaned up whitespace * Moved feedback pus to helper funx * Async * Used async_mock_service for tests * Lint --- homeassistant/components/snips.py | 74 +++++++-- tests/components/test_snips.py | 263 ++++++++++++++++++++++-------- 2 files changed, 256 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index d085b1279cb..812906e7be9 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -4,13 +4,13 @@ Support for Snips on-device ASR and NLU. For more details about this component, please refer to the documentation at https://home-assistant.io/components/snips/ """ -import asyncio import json import logging from datetime import timedelta import voluptuous as vol +from homeassistant.core import callback from homeassistant.helpers import intent, config_validation as cv import homeassistant.components.mqtt as mqtt @@ -19,11 +19,18 @@ DEPENDENCIES = ['mqtt'] CONF_INTENTS = 'intents' CONF_ACTION = 'action' +CONF_FEEDBACK = 'feedback_sounds' +CONF_PROBABILITY = 'probability_threshold' +CONF_SITE_IDS = 'site_ids' SERVICE_SAY = 'say' SERVICE_SAY_ACTION = 'say_action' +SERVICE_FEEDBACK_ON = 'feedback_on' +SERVICE_FEEDBACK_OFF = 'feedback_off' INTENT_TOPIC = 'hermes/intent/#' +FEEDBACK_ON_TOPIC = 'hermes/feedback/sound/toggleOn' +FEEDBACK_OFF_TOPIC = 'hermes/feedback/sound/toggleOff' ATTR_TEXT = 'text' ATTR_SITE_ID = 'site_id' @@ -34,7 +41,12 @@ ATTR_INTENT_FILTER = 'intent_filter' _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: {} + DOMAIN: vol.Schema({ + vol.Optional(CONF_FEEDBACK): cv.boolean, + vol.Optional(CONF_PROBABILITY, default=0): vol.Coerce(float), + vol.Optional(CONF_SITE_IDS, default=['default']): + vol.All(cv.ensure_list, [cv.string]), + }), }, extra=vol.ALLOW_EXTRA) INTENT_SCHEMA = vol.Schema({ @@ -57,7 +69,6 @@ SERVICE_SCHEMA_SAY = vol.Schema({ vol.Optional(ATTR_SITE_ID, default='default'): str, vol.Optional(ATTR_CUSTOM_DATA, default=''): str }) - SERVICE_SCHEMA_SAY_ACTION = vol.Schema({ vol.Required(ATTR_TEXT): str, vol.Optional(ATTR_SITE_ID, default='default'): str, @@ -65,13 +76,31 @@ SERVICE_SCHEMA_SAY_ACTION = vol.Schema({ vol.Optional(ATTR_CAN_BE_ENQUEUED, default=True): cv.boolean, vol.Optional(ATTR_INTENT_FILTER): vol.All(cv.ensure_list), }) +SERVICE_SCHEMA_FEEDBACK = vol.Schema({ + vol.Optional(ATTR_SITE_ID, default='default'): str +}) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Activate Snips component.""" - @asyncio.coroutine - def message_received(topic, payload, qos): + @callback + def async_set_feedback(site_ids, state): + """Set Feedback sound state.""" + site_ids = (site_ids if site_ids + else config[DOMAIN].get(CONF_SITE_IDS)) + topic = (FEEDBACK_ON_TOPIC if state + else FEEDBACK_OFF_TOPIC) + for site_id in site_ids: + payload = json.dumps({'siteId': site_id}) + hass.components.mqtt.async_publish( + FEEDBACK_ON_TOPIC, None, qos=0, retain=False) + hass.components.mqtt.async_publish( + topic, payload, qos=int(state), retain=state) + + if CONF_FEEDBACK in config[DOMAIN]: + async_set_feedback(None, config[DOMAIN][CONF_FEEDBACK]) + + async def message_received(topic, payload, qos): """Handle new messages on MQTT.""" _LOGGER.debug("New intent: %s", payload) @@ -81,6 +110,13 @@ def async_setup(hass, config): _LOGGER.error('Received invalid JSON: %s', payload) return + if (request['intent']['probability'] + < config[DOMAIN].get(CONF_PROBABILITY)): + _LOGGER.warning("Intent below probaility threshold %s < %s", + request['intent']['probability'], + config[DOMAIN].get(CONF_PROBABILITY)) + return + try: request = INTENT_SCHEMA(request) except vol.Invalid as err: @@ -97,7 +133,7 @@ def async_setup(hass, config): slots[slot['slotName']] = {'value': resolve_slot_values(slot)} try: - intent_response = yield from intent.async_handle( + intent_response = await intent.async_handle( hass, DOMAIN, intent_type, slots, request['input']) if 'plain' in intent_response.speech: snips_response = intent_response.speech['plain']['speech'] @@ -115,11 +151,10 @@ def async_setup(hass, config): mqtt.async_publish(hass, 'hermes/dialogueManager/endSession', json.dumps(notification)) - yield from hass.components.mqtt.async_subscribe( + await hass.components.mqtt.async_subscribe( INTENT_TOPIC, message_received) - @asyncio.coroutine - def snips_say(call): + async def snips_say(call): """Send a Snips notification message.""" notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), @@ -129,8 +164,7 @@ def async_setup(hass, config): json.dumps(notification)) return - @asyncio.coroutine - def snips_say_action(call): + async def snips_say_action(call): """Send a Snips action message.""" notification = {'siteId': call.data.get(ATTR_SITE_ID, 'default'), 'customData': call.data.get(ATTR_CUSTOM_DATA, ''), @@ -144,12 +178,26 @@ def async_setup(hass, config): json.dumps(notification)) return + async def feedback_on(call): + """Turn feedback sounds on.""" + async_set_feedback(call.data.get(ATTR_SITE_ID), True) + + async def feedback_off(call): + """Turn feedback sounds off.""" + async_set_feedback(call.data.get(ATTR_SITE_ID), False) + hass.services.async_register( DOMAIN, SERVICE_SAY, snips_say, schema=SERVICE_SCHEMA_SAY) hass.services.async_register( DOMAIN, SERVICE_SAY_ACTION, snips_say_action, schema=SERVICE_SCHEMA_SAY_ACTION) + hass.services.async_register( + DOMAIN, SERVICE_FEEDBACK_ON, feedback_on, + schema=SERVICE_SCHEMA_FEEDBACK) + hass.services.async_register( + DOMAIN, SERVICE_FEEDBACK_OFF, feedback_off, + schema=SERVICE_SCHEMA_FEEDBACK) return True diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index f37beef7960..2342e897708 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -1,20 +1,92 @@ """Test the Snips component.""" -import asyncio import json import logging -from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component +from homeassistant.components.mqtt import MQTT_PUBLISH_SCHEMA +import homeassistant.components.snips as snips from tests.common import (async_fire_mqtt_message, async_mock_intent, async_mock_service) -from homeassistant.components.snips import (SERVICE_SCHEMA_SAY, - SERVICE_SCHEMA_SAY_ACTION) -@asyncio.coroutine -def test_snips_intent(hass, mqtt_mock): +async def test_snips_config(hass, mqtt_mock): + """Test Snips Config.""" + result = await async_setup_component(hass, "snips", { + "snips": { + "feedback_sounds": True, + "probability_threshold": .5, + "site_ids": ["default", "remote"] + }, + }) + assert result + + +async def test_snips_bad_config(hass, mqtt_mock): + """Test Snips bad config.""" + result = await async_setup_component(hass, "snips", { + "snips": { + "feedback_sounds": "on", + "probability": "none", + "site_ids": "default" + }, + }) + assert not result + + +async def test_snips_config_feedback_on(hass, mqtt_mock): + """Test Snips Config.""" + calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA) + result = await async_setup_component(hass, "snips", { + "snips": { + "feedback_sounds": True + }, + }) + assert result + await hass.async_block_till_done() + + assert len(calls) == 2 + topic = calls[0].data['topic'] + assert topic == 'hermes/feedback/sound/toggleOn' + topic = calls[1].data['topic'] + assert topic == 'hermes/feedback/sound/toggleOn' + assert calls[1].data['qos'] == 1 + assert calls[1].data['retain'] + + +async def test_snips_config_feedback_off(hass, mqtt_mock): + """Test Snips Config.""" + calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA) + result = await async_setup_component(hass, "snips", { + "snips": { + "feedback_sounds": False + }, + }) + assert result + await hass.async_block_till_done() + + assert len(calls) == 2 + topic = calls[0].data['topic'] + assert topic == 'hermes/feedback/sound/toggleOn' + topic = calls[1].data['topic'] + assert topic == 'hermes/feedback/sound/toggleOff' + assert calls[1].data['qos'] == 0 + assert not calls[1].data['retain'] + + +async def test_snips_config_no_feedback(hass, mqtt_mock): + """Test Snips Config.""" + calls = async_mock_service(hass, 'snips', 'say') + result = await async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + await hass.async_block_till_done() + assert len(calls) == 0 + + +async def test_snips_intent(hass, mqtt_mock): """Test intent via Snips.""" - result = yield from async_setup_component(hass, "snips", { + result = await async_setup_component(hass, "snips", { "snips": {}, }) assert result @@ -41,7 +113,7 @@ def test_snips_intent(hass, mqtt_mock): async_fire_mqtt_message(hass, 'hermes/intent/Lights', payload) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] assert intent.platform == 'snips' @@ -50,10 +122,9 @@ def test_snips_intent(hass, mqtt_mock): assert intent.text_input == 'turn the lights green' -@asyncio.coroutine -def test_snips_intent_with_duration(hass, mqtt_mock): +async def test_snips_intent_with_duration(hass, mqtt_mock): """Test intent with Snips duration.""" - result = yield from async_setup_component(hass, "snips", { + result = await async_setup_component(hass, "snips", { "snips": {}, }) assert result @@ -61,7 +132,8 @@ def test_snips_intent_with_duration(hass, mqtt_mock): { "input": "set a timer of five minutes", "intent": { - "intentName": "SetTimer" + "intentName": "SetTimer", + "probability": 1 }, "slots": [ { @@ -92,7 +164,7 @@ def test_snips_intent_with_duration(hass, mqtt_mock): async_fire_mqtt_message(hass, 'hermes/intent/SetTimer', payload) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] assert intent.platform == 'snips' @@ -100,22 +172,14 @@ def test_snips_intent_with_duration(hass, mqtt_mock): assert intent.slots == {'timer_duration': {'value': 300}} -@asyncio.coroutine -def test_intent_speech_response(hass, mqtt_mock): +async def test_intent_speech_response(hass, mqtt_mock): """Test intent speech response via Snips.""" - event = 'call_service' - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - result = yield from async_setup_component(hass, "snips", { + calls = async_mock_service(hass, 'mqtt', 'publish', MQTT_PUBLISH_SCHEMA) + result = await async_setup_component(hass, "snips", { "snips": {}, }) assert result - result = yield from async_setup_component(hass, "intent_script", { + result = await async_setup_component(hass, "intent_script", { "intent_script": { "spokenIntent": { "speech": { @@ -131,31 +195,28 @@ def test_intent_speech_response(hass, mqtt_mock): "input": "speak to me", "sessionId": "abcdef0123456789", "intent": { - "intentName": "spokenIntent" + "intentName": "spokenIntent", + "probability": 1 }, "slots": [] } """ - hass.bus.async_listen(event, record_event) async_fire_mqtt_message(hass, 'hermes/intent/spokenIntent', payload) - yield from hass.async_block_till_done() + await hass.async_block_till_done() - assert len(events) == 1 - assert events[0].data['domain'] == 'mqtt' - assert events[0].data['service'] == 'publish' - payload = json.loads(events[0].data['service_data']['payload']) - topic = events[0].data['service_data']['topic'] + assert len(calls) == 1 + payload = json.loads(calls[0].data['payload']) + topic = calls[0].data['topic'] assert payload['sessionId'] == 'abcdef0123456789' assert payload['text'] == 'I am speaking to you' assert topic == 'hermes/dialogueManager/endSession' -@asyncio.coroutine -def test_unknown_intent(hass, mqtt_mock, caplog): +async def test_unknown_intent(hass, mqtt_mock, caplog): """Test unknown intent.""" caplog.set_level(logging.WARNING) - result = yield from async_setup_component(hass, "snips", { + result = await async_setup_component(hass, "snips", { "snips": {}, }) assert result @@ -164,21 +225,21 @@ def test_unknown_intent(hass, mqtt_mock, caplog): "input": "I don't know what I am supposed to do", "sessionId": "abcdef1234567890", "intent": { - "intentName": "unknownIntent" + "intentName": "unknownIntent", + "probability": 1 }, "slots": [] } """ async_fire_mqtt_message(hass, 'hermes/intent/unknownIntent', payload) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert 'Received unknown intent unknownIntent' in caplog.text -@asyncio.coroutine -def test_snips_intent_user(hass, mqtt_mock): +async def test_snips_intent_user(hass, mqtt_mock): """Test intentName format user_XXX__intentName.""" - result = yield from async_setup_component(hass, "snips", { + result = await async_setup_component(hass, "snips", { "snips": {}, }) assert result @@ -186,7 +247,8 @@ def test_snips_intent_user(hass, mqtt_mock): { "input": "what to do", "intent": { - "intentName": "user_ABCDEF123__Lights" + "intentName": "user_ABCDEF123__Lights", + "probability": 1 }, "slots": [] } @@ -194,7 +256,7 @@ def test_snips_intent_user(hass, mqtt_mock): intents = async_mock_intent(hass, 'Lights') async_fire_mqtt_message(hass, 'hermes/intent/user_ABCDEF123__Lights', payload) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -202,10 +264,9 @@ def test_snips_intent_user(hass, mqtt_mock): assert intent.intent_type == 'Lights' -@asyncio.coroutine -def test_snips_intent_username(hass, mqtt_mock): +async def test_snips_intent_username(hass, mqtt_mock): """Test intentName format username:intentName.""" - result = yield from async_setup_component(hass, "snips", { + result = await async_setup_component(hass, "snips", { "snips": {}, }) assert result @@ -213,7 +274,8 @@ def test_snips_intent_username(hass, mqtt_mock): { "input": "what to do", "intent": { - "intentName": "username:Lights" + "intentName": "username:Lights", + "probability": 1 }, "slots": [] } @@ -221,7 +283,7 @@ def test_snips_intent_username(hass, mqtt_mock): intents = async_mock_intent(hass, 'Lights') async_fire_mqtt_message(hass, 'hermes/intent/username:Lights', payload) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(intents) == 1 intent = intents[0] @@ -229,15 +291,41 @@ def test_snips_intent_username(hass, mqtt_mock): assert intent.intent_type == 'Lights' -@asyncio.coroutine -def test_snips_say(hass, caplog): +async def test_snips_low_probability(hass, mqtt_mock, caplog): + """Test intent via Snips.""" + caplog.set_level(logging.WARNING) + result = await async_setup_component(hass, "snips", { + "snips": { + "probability_threshold": 0.5 + }, + }) + assert result + payload = """ + { + "input": "I am not sure what to say", + "intent": { + "intentName": "LightsMaybe", + "probability": 0.49 + }, + "slots": [] + } + """ + + async_mock_intent(hass, 'LightsMaybe') + async_fire_mqtt_message(hass, 'hermes/intent/LightsMaybe', + payload) + await hass.async_block_till_done() + assert 'Intent below probaility threshold 0.49 < 0.5' in caplog.text + + +async def test_snips_say(hass, caplog): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'say', - SERVICE_SCHEMA_SAY) + snips.SERVICE_SCHEMA_SAY) data = {'text': 'Hello'} - yield from hass.services.async_call('snips', 'say', data) - yield from hass.async_block_till_done() + await hass.services.async_call('snips', 'say', data) + await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].domain == 'snips' @@ -245,15 +333,14 @@ def test_snips_say(hass, caplog): assert calls[0].data['text'] == 'Hello' -@asyncio.coroutine -def test_snips_say_action(hass, caplog): +async def test_snips_say_action(hass, caplog): """Test snips say_action with invalid config.""" calls = async_mock_service(hass, 'snips', 'say_action', - SERVICE_SCHEMA_SAY_ACTION) + snips.SERVICE_SCHEMA_SAY_ACTION) data = {'text': 'Hello', 'intent_filter': ['myIntent']} - yield from hass.services.async_call('snips', 'say_action', data) - yield from hass.async_block_till_done() + await hass.services.async_call('snips', 'say_action', data) + await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].domain == 'snips' @@ -262,31 +349,71 @@ def test_snips_say_action(hass, caplog): assert calls[0].data['intent_filter'] == ['myIntent'] -@asyncio.coroutine -def test_snips_say_invalid_config(hass, caplog): +async def test_snips_say_invalid_config(hass, caplog): """Test snips say with invalid config.""" calls = async_mock_service(hass, 'snips', 'say', - SERVICE_SCHEMA_SAY) + snips.SERVICE_SCHEMA_SAY) data = {'text': 'Hello', 'badKey': 'boo'} - yield from hass.services.async_call('snips', 'say', data) - yield from hass.async_block_till_done() + await hass.services.async_call('snips', 'say', data) + await hass.async_block_till_done() assert len(calls) == 0 assert 'ERROR' in caplog.text assert 'Invalid service data' in caplog.text -@asyncio.coroutine -def test_snips_say_action_invalid_config(hass, caplog): +async def test_snips_say_action_invalid(hass, caplog): """Test snips say_action with invalid config.""" calls = async_mock_service(hass, 'snips', 'say_action', - SERVICE_SCHEMA_SAY_ACTION) + snips.SERVICE_SCHEMA_SAY_ACTION) data = {'text': 'Hello', 'can_be_enqueued': 'notabool'} - yield from hass.services.async_call('snips', 'say_action', data) - yield from hass.async_block_till_done() + await hass.services.async_call('snips', 'say_action', data) + await hass.async_block_till_done() assert len(calls) == 0 assert 'ERROR' in caplog.text assert 'Invalid service data' in caplog.text + + +async def test_snips_feedback_on(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'feedback_on', + snips.SERVICE_SCHEMA_FEEDBACK) + + data = {'site_id': 'remote'} + await hass.services.async_call('snips', 'feedback_on', data) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'feedback_on' + assert calls[0].data['site_id'] == 'remote' + + +async def test_snips_feedback_off(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'feedback_off', + snips.SERVICE_SCHEMA_FEEDBACK) + + data = {'site_id': 'remote'} + await hass.services.async_call('snips', 'feedback_off', data) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'snips' + assert calls[0].service == 'feedback_off' + assert calls[0].data['site_id'] == 'remote' + + +async def test_snips_feedback_config(hass, caplog): + """Test snips say with invalid config.""" + calls = async_mock_service(hass, 'snips', 'feedback_on', + snips.SERVICE_SCHEMA_FEEDBACK) + + data = {'site_id': 'remote', 'test': 'test'} + await hass.services.async_call('snips', 'feedback_on', data) + await hass.async_block_till_done() + + assert len(calls) == 0 From 2b86059fd0e8c6417d79f4e75faa92e3a5fb3da9 Mon Sep 17 00:00:00 2001 From: Sean Wilson Date: Mon, 9 Apr 2018 13:38:57 -0400 Subject: [PATCH 329/924] Add missing DISCHRG state (#13787) * Add missing ups.status states. * Add missing DISCHRG state. --- homeassistant/components/sensor/nut.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index e0d5b7250e9..b8917080efc 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -113,6 +113,7 @@ STATE_TYPES = { 'HB': 'High Battery', 'RB': 'Battery Needs Replaced', 'CHRG': 'Battery Charging', + 'DISCHRG': 'Battery Discharging', 'BYPASS': 'Bypass Active', 'CAL': 'Runtime Calibration', 'OFF': 'Offline', From ae4e792651d72ab6f629757e7b1c66219c4e3599 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 9 Apr 2018 22:57:10 +0200 Subject: [PATCH 330/924] Improved upgradeability HomeKit security_systems (#13783) --- .../homekit/type_security_systems.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 235a8b22e7c..0c3c3e42d4b 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -67,15 +67,13 @@ class SecuritySystem(HomeAccessory): return hass_state = new_state.state - if hass_state not in HASS_TO_HOMEKIT: - return + if hass_state in HASS_TO_HOMEKIT: + current_security_state = HASS_TO_HOMEKIT[hass_state] + self.char_current_state.set_value(current_security_state) + _LOGGER.debug('%s: Updated current state to %s (%d)', + self.entity_id, hass_state, current_security_state) - current_security_state = HASS_TO_HOMEKIT[hass_state] - self.char_current_state.set_value(current_security_state) - _LOGGER.debug('%s: Updated current state to %s (%d)', - self.entity_id, hass_state, current_security_state) - - if not self.flag_target_state: - self.char_target_state.set_value(current_security_state) - if self.char_target_state.value == self.char_current_state.value: - self.flag_target_state = False + if not self.flag_target_state: + self.char_target_state.set_value(current_security_state) + if self.char_target_state.value == self.char_current_state.value: + self.flag_target_state = False From 7595401dcba96741832823d0a5d9b1855611f163 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 10 Apr 2018 01:24:06 +0200 Subject: [PATCH 331/924] Qwikswitch Entity Register (#13791) * Entity Register * feedback --- homeassistant/components/qwikswitch.py | 6 ++++++ homeassistant/components/sensor/qwikswitch.py | 7 +++++++ tests/components/sensor/test_qwikswitch.py | 3 --- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 36bd726fa2d..4d34ccca24a 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -64,6 +64,12 @@ class QSEntity(Entity): """QS sensors gets packets in update_packet.""" return False + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return "qs{}".format(self.qsid) + + @callback def update_packet(self, packet): """Receive update packet from QSUSB. Match dispather_send signature.""" self.async_schedule_update_ha_state() diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py index 98c67b7a21c..ebd5f5254d4 100644 --- a/homeassistant/components/sensor/qwikswitch.py +++ b/homeassistant/components/sensor/qwikswitch.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/sensor.qwikswitch/ import logging from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH, QSEntity +from homeassistant.core import callback DEPENDENCIES = [QWIKSWITCH] @@ -41,6 +42,7 @@ class QSSensor(QSEntity): if isinstance(self.unit, type): self.unit = "{}:{}".format(self.sensor_type, self.channel) + @callback def update_packet(self, packet): """Receive update packet from QSUSB.""" val = self._decode(packet.get('data'), channel=self.channel) @@ -55,6 +57,11 @@ class QSSensor(QSEntity): """Return the value of the sensor.""" return str(self._val) + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return "qs{}:{}".format(self.qsid, self.channel) + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" diff --git a/tests/components/sensor/test_qwikswitch.py b/tests/components/sensor/test_qwikswitch.py index d9799b8530e..d9dfe072fc0 100644 --- a/tests/components/sensor/test_qwikswitch.py +++ b/tests/components/sensor/test_qwikswitch.py @@ -1,5 +1,4 @@ """Test qwikswitch sensors.""" -import asyncio import logging import pytest @@ -29,7 +28,6 @@ class AiohttpClientMockResponseList(list): async def wait_till_empty(self, hass): """Wait until empty.""" while self: - await asyncio.sleep(1) await hass.async_block_till_done() await hass.async_block_till_done() @@ -54,7 +52,6 @@ def aioclient_mock(): yield mock_session -# @asyncio.coroutine async def test_sensor_device(hass, aioclient_mock): """Test a sensor device.""" config = { From 5ac52b74e0951225905562bac68ba5eddb6c6e66 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Apr 2018 21:21:26 -0400 Subject: [PATCH 332/924] Remove vendor lookup for mac addresses (#13788) * Remove vendor lookup for mac addresses * Fix tests --- .../components/device_tracker/__init__.py | 61 +-------- tests/components/device_tracker/test_init.py | 128 +----------------- tests/conftest.py | 4 +- 3 files changed, 6 insertions(+), 187 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 682496335a0..45f0e51a214 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -9,8 +9,6 @@ from datetime import timedelta import logging from typing import Any, List, Sequence, Callable -import aiohttp -import async_timeout import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform @@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass from homeassistant.components import group, zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name' ATTR_MAC = 'mac' ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' -ATTR_VENDOR = 'vendor' ATTR_CONSIDER_HOME = 'consider_home' SOURCE_TYPE_GPS = 'gps' @@ -328,14 +324,10 @@ class DeviceTracker(object): self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) - # lookup mac vendor string to be stored in config - yield from device.set_vendor_for_mac() - self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, ATTR_HOST_NAME: device.host_name, ATTR_MAC: device.mac, - ATTR_VENDOR: device.vendor, }) # update known_devices.yaml @@ -413,7 +405,6 @@ class Device(Entity): consider_home = None # type: dt_util.dt.timedelta battery = None # type: int attributes = None # type: dict - vendor = None # type: str icon = None # type: str # Track if the last update of this device was HOME. @@ -423,7 +414,7 @@ class Device(Entity): def __init__(self, hass: HomeAssistantType, consider_home: timedelta, track: bool, dev_id: str, mac: str, name: str = None, picture: str = None, gravatar: str = None, icon: str = None, - hide_if_away: bool = False, vendor: str = None) -> None: + hide_if_away: bool = False) -> None: """Initialize a device.""" self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -451,7 +442,6 @@ class Device(Entity): self.icon = icon self.away_hide = hide_if_away - self.vendor = vendor self.source_type = None @@ -567,51 +557,6 @@ class Device(Entity): self._state = STATE_HOME self.last_update_home = True - @asyncio.coroutine - def set_vendor_for_mac(self): - """Set vendor string using api.macvendors.com.""" - self.vendor = yield from self.get_vendor_for_mac() - - @asyncio.coroutine - def get_vendor_for_mac(self): - """Try to find the vendor string for a given MAC address.""" - if not self.mac: - return None - - if '_' in self.mac: - _, mac = self.mac.split('_', 1) - else: - mac = self.mac - - if not len(mac.split(':')) == 6: - return 'unknown' - - # We only need the first 3 bytes of the MAC for a lookup - # this improves somewhat on privacy - oui_bytes = mac.split(':')[0:3] - # bytes like 00 get truncates to 0, API needs full bytes - oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) - url = 'http://api.macvendors.com/' + oui - try: - websession = async_get_clientsession(self.hass) - - with async_timeout.timeout(5, loop=self.hass.loop): - resp = yield from websession.get(url) - # mac vendor found, response is the string - if resp.status == 200: - vendor_string = yield from resp.text() - return vendor_string - # If vendor is not known to the API (404) or there - # was a failure during the lookup (500); set vendor - # to something other then None to prevent retry - # as the value is only relevant when it is to be stored - # in the 'known_devices.yaml' file which only happens - # the first time the device is seen. - return 'unknown' - except (asyncio.TimeoutError, aiohttp.ClientError): - # Same as above - return 'unknown' - @asyncio.coroutine def async_added_to_hass(self): """Add an entity.""" @@ -685,7 +630,6 @@ def async_load_config(path: str, hass: HomeAssistantType, vol.Optional('picture', default=None): vol.Any(None, cv.string), vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( cv.time_period, cv.positive_timedelta), - vol.Optional('vendor', default=None): vol.Any(None, cv.string), }) try: result = [] @@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType, return [] for dev_id, device in devices.items(): + # Deprecated option. We just ignore it to avoid breaking change + device.pop('vendor', None) try: device = dev_schema(device) device['dev_id'] = cv.slugify(dev_id) @@ -772,7 +718,6 @@ def update_config(path: str, dev_id: str, device: Device): 'picture': device.config_picture, 'track': device.track, CONF_AWAY_HIDE: device.away_hide, - 'vendor': device.vendor, }} out.write('\n') out.write(dump(device)) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index c051983d8fa..912bd315ecd 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -24,9 +24,7 @@ from homeassistant.remote import JSONEncoder from tests.common import ( get_test_home_assistant, fire_time_changed, - patch_yaml_files, assert_setup_component, mock_restore_cache, mock_coro) - -from ...test_util.aiohttp import mock_aiohttp_client + patch_yaml_files, assert_setup_component, mock_restore_cache) TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -111,7 +109,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.config_picture, config.config_picture) self.assertEqual(device.away_hide, config.away_hide) self.assertEqual(device.consider_home, config.consider_home) - self.assertEqual(device.vendor, config.vendor) self.assertEqual(device.icon, config.icon) # pylint: disable=invalid-name @@ -173,124 +170,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): "55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar") self.assertEqual(device.config_picture, gravatar_url) - def test_mac_vendor_lookup(self): - """Test if vendor string is lookup on macvendors API.""" - mac = 'B8:27:EB:00:00:00' - vendor_string = 'Raspberry Pi Foundation' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - assert aioclient_mock.call_count == 1 - - self.assertEqual(device.vendor, vendor_string) - - def test_mac_vendor_mac_formats(self): - """Verify all variations of MAC addresses are handled correctly.""" - vendor_string = 'Raspberry Pi Foundation' - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - aioclient_mock.get('http://api.macvendors.com/00:27:eb', - text=vendor_string) - - mac = 'B8:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - mac = '0:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - mac = 'PREFIXED_B8:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - def test_mac_vendor_lookup_unknown(self): - """Prevent another mac vendor lookup if was not found first time.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - status=404) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_error(self): - """Prevent another lookup if failure during API call.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - status=500) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_exception(self): - """Prevent another lookup if exception during API call.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - exc=asyncio.TimeoutError()) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_on_see(self): - """Test if macvendor is looked up when device is seen.""" - mac = 'B8:27:EB:00:00:00' - vendor_string = 'Raspberry Pi Foundation' - - tracker = device_tracker.DeviceTracker( - self.hass, timedelta(seconds=60), 0, {}, []) - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - - run_coroutine_threadsafe( - tracker.async_see(mac=mac), self.hass.loop).result() - assert aioclient_mock.call_count == 1, \ - 'No http request for macvendor made!' - self.assertEqual(tracker.devices['b827eb000000'].vendor, vendor_string) - @patch( 'homeassistant.components.device_tracker.DeviceTracker.see') @patch( @@ -463,7 +342,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'entity_id': 'device_tracker.hello', 'host_name': 'hello', 'mac': 'MAC_1', - 'vendor': 'unknown', } # pylint: disable=invalid-name @@ -495,9 +373,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): timedelta(seconds=0)) assert len(config) == 0 - @patch('homeassistant.components.device_tracker.Device' - '.set_vendor_for_mac', return_value=mock_coro()) - def test_see_state(self, mock_set_vendor): + def test_see_state(self): """Test device tracker see records state correctly.""" self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, TEST_PLATFORM)) diff --git a/tests/conftest.py b/tests/conftest.py index 8f0ca787721..269d460ebb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,7 +123,5 @@ def mock_device_tracker_conf(): ), patch( 'homeassistant.components.device_tracker.async_load_config', side_effect=lambda *args: mock_coro(devices) - ), patch('homeassistant.components.device_tracker' - '.Device.set_vendor_for_mac'): - + ): yield devices From c8a464d8f9a5860446ed3c6b420ad3d9d76b2bc4 Mon Sep 17 00:00:00 2001 From: citruz Date: Tue, 10 Apr 2018 03:24:18 +0200 Subject: [PATCH 333/924] Updated beacontools to 1.2.3 (#13792) --- homeassistant/components/sensor/eddystone_temperature.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/eddystone_temperature.py b/homeassistant/components/sensor/eddystone_temperature.py index 06accb26eb6..2c8ad4781d0 100644 --- a/homeassistant/components/sensor/eddystone_temperature.py +++ b/homeassistant/components/sensor/eddystone_temperature.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START) -REQUIREMENTS = ['beacontools[scan]==1.2.1', 'construct==2.9.41'] +REQUIREMENTS = ['beacontools[scan]==1.2.3', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a747b5c3090..5471a1ce9e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -131,7 +131,7 @@ basicmodem==0.7 batinfo==0.4.2 # homeassistant.components.sensor.eddystone_temperature -# beacontools[scan]==1.2.1 +# beacontools[scan]==1.2.3 # homeassistant.components.device_tracker.linksys_ap # homeassistant.components.sensor.geizhals From bd93f10d3c5643a488d0fa74070af5f7ba82c4d1 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Tue, 10 Apr 2018 03:24:50 +0200 Subject: [PATCH 334/924] script/lazytox: Ensure Flake8 passes for tests/ (#13794) --- script/lazytox.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/script/lazytox.py b/script/lazytox.py index 2639d640753..19af5560dfb 100755 --- a/script/lazytox.py +++ b/script/lazytox.py @@ -18,7 +18,7 @@ except ImportError: RE_ASCII = re.compile(r"\033\[[^m]*m") -Error = namedtuple('Error', ['file', 'line', 'col', 'msg']) +Error = namedtuple('Error', ['file', 'line', 'col', 'msg', 'skip']) PASS = 'green' FAIL = 'bold_red' @@ -109,8 +109,9 @@ async def pylint(files): line = line.split(':') if len(line) < 3: continue - res.append(Error(line[0].replace('\\', '/'), - line[1], "", line[2].strip())) + _fn = line[0].replace('\\', '/') + res.append(Error( + _fn, line[1], '', line[2].strip(), _fn.startswith('tests/'))) return res @@ -122,8 +123,8 @@ async def flake8(files): line = line.split(':') if len(line) < 4: continue - res.append(Error(line[0].replace('\\', '/'), - line[1], line[2], line[3].strip())) + _fn = line[0].replace('\\', '/') + res.append(Error(_fn, line[1], line[2], line[3].strip(), False)) return res @@ -144,7 +145,7 @@ async def lint(files): err_msg = "{} {}:{} {}".format(err.file, err.line, err.col, err.msg) # tests/* does not have to pass lint - if err.file.startswith('tests/'): + if err.skip: print(err_msg) else: printc(FAIL, err_msg) From 7ea776dff4cc88ff7de0752136a452dd44dca995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Kut=C3=BD?= <6du1ro.n@gmail.com> Date: Tue, 10 Apr 2018 08:20:47 +0200 Subject: [PATCH 335/924] Fix bad metrics format for short metrics. (#13778) --- homeassistant/components/prometheus.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index f9629ca726a..dc1cbd945a7 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -185,6 +185,9 @@ class Metrics(object): unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) metric = state.entity_id.split(".")[1] + if '_' not in str(metric): + metric = state.entity_id.replace('.', '_') + try: int(metric.split("_")[-1]) metric = "_".join(metric.split("_")[:-1]) From 2707d35a8638662decd04a3383d5f1927acf8bd0 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Tue, 10 Apr 2018 00:12:21 -0700 Subject: [PATCH 336/924] Update bellows to 0.5.2 (#13800) --- homeassistant/components/zha/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 39419034545..73c1fdf9075 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers import discovery, entity from homeassistant.util import slugify REQUIREMENTS = [ - 'bellows==0.5.1', + 'bellows==0.5.2', 'zigpy==0.0.3', 'zigpy-xbee==0.0.2', ] diff --git a/requirements_all.txt b/requirements_all.txt index 5471a1ce9e2..cb45639a4d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,7 +140,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.5.1 +bellows==0.5.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.0 From cf88d8a1b92b91f0843186a8d0f2b8eeadbf6885 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 10 Apr 2018 14:11:00 -0400 Subject: [PATCH 337/924] iglo hs color fix (#13808) --- homeassistant/components/light/iglo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 77e3972968c..f40dc2ce84e 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -79,7 +79,7 @@ class IGloLamp(Light): @property def hs_color(self): """Return the hs value.""" - return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb']) + return color_util.color_RGB_to_hs(*self._lamp.state()['rgb']) @property def effect(self): From 978a79d369b2d0b1670e783c7e3856f7388dffcf Mon Sep 17 00:00:00 2001 From: Toby Gray Date: Tue, 10 Apr 2018 19:38:36 +0100 Subject: [PATCH 338/924] device_tracker.ubus: Handle devices not running DHCP (#13579) --- homeassistant/components/device_tracker/ubus.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index dd12df7b070..3d7ef5cef6e 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -103,6 +103,9 @@ class UbusDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" if self.mac2name is None: self._generate_mac2name() + if self.mac2name is None: + # Generation of mac2name dictionary failed + return None name = self.mac2name.get(device.upper(), None) return name From 191e32f6cf12bdda95c043b92c974c9aef90be19 Mon Sep 17 00:00:00 2001 From: Wojtek Date: Tue, 10 Apr 2018 21:11:45 +0200 Subject: [PATCH 339/924] Update yweather.py (#13802) Map clear-night string to 31 value. --- homeassistant/components/weather/yweather.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index f9610e469b2..5987cf7621f 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -32,6 +32,7 @@ DEFAULT_NAME = 'Yweather' SCAN_INTERVAL = timedelta(minutes=10) CONDITION_CLASSES = { + 'clear-night': [31], 'cloudy': [26, 27, 28, 29, 30], 'fog': [19, 20, 21, 22, 23], 'hail': [17, 18, 35], From 16a1a4e0b187e99b9b113ad8f946bfac66e62ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 10 Apr 2018 22:12:55 +0200 Subject: [PATCH 340/924] Tibber lib update (#13811) --- homeassistant/components/sensor/tibber.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index aaaa8366909..ca1c1922ab5 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -20,7 +20,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util -REQUIREMENTS = ['pyTibber==0.4.0'] +REQUIREMENTS = ['pyTibber==0.4.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index cb45639a4d6..a80a59c2509 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,7 +670,7 @@ pyHS100==0.3.0 pyRFXtrx==0.22.0 # homeassistant.components.sensor.tibber -pyTibber==0.4.0 +pyTibber==0.4.1 # homeassistant.components.switch.dlink pyW215==0.6.0 From b2695e498d00ae79f01035be885d34c9be69b5a7 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 10 Apr 2018 23:33:56 +0200 Subject: [PATCH 341/924] Update pyhomematic to 0.1.41 (#13814) * Update requirements_all.txt * Update __init__.py --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index c542cd9e88e..23fe9685418 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.40'] +REQUIREMENTS = ['pyhomematic==0.1.41'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a80a59c2509..d5d64381528 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -777,7 +777,7 @@ pyhik==0.1.8 pyhiveapi==0.2.11 # homeassistant.components.homematic -pyhomematic==0.1.40 +pyhomematic==0.1.41 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.1 From 8d48164f25f3b7f272ee486ecdeb6e1e8e4c6174 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Tue, 10 Apr 2018 18:38:23 -0700 Subject: [PATCH 342/924] Add support for Eufy bulbs and switches (#13773) * Add support for Eufy bulbs and switches Add support for driving bulbs and switches from the Eufy range. * Fix hound checks * Satisfy pylint * Handle review comments * Review updates and test fixes * PyLint is a bit too aggressive --- .coveragerc | 3 + homeassistant/components/eufy.py | 77 ++++++++++++ homeassistant/components/light/eufy.py | 158 ++++++++++++++++++++++++ homeassistant/components/switch/eufy.py | 73 +++++++++++ requirements_all.txt | 3 + 5 files changed, 314 insertions(+) create mode 100644 homeassistant/components/eufy.py create mode 100644 homeassistant/components/light/eufy.py create mode 100644 homeassistant/components/switch/eufy.py diff --git a/.coveragerc b/.coveragerc index 6b1ca91a574..666134488fe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,9 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/eufy.py + homeassistant/components/*/eufy.py + homeassistant/components/gc100.py homeassistant/components/*/gc100.py diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py new file mode 100644 index 00000000000..53584be9fdc --- /dev/null +++ b/homeassistant/components/eufy.py @@ -0,0 +1,77 @@ +""" +Support for Eufy devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/eufy/ +""" +import logging + +import voluptuous as vol + +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ADDRESS, \ + CONF_DEVICES, CONF_USERNAME, CONF_PASSWORD, CONF_TYPE, CONF_NAME +from homeassistant.helpers import discovery + +import homeassistant.helpers.config_validation as cv + + +REQUIREMENTS = ['lakeside==0.4'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'eufy' + +DEVICE_SCHEMA = vol.Schema({ + vol.Required(CONF_ADDRESS): cv.string, + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_TYPE): cv.string, + vol.Optional(CONF_NAME): cv.string +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional(CONF_DEVICES, default=[]): vol.All(cv.ensure_list, + [DEVICE_SCHEMA]), + vol.Inclusive(CONF_USERNAME, 'authentication'): cv.string, + vol.Inclusive(CONF_PASSWORD, 'authentication'): cv.string, + }), +}, extra=vol.ALLOW_EXTRA) + +EUFY_DISPATCH = { + 'T1011': 'light', + 'T1012': 'light', + 'T1013': 'light', + 'T1201': 'switch', + 'T1202': 'switch', + 'T1211': 'switch' +} + + +def setup(hass, config): + """Set up Eufy devices.""" + # pylint: disable=import-error + import lakeside + + if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: + data = lakeside.get_devices(config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD]) + for device in data: + kind = device['type'] + if kind not in EUFY_DISPATCH: + continue + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, + config) + + for device_info in config[DOMAIN][CONF_DEVICES]: + kind = device_info['type'] + if kind not in EUFY_DISPATCH: + continue + device = {} + device['address'] = device_info['address'] + device['code'] = device_info['access_token'] + device['type'] = device_info['type'] + device['name'] = device_info['name'] + discovery.load_platform(hass, EUFY_DISPATCH[kind], DOMAIN, device, + config) + + return True diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py new file mode 100644 index 00000000000..fa6550d2682 --- /dev/null +++ b/homeassistant/components/light/eufy.py @@ -0,0 +1,158 @@ +""" +Support for Eufy lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.eufy/ +""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, Light) + +import homeassistant.util.color as color_util + +from homeassistant.util.color import ( + color_temperature_mired_to_kelvin as mired_to_kelvin, + color_temperature_kelvin_to_mired as kelvin_to_mired) + +DEPENDENCIES = ['eufy'] + +_LOGGER = logging.getLogger(__name__) + +EUFY_MAX_KELVIN = 6500 +EUFY_MIN_KELVIN = 2700 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Eufy bulbs.""" + if discovery_info is None: + return + add_devices([EufyLight(discovery_info)], True) + + +class EufyLight(Light): + """Representation of a Eufy light.""" + + def __init__(self, device): + """Initialize the light.""" + # pylint: disable=import-error + import lakeside + + self._temp = None + self._brightness = None + self._hs = None + self._state = None + self._name = device['name'] + self._address = device['address'] + self._code = device['code'] + self._type = device['type'] + self._bulb = lakeside.bulb(self._address, self._code, self._type) + if self._type == "T1011": + self._features = SUPPORT_BRIGHTNESS + elif self._type == "T1012": + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + elif self._type == "T1013": + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR + self._bulb.connect() + + def update(self): + """Synchronise state from the bulb.""" + self._bulb.update() + self._brightness = self._bulb.brightness + self._temp = self._bulb.temperature + if self._bulb.colors: + self._hs = color_util.color_RGB_to_hsv(*self._bulb.colors) + else: + self._hs = None + self._state = self._bulb.power + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + return int(self._brightness * 255 / 100) + + @property + def min_mireds(self): + """Return minimum supported color temperature.""" + return kelvin_to_mired(EUFY_MAX_KELVIN) + + @property + def max_mireds(self): + """Return maximu supported color temperature.""" + return kelvin_to_mired(EUFY_MIN_KELVIN) + + @property + def color_temp(self): + """Return the color temperature of this light.""" + temp_in_k = int(EUFY_MIN_KELVIN + (self._temp * + (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) + / 100)) + return kelvin_to_mired(temp_in_k) + + @property + def hs_color(self): + """Return the color of this light.""" + return self._hs + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + def turn_on(self, **kwargs): + """Turn the specified light on.""" + brightness = kwargs.get(ATTR_BRIGHTNESS) + colortemp = kwargs.get(ATTR_COLOR_TEMP) + # pylint: disable=invalid-name + hs = kwargs.get(ATTR_HS_COLOR) + + if brightness is not None: + brightness = int(brightness * 100 / 255) + else: + brightness = max(1, self._brightness) + + if colortemp is not None: + temp_in_k = mired_to_kelvin(colortemp) + relative_temp = temp_in_k - EUFY_MIN_KELVIN + temp = int(relative_temp * 100 / + (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) + else: + temp = None + + if hs is not None: + rgb = color_util.color_hsv_to_RGB( + hs[0], hs[1], brightness / 255 * 100) + else: + rgb = None + + try: + self._bulb.set_state(power=True, brightness=brightness, + temperature=temp, colors=rgb) + except BrokenPipeError: + self._bulb.connect() + self._bulb.set_state(power=True, brightness=brightness, + temperature=temp, colors=rgb) + + def turn_off(self, **kwargs): + """Turn the specified light off.""" + try: + self._bulb.set_state(power=False) + except BrokenPipeError: + self._bulb.connect() + self._bulb.set_state(power=False) diff --git a/homeassistant/components/switch/eufy.py b/homeassistant/components/switch/eufy.py new file mode 100644 index 00000000000..891525d3979 --- /dev/null +++ b/homeassistant/components/switch/eufy.py @@ -0,0 +1,73 @@ +""" +Support for Eufy switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.eufy/ +""" +import logging + +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['eufy'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Eufy switches.""" + if discovery_info is None: + return + add_devices([EufySwitch(discovery_info)], True) + + +class EufySwitch(SwitchDevice): + """Representation of a Eufy switch.""" + + def __init__(self, device): + """Initialize the light.""" + # pylint: disable=import-error + import lakeside + + self._state = None + self._name = device['name'] + self._address = device['address'] + self._code = device['code'] + self._type = device['type'] + self._switch = lakeside.switch(self._address, self._code, self._type) + self._switch.connect() + + def update(self): + """Synchronise state from the switch.""" + self._switch.update() + self._state = self._switch.power + + @property + def unique_id(self): + """Return the ID of this light.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the specified switch on.""" + try: + self._switch.set_state(True) + except BrokenPipeError: + self._switch.connect() + self._switch.set_state(power=True) + + def turn_off(self, **kwargs): + """Turn the specified switch off.""" + try: + self._switch.set_state(False) + except BrokenPipeError: + self._switch.connect() + self._switch.set_state(False) diff --git a/requirements_all.txt b/requirements_all.txt index d5d64381528..1b3d3206c60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -454,6 +454,9 @@ keyring==12.0.0 # homeassistant.scripts.keyring keyrings.alt==3.0 +# homeassistant.components.eufy +lakeside==0.4 + # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http libnacl==1.6.1 From 2a5751c09d62823371da14e9bdb1b19143851c85 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 11 Apr 2018 22:24:14 +0200 Subject: [PATCH 343/924] Homekit refactor (#13707) --- homeassistant/components/homekit/__init__.py | 85 +++++++---------- .../components/homekit/accessories.py | 59 ++++++++---- homeassistant/components/homekit/const.py | 2 - .../components/homekit/type_covers.py | 35 +++---- .../components/homekit/type_lights.py | 53 ++++------- .../components/homekit/type_locks.py | 30 ++---- .../homekit/type_security_systems.py | 31 ++---- .../components/homekit/type_sensors.py | 59 ++++-------- .../components/homekit/type_switches.py | 23 ++--- .../components/homekit/type_thermostats.py | 95 ++++++++----------- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_accessories.py | 40 ++++---- .../homekit/test_get_accessories.py | 22 ++--- tests/components/homekit/test_homekit.py | 2 +- tests/components/homekit/test_type_covers.py | 2 +- tests/components/homekit/test_type_lights.py | 16 +++- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 8 +- tests/components/homekit/test_type_sensors.py | 11 ++- .../components/homekit/test_type_switches.py | 6 +- .../homekit/test_type_thermostats.py | 22 ++++- tests/components/homekit/test_util.py | 2 +- 22 files changed, 275 insertions(+), 332 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 22c74faf5f0..02d21889f6b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,11 +8,9 @@ from zlib import adler32 import voluptuous as vol -from homeassistant.components.climate import ( - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.cover import SUPPORT_SET_POSITION from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv @@ -79,63 +77,46 @@ def get_accessory(hass, state, aid, config): state.entity_id) return None - if state.domain == 'sensor': - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: - _LOGGER.debug('Add "%s" as "%s"', - state.entity_id, 'TemperatureSensor') - return TYPES['TemperatureSensor'](hass, state.entity_id, - state.name, aid=aid) - elif unit == '%': - _LOGGER.debug('Add "%s" as %s"', - state.entity_id, 'HumiditySensor') - return TYPES['HumiditySensor'](hass, state.entity_id, state.name, - aid=aid) + a_type = None + config = config or {} + + if state.domain == 'alarm_control_panel': + a_type = 'SecuritySystem' elif state.domain == 'binary_sensor' or state.domain == 'device_tracker': - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'BinarySensor') - return TYPES['BinarySensor'](hass, state.entity_id, - state.name, aid=aid) + a_type = 'BinarySensor' + + elif state.domain == 'climate': + a_type = 'Thermostat' elif state.domain == 'cover': # Only add covers that support set_cover_position features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_SET_POSITION: - _LOGGER.debug('Add "%s" as "%s"', - state.entity_id, 'WindowCovering') - return TYPES['WindowCovering'](hass, state.entity_id, state.name, - aid=aid) - - elif state.domain == 'alarm_control_panel': - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'SecuritySystem') - return TYPES['SecuritySystem'](hass, state.entity_id, state.name, - alarm_code=config.get(ATTR_CODE), - aid=aid) - - elif state.domain == 'climate': - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - support_temp_range = SUPPORT_TARGET_TEMPERATURE_LOW | \ - SUPPORT_TARGET_TEMPERATURE_HIGH - # Check if climate device supports auto mode - support_auto = bool(features & support_temp_range) - - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Thermostat') - return TYPES['Thermostat'](hass, state.entity_id, - state.name, support_auto, aid=aid) + a_type = 'WindowCovering' elif state.domain == 'light': - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Light') - return TYPES['Light'](hass, state.entity_id, state.name, aid=aid) + a_type = 'Light' elif state.domain == 'lock': - return TYPES['Lock'](hass, state.entity_id, state.name, aid=aid) + a_type = 'Lock' + + elif state.domain == 'sensor': + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: + a_type = 'TemperatureSensor' + elif unit == '%': + a_type = 'HumiditySensor' elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean' or state.domain == 'script': - _LOGGER.debug('Add "%s" as "%s"', state.entity_id, 'Switch') - return TYPES['Switch'](hass, state.entity_id, state.name, aid=aid) + a_type = 'Switch' - return None + if a_type is None: + return None + + _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) + return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config) def generate_aid(entity_id): @@ -151,7 +132,7 @@ class HomeKit(): def __init__(self, hass, port, entity_filter, entity_config): """Initialize a HomeKit object.""" - self._hass = hass + self.hass = hass self._port = port self._filter = entity_filter self._config = entity_config @@ -164,11 +145,11 @@ class HomeKit(): """Setup bridge and accessory driver.""" from .accessories import HomeBridge, HomeDriver - self._hass.bus.async_listen_once( + self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self.stop) - path = self._hass.config.path(HOMEKIT_FILE) - self.bridge = HomeBridge(self._hass) + path = self.hass.config.path(HOMEKIT_FILE) + self.bridge = HomeBridge(self.hass) self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) def add_bridge_accessory(self, state): @@ -177,7 +158,7 @@ class HomeKit(): return aid = generate_aid(state.entity_id) conf = self._config.pop(state.entity_id, {}) - acc = get_accessory(self._hass, state, aid, conf) + acc = get_accessory(self.hass, state, aid, conf) if acc is not None: self.bridge.add_accessory(acc) @@ -192,12 +173,12 @@ class HomeKit(): type_covers, type_lights, type_locks, type_security_systems, type_sensors, type_switches, type_thermostats) - for state in self._hass.states.all(): + for state in self.hass.states.all(): self.add_bridge_accessory(state) self.bridge.set_broker(self.driver) if not self.bridge.paired: - show_setup_message(self.bridge, self._hass) + show_setup_message(self.hass, self.bridge) _LOGGER.debug('Driver start') self.driver.start() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ec2c49f5e43..d9b90a77d68 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -7,14 +7,14 @@ import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver -from homeassistant.core import callback +from homeassistant.core import callback as ha_callback from homeassistant.helpers.event import ( async_track_state_change, track_point_in_utc_time) from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, - BRIDGE_NAME, MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, + DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, + SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) def debounce(func): """Decorator function. Debounce callbacks form HomeKit.""" - @callback + @ha_callback def call_later_listener(*args): """Callback listener called from call_later.""" # pylint: disable=unsubscriptable-object @@ -72,6 +72,18 @@ def add_preload_service(acc, service, chars=None): return service +def setup_char(char_name, service, value=None, properties=None, callback=None): + """Helper function to return fully configured characteristic.""" + char = service.get_characteristic(char_name) + if value: + char.value = value + if properties: + char.override_properties(properties) + if callback: + char.setter_callback = callback + return char + + def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, serial_number='0000'): """Set the default accessory information.""" @@ -85,14 +97,13 @@ def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, class HomeAccessory(Accessory): """Adapter class for Accessory.""" - # pylint: disable=no-member - - def __init__(self, name=ACCESSORY_NAME, model=ACCESSORY_MODEL, - category='OTHER', **kwargs): + def __init__(self, hass, name, entity_id, aid, category): """Initialize a Accessory object.""" - super().__init__(name, **kwargs) - set_accessory_info(self, name, model) + super().__init__(name, aid=aid) + set_accessory_info(self, name, model=entity_id) self.category = getattr(Category, category, Category.OTHER) + self.entity_id = entity_id + self.hass = hass def _set_services(self): add_preload_service(self, SERV_ACCESSORY_INFO) @@ -100,19 +111,33 @@ class HomeAccessory(Accessory): def run(self): """Method called by accessory after driver is started.""" state = self.hass.states.get(self.entity_id) - self.update_state(new_state=state) + self.update_state_callback(new_state=state) async_track_state_change( - self.hass, self.entity_id, self.update_state) + self.hass, self.entity_id, self.update_state_callback) + + def update_state_callback(self, entity_id=None, old_state=None, + new_state=None): + """Callback from state change listener.""" + _LOGGER.debug('New_state: %s', new_state) + if new_state is None: + return + self.update_state(new_state) + + def update_state(self, new_state): + """Method called on state change to update HomeKit value. + + Overridden by accessory types. + """ + pass class HomeBridge(Bridge): """Adapter class for Bridge.""" - def __init__(self, hass, name=BRIDGE_NAME, - model=BRIDGE_MODEL, **kwargs): + def __init__(self, hass, name=BRIDGE_NAME): """Initialize a Bridge object.""" - super().__init__(name, **kwargs) - set_accessory_info(self, name, model) + super().__init__(name) + set_accessory_info(self, name, model=BRIDGE_MODEL) self.hass = hass def _set_services(self): @@ -130,7 +155,7 @@ class HomeBridge(Bridge): def remove_paired_client(self, client_uuid): """Override super function to show setup message if unpaired.""" super().remove_paired_client(client_uuid) - show_setup_message(self, self.hass) + show_setup_message(self.hass, self) class HomeDriver(AccessoryDriver): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index e5a4c80a430..80f2fd039e6 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -18,8 +18,6 @@ DEFAULT_PORT = 51827 SERVICE_HOMEKIT_START = 'start' # #### STRING CONSTANTS #### -ACCESSORY_MODEL = 'homekit.accessory' -ACCESSORY_NAME = 'Home Accessory' BRIDGE_MODEL = 'homekit.bridge' BRIDGE_NAME = 'Home Assistant' MANUFACTURER = 'HomeAssistant' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 781f52941fc..7c7ab3e3683 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -4,12 +4,11 @@ import logging from homeassistant.components.cover import ATTR_CURRENT_POSITION from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE) - _LOGGER = logging.getLogger(__name__) @@ -20,29 +19,20 @@ class WindowCovering(HomeAccessory): The cover entity must support: set_cover_position. """ - def __init__(self, hass, entity_id, display_name, **kwargs): + def __init__(self, *args, config): """Initialize a WindowCovering accessory object.""" - super().__init__(display_name, entity_id, - CATEGORY_WINDOW_COVERING, **kwargs) - - self.hass = hass - self.entity_id = entity_id - + super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.current_position = None self.homekit_target = None serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = serv_cover. \ - get_characteristic(CHAR_CURRENT_POSITION) - self.char_target_position = serv_cover. \ - get_characteristic(CHAR_TARGET_POSITION) - self.char_position_state = serv_cover. \ - get_characteristic(CHAR_POSITION_STATE) - self.char_current_position.value = 0 - self.char_target_position.value = 0 - self.char_position_state.value = 0 - - self.char_target_position.setter_callback = self.move_cover + self.char_current_position = setup_char( + CHAR_CURRENT_POSITION, serv_cover, value=0) + self.char_target_position = setup_char( + CHAR_TARGET_POSITION, serv_cover, value=0, + callback=self.move_cover) + self.char_position_state = setup_char( + CHAR_POSITION_STATE, serv_cover, value=0) def move_cover(self, value): """Move cover to value if call came from HomeKit.""" @@ -56,11 +46,8 @@ class WindowCovering(HomeAccessory): self.hass.components.cover.set_cover_position( value, self.entity_id) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update cover position after state changed.""" - if new_state is None: - return - current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, int): self.current_position = current_position diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 4fbfb995859..9a7bce76fba 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -7,7 +7,8 @@ from homeassistant.components.light import ( from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES -from .accessories import HomeAccessory, add_preload_service, debounce +from .accessories import ( + HomeAccessory, add_preload_service, debounce, setup_char) from .const import ( CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) @@ -24,12 +25,9 @@ class Light(HomeAccessory): Currently supports: state, brightness, color temperature, rgb_color. """ - def __init__(self, hass, entity_id, name, **kwargs): + def __init__(self, *args, config): """Initialize a new Light accessory object.""" - super().__init__(name, entity_id, CATEGORY_LIGHT, **kwargs) - - self.hass = hass - self.entity_id = entity_id + super().__init__(*args, category=CATEGORY_LIGHT) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} @@ -49,36 +47,29 @@ class Light(HomeAccessory): self._saturation = None serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) - self.char_on = serv_light.get_characteristic(CHAR_ON) - self.char_on.setter_callback = self.set_state - self.char_on.value = self._state + self.char_on = setup_char( + CHAR_ON, serv_light, value=self._state, callback=self.set_state) if CHAR_BRIGHTNESS in self.chars: - self.char_brightness = serv_light \ - .get_characteristic(CHAR_BRIGHTNESS) - self.char_brightness.setter_callback = self.set_brightness - self.char_brightness.value = 0 + self.char_brightness = setup_char( + CHAR_BRIGHTNESS, serv_light, value=0, + callback=self.set_brightness) if CHAR_COLOR_TEMPERATURE in self.chars: - self.char_color_temperature = serv_light \ - .get_characteristic(CHAR_COLOR_TEMPERATURE) - self.char_color_temperature.setter_callback = \ - self.set_color_temperature min_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MAX_MIREDS, 500) - self.char_color_temperature.override_properties({ - 'minValue': min_mireds, 'maxValue': max_mireds}) - self.char_color_temperature.value = min_mireds + self.char_color_temperature = setup_char( + CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds, + properties={'minValue': min_mireds, 'maxValue': max_mireds}, + callback=self.set_color_temperature) if CHAR_HUE in self.chars: - self.char_hue = serv_light.get_characteristic(CHAR_HUE) - self.char_hue.setter_callback = self.set_hue - self.char_hue.value = 0 + self.char_hue = setup_char( + CHAR_HUE, serv_light, value=0, callback=self.set_hue) if CHAR_SATURATION in self.chars: - self.char_saturation = serv_light \ - .get_characteristic(CHAR_SATURATION) - self.char_saturation.setter_callback = self.set_saturation - self.char_saturation.value = 75 + self.char_saturation = setup_char( + CHAR_SATURATION, serv_light, value=75, + callback=self.set_saturation) def set_state(self, value): """Set state if call came from HomeKit.""" @@ -136,11 +127,8 @@ class Light(HomeAccessory): self.hass.components.light.turn_on( self.entity_id, hs_color=color) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update light after state change.""" - if not new_state: - return - # Handle State state = new_state.state if state in (STATE_ON, STATE_OFF): @@ -162,7 +150,8 @@ class Light(HomeAccessory): if CHAR_COLOR_TEMPERATURE in self.chars: color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) if not self._flag[CHAR_COLOR_TEMPERATURE] \ - and isinstance(color_temperature, int): + and isinstance(color_temperature, int) and \ + self.char_color_temperature.value != color_temperature: self.char_color_temperature.set_value(color_temperature) self._flag[CHAR_COLOR_TEMPERATURE] = False diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 9df0c101eff..f34fc6c6a7f 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -5,7 +5,7 @@ from homeassistant.components.lock import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) @@ -27,25 +27,18 @@ class Lock(HomeAccessory): The lock entity must support: unlock and lock. """ - def __init__(self, hass, entity_id, name, **kwargs): + def __init__(self, *args, config): """Initialize a Lock accessory object.""" - super().__init__(name, entity_id, CATEGORY_LOCK, **kwargs) - - self.hass = hass - self.entity_id = entity_id - + super().__init__(*args, category=CATEGORY_LOCK) self.flag_target_state = False serv_lock_mechanism = add_preload_service(self, SERV_LOCK) - self.char_current_state = serv_lock_mechanism. \ - get_characteristic(CHAR_LOCK_CURRENT_STATE) - self.char_target_state = serv_lock_mechanism. \ - get_characteristic(CHAR_LOCK_TARGET_STATE) - - self.char_current_state.value = HASS_TO_HOMEKIT[STATE_UNKNOWN] - self.char_target_state.value = HASS_TO_HOMEKIT[STATE_LOCKED] - - self.char_target_state.setter_callback = self.set_state + self.char_current_state = setup_char( + CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism, + value=HASS_TO_HOMEKIT[STATE_UNKNOWN]) + self.char_target_state = setup_char( + CHAR_LOCK_TARGET_STATE, serv_lock_mechanism, + value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" @@ -58,11 +51,8 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call('lock', service, params) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update lock after state changed.""" - if new_state is None: - return - hass_state = new_state.state if hass_state in HASS_TO_HOMEKIT: current_lock_state = HASS_TO_HOMEKIT[hass_state] diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 0c3c3e42d4b..6b8457a3aa5 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -7,7 +7,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_CODE) from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) @@ -27,26 +27,18 @@ STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, hass, entity_id, display_name, alarm_code, **kwargs): + def __init__(self, *args, config): """Initialize a SecuritySystem accessory object.""" - super().__init__(display_name, entity_id, - CATEGORY_ALARM_SYSTEM, **kwargs) - - self.hass = hass - self.entity_id = entity_id - self._alarm_code = alarm_code - + super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) + self._alarm_code = config[ATTR_CODE] self.flag_target_state = False serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = serv_alarm. \ - get_characteristic(CHAR_CURRENT_SECURITY_STATE) - self.char_current_state.value = 3 - self.char_target_state = serv_alarm. \ - get_characteristic(CHAR_TARGET_SECURITY_STATE) - self.char_target_state.value = 3 - - self.char_target_state.setter_callback = self.set_security_state + self.char_current_state = setup_char( + CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3) + self.char_target_state = setup_char( + CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3, + callback=self.set_security_state) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" @@ -61,11 +53,8 @@ class SecuritySystem(HomeAccessory): params[ATTR_CODE] = self._alarm_code self.hass.services.call('alarm_control_panel', service, params) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update security state after state changed.""" - if new_state is None: - return - hass_state = new_state.state if hass_state in HASS_TO_HOMEKIT: current_security_state = HASS_TO_HOMEKIT[hass_state] diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index b25eb784d6b..790f0de6103 100755 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -6,7 +6,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, @@ -20,10 +20,8 @@ from .const import ( DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import convert_to_float, temperature_to_homekit - _LOGGER = logging.getLogger(__name__) - BINARY_SENSOR_SERVICE_MAP = { DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED), @@ -43,24 +41,17 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, hass, entity_id, name, **kwargs): + def __init__(self, *args, config): """Initialize a TemperatureSensor accessory object.""" - super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) - - self.hass = hass - self.entity_id = entity_id - + super().__init__(*args, category=CATEGORY_SENSOR) serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = serv_temp.get_characteristic(CHAR_CURRENT_TEMPERATURE) - self.char_temp.override_properties(properties=PROP_CELSIUS) - self.char_temp.value = 0 + self.char_temp = setup_char( + CHAR_CURRENT_TEMPERATURE, serv_temp, value=0, + properties=PROP_CELSIUS) self.unit = None - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update temperature after state changed.""" - if new_state is None: - return - unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) temperature = convert_to_float(new_state.state) if temperature: @@ -74,23 +65,15 @@ class TemperatureSensor(HomeAccessory): class HumiditySensor(HomeAccessory): """Generate a HumiditySensor accessory as humidity sensor.""" - def __init__(self, hass, entity_id, name, *args, **kwargs): + def __init__(self, *args, config): """Initialize a HumiditySensor accessory object.""" - super().__init__(name, entity_id, CATEGORY_SENSOR, *args, **kwargs) - - self.hass = hass - self.entity_id = entity_id - + super().__init__(*args, category=CATEGORY_SENSOR) serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) - self.char_humidity = serv_humidity \ - .get_characteristic(CHAR_CURRENT_HUMIDITY) - self.char_humidity.value = 0 + self.char_humidity = setup_char( + CHAR_CURRENT_HUMIDITY, serv_humidity, value=0) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update accessory after state change.""" - if new_state is None: - return - humidity = convert_to_float(new_state.state) if humidity: self.char_humidity.set_value(humidity) @@ -102,28 +85,20 @@ class HumiditySensor(HomeAccessory): class BinarySensor(HomeAccessory): """Generate a BinarySensor accessory as binary sensor.""" - def __init__(self, hass, entity_id, name, **kwargs): + def __init__(self, *args, config): """Initialize a BinarySensor accessory object.""" - super().__init__(name, entity_id, CATEGORY_SENSOR, **kwargs) - - self.hass = hass - self.entity_id = entity_id - - device_class = hass.states.get(entity_id).attributes \ + super().__init__(*args, category=CATEGORY_SENSOR) + device_class = self.hass.states.get(self.entity_id).attributes \ .get(ATTR_DEVICE_CLASS) service_char = BINARY_SENSOR_SERVICE_MAP[device_class] \ if device_class in BINARY_SENSOR_SERVICE_MAP \ else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] service = add_preload_service(self, service_char[0]) - self.char_detected = service.get_characteristic(service_char[1]) - self.char_detected.value = 0 + self.char_detected = setup_char(service_char[1], service, value=0) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update accessory after state change.""" - if new_state is None: - return - state = new_state.state detected = (state == STATE_ON) or (state == STATE_HOME) self.char_detected.set_value(detected) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 854cb49d181..aaf13e4ea7e 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -6,7 +6,7 @@ from homeassistant.const import ( from homeassistant.core import split_entity_id from . import TYPES -from .accessories import HomeAccessory, add_preload_service +from .accessories import HomeAccessory, add_preload_service, setup_char from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON _LOGGER = logging.getLogger(__name__) @@ -16,20 +16,15 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, hass, entity_id, display_name, **kwargs): + def __init__(self, *args, config): """Initialize a Switch accessory object to represent a remote.""" - super().__init__(display_name, entity_id, CATEGORY_SWITCH, **kwargs) - - self.hass = hass - self.entity_id = entity_id - self._domain = split_entity_id(entity_id)[0] - + super().__init__(*args, category=CATEGORY_SWITCH) + self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False serv_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = serv_switch.get_characteristic(CHAR_ON) - self.char_on.value = False - self.char_on.setter_callback = self.set_state + self.char_on = setup_char( + CHAR_ON, serv_switch, value=False, callback=self.set_state) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" @@ -40,15 +35,11 @@ class Switch(HomeAccessory): self.hass.services.call(self._domain, service, {ATTR_ENTITY_ID: self.entity_id}) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update switch state after state changed.""" - if new_state is None: - return - current_state = (new_state.state == STATE_ON) if not self.flag_target_state: _LOGGER.debug('%s: Set current state to %s', self.entity_id, current_state) self.char_on.set_value(current_state) - self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index daf81c51c4d..ce10b96c51c 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -5,12 +5,15 @@ from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO) + STATE_HEAT, STATE_COOL, STATE_AUTO, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, debounce +from .accessories import ( + HomeAccessory, add_preload_service, debounce, setup_char) from .const import ( CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, @@ -26,74 +29,63 @@ HC_HASS_TO_HOMEKIT = {STATE_OFF: 0, STATE_HEAT: 1, STATE_COOL: 2, STATE_AUTO: 3} HC_HOMEKIT_TO_HASS = {c: s for s, c in HC_HASS_TO_HOMEKIT.items()} +SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \ + SUPPORT_TARGET_TEMPERATURE_HIGH + @TYPES.register('Thermostat') class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, hass, entity_id, display_name, support_auto, **kwargs): + def __init__(self, *args, config): """Initialize a Thermostat accessory object.""" - super().__init__(display_name, entity_id, - CATEGORY_THERMOSTAT, **kwargs) - - self.hass = hass - self.entity_id = entity_id - self._call_timer = None + super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS - self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False # Add additional characteristics if auto mode is supported - extra_chars = [ - CHAR_COOLING_THRESHOLD_TEMPERATURE, - CHAR_HEATING_THRESHOLD_TEMPERATURE] if support_auto else None + self.chars = [] + features = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_TEMP_RANGE: + self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, + CHAR_HEATING_THRESHOLD_TEMPERATURE)) - # Preload the thermostat service - serv_thermostat = add_preload_service(self, SERV_THERMOSTAT, - extra_chars) + serv_thermostat = add_preload_service( + self, SERV_THERMOSTAT, self.chars) # Current and target mode characteristics - self.char_current_heat_cool = serv_thermostat. \ - get_characteristic(CHAR_CURRENT_HEATING_COOLING) - self.char_current_heat_cool.value = 0 - self.char_target_heat_cool = serv_thermostat. \ - get_characteristic(CHAR_TARGET_HEATING_COOLING) - self.char_target_heat_cool.value = 0 - self.char_target_heat_cool.setter_callback = self.set_heat_cool + self.char_current_heat_cool = setup_char( + CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0) + self.char_target_heat_cool = setup_char( + CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0, + callback=self.set_heat_cool) # Current and target temperature characteristics - self.char_current_temp = serv_thermostat. \ - get_characteristic(CHAR_CURRENT_TEMPERATURE) - self.char_current_temp.value = 21.0 - self.char_target_temp = serv_thermostat. \ - get_characteristic(CHAR_TARGET_TEMPERATURE) - self.char_target_temp.value = 21.0 - self.char_target_temp.setter_callback = self.set_target_temperature + self.char_current_temp = setup_char( + CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0) + self.char_target_temp = setup_char( + CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0, + callback=self.set_target_temperature) # Display units characteristic - self.char_display_units = serv_thermostat. \ - get_characteristic(CHAR_TEMP_DISPLAY_UNITS) - self.char_display_units.value = 0 + self.char_display_units = setup_char( + CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0) # If the device supports it: high and low temperature characteristics - if support_auto: - self.char_cooling_thresh_temp = serv_thermostat. \ - get_characteristic(CHAR_COOLING_THRESHOLD_TEMPERATURE) - self.char_cooling_thresh_temp.value = 23.0 - self.char_cooling_thresh_temp.setter_callback = \ - self.set_cooling_threshold - - self.char_heating_thresh_temp = serv_thermostat. \ - get_characteristic(CHAR_HEATING_THRESHOLD_TEMPERATURE) - self.char_heating_thresh_temp.value = 19.0 - self.char_heating_thresh_temp.setter_callback = \ - self.set_heating_threshold - else: - self.char_cooling_thresh_temp = None - self.char_heating_thresh_temp = None + self.char_cooling_thresh_temp = None + self.char_heating_thresh_temp = None + if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: + self.char_cooling_thresh_temp = setup_char( + CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat, + value=23.0, callback=self.set_cooling_threshold) + if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: + self.char_heating_thresh_temp = setup_char( + CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat, + value=19.0, callback=self.set_heating_threshold) def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" @@ -141,11 +133,8 @@ class Thermostat(HomeAccessory): self.hass.components.climate.set_temperature( temperature=value, entity_id=self.entity_id) - def update_state(self, entity_id=None, old_state=None, new_state=None): + def update_state(self, new_state): """Update security state after state changed.""" - if new_state is None: - return - self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index af2c74d9c3c..e14b6c47bc8 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -33,7 +33,7 @@ def validate_entity_config(values): return entities -def show_setup_message(bridge, hass): +def show_setup_message(hass, bridge): """Display persistent notification with setup information.""" pin = bridge.pincode.decode() _LOGGER.info('Pincode: %s', pin) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index b7bf625a2d6..f8e026483aa 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,9 +10,8 @@ from homeassistant.components.homekit.accessories import ( add_preload_service, set_accessory_info, debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - ACCESSORY_MODEL, ACCESSORY_NAME, BRIDGE_MODEL, BRIDGE_NAME, - SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, CHAR_MODEL, - CHAR_NAME, CHAR_SERIAL_NUMBER) + BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util @@ -92,7 +91,7 @@ class TestAccessories(unittest.TestCase): def test_set_accessory_info(self): """Test setting the basic accessory information.""" # Test HomeAccessory - acc = HomeAccessory() + acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '') set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') serv = acc.get_service(SERV_ACCESSORY_INFO) @@ -104,7 +103,7 @@ class TestAccessories(unittest.TestCase): serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') # Test HomeBridge - acc = HomeBridge(None) + acc = HomeBridge('hass') set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') serv = acc.get_service(SERV_ACCESSORY_INFO) @@ -116,26 +115,37 @@ class TestAccessories(unittest.TestCase): def test_home_accessory(self): """Test HomeAccessory class.""" - acc = HomeAccessory() - self.assertEqual(acc.display_name, ACCESSORY_NAME) + hass = get_test_home_assistant() + + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, '') + self.assertEqual(acc.hass, hass) + self.assertEqual(acc.display_name, 'Home Accessory') self.assertEqual(acc.category, 1) # Category.OTHER self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, ACCESSORY_MODEL) + serv.get_characteristic(CHAR_MODEL).value, 'homekit.accessory') - acc = HomeAccessory('test_name', 'test_model', 'FAN', aid=2) + hass.states.set('homekit.accessory', 'on') + hass.block_till_done() + acc.run() + hass.states.set('homekit.accessory', 'off') + hass.block_till_done() + + acc = HomeAccessory('hass', 'test_name', 'test_model', 2, '') self.assertEqual(acc.display_name, 'test_name') - self.assertEqual(acc.category, 3) # Category.FAN self.assertEqual(acc.aid, 2) self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, 'test_model') + hass.stop() + def test_home_bridge(self): """Test HomeBridge class.""" - bridge = HomeBridge(None) + bridge = HomeBridge('hass') + self.assertEqual(bridge.hass, 'hass') self.assertEqual(bridge.display_name, BRIDGE_NAME) self.assertEqual(bridge.category, 2) # Category.BRIDGE self.assertEqual(len(bridge.services), 1) @@ -144,12 +154,10 @@ class TestAccessories(unittest.TestCase): self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) - bridge = HomeBridge('hass', 'test_name', 'test_model') + bridge = HomeBridge('hass', 'test_name') self.assertEqual(bridge.display_name, 'test_name') self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'test_model') # setup_message bridge.setup_message() @@ -174,11 +182,11 @@ class TestAccessories(unittest.TestCase): self.assertEqual( mock_remove_paired_client.call_args, call('client_uuid')) - self.assertEqual(mock_show_msg.call_args, call(bridge, 'hass')) + self.assertEqual(mock_show_msg.call_args, call('hass', bridge)) def test_home_driver(self): """Test HomeDriver class.""" - bridge = HomeBridge(None) + bridge = HomeBridge('hass') ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e323431ac3f..6f2521fc4e5 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -19,14 +19,14 @@ CONFIG = {} def test_get_accessory_invalid_aid(caplog): """Test with unsupported component.""" assert get_accessory(None, State('light.demo', 'on'), - aid=None, config=None) is None + None, config=None) is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg def test_not_supported(): """Test if none is returned if entity isn't supported.""" - assert get_accessory(None, State('demo.demo', 'on'), aid=2, config=None) \ + assert get_accessory(None, State('demo.demo', 'on'), 2, config=None) \ is None @@ -48,7 +48,6 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) get_accessory(None, state, 2, {}) - # pylint: disable=invalid-name def test_sensor_temperature_fahrenheit(self): """Test temperature sensor with Fahrenheit as unit.""" with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): @@ -91,8 +90,9 @@ class TestGetAccessories(unittest.TestCase): get_accessory(None, state, 2, config) # pylint: disable=unsubscriptable-object + print(self.mock_type.call_args[1]) self.assertEqual( - self.mock_type.call_args[1].get('alarm_code'), '1234') + self.mock_type.call_args[1]['config'][ATTR_CODE], '1234') def test_climate(self): """Test climate devices.""" @@ -100,10 +100,6 @@ class TestGetAccessories(unittest.TestCase): state = State('climate.test', 'auto') get_accessory(None, state, 2, {}) - # pylint: disable=unsubscriptable-object - self.assertEqual( - self.mock_type.call_args[0][-1], False) # support_auto - def test_light(self): """Test light devices.""" with patch.dict(TYPES, {'Light': self.mock_type}): @@ -119,10 +115,6 @@ class TestGetAccessories(unittest.TestCase): SUPPORT_TARGET_TEMPERATURE_HIGH}) get_accessory(None, state, 2, {}) - # pylint: disable=unsubscriptable-object - self.assertEqual( - self.mock_type.call_args[0][-1], True) # support_auto - def test_switch(self): """Test switch.""" with patch.dict(TYPES, {'Switch': self.mock_type}): @@ -140,3 +132,9 @@ class TestGetAccessories(unittest.TestCase): with patch.dict(TYPES, {'Switch': self.mock_type}): state = State('input_boolean.test', 'on') get_accessory(None, state, 2, {}) + + def test_lock(self): + """Test lock.""" + with patch.dict(TYPES, {'Lock': self.mock_type}): + state = State('lock.test', 'locked') + get_accessory(None, state, 2, {}) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 51a965b5817..d1ad232d279 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -173,7 +173,7 @@ class TestHomeKit(unittest.TestCase): self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) self.assertEqual(mock_show_setup_msg.mock_calls, [ - call(homekit.bridge, self.hass)]) + call(self.hass, homekit.bridge)]) self.assertEqual(homekit.driver.mock_calls, [call.start()]) self.assertTrue(homekit.started) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 1fa1ef1728e..8e26ab519d1 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -35,7 +35,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - acc = WindowCovering(self.hass, window_cover, 'Cover', aid=2) + acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index af8676dfd74..10bf469c08d 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -50,9 +50,11 @@ class TestHomekitLights(unittest.TestCase): def test_light_basic(self): """Test light with char state.""" entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) - acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) + self.hass.block_till_done() + acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) self.assertEqual(acc.aid, 2) self.assertEqual(acc.category, 5) # Lightbulb self.assertEqual(acc.char_on.value, 0) @@ -94,9 +96,11 @@ class TestHomekitLights(unittest.TestCase): def test_light_brightness(self): """Test light with brightness.""" entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) - acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) + self.hass.block_till_done() + acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) self.assertEqual(acc.char_brightness.value, 0) acc.run() @@ -135,10 +139,12 @@ class TestHomekitLights(unittest.TestCase): def test_light_color_temperature(self): """Test light with color temperature.""" entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) - acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) + self.hass.block_till_done() + acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) self.assertEqual(acc.char_color_temperature.value, 153) acc.run() @@ -157,10 +163,12 @@ class TestHomekitLights(unittest.TestCase): def test_light_rgb_color(self): """Test light with rgb_color.""" entity_id = 'light.demo' + self.hass.states.set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) - acc = self.light_cls(self.hass, entity_id, 'Light', aid=2) + self.hass.block_till_done() + acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) self.assertEqual(acc.char_hue.value, 0) self.assertEqual(acc.char_saturation.value, 75) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index d19bcdf3ec5..b2053116060 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -33,7 +33,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" kitchen_lock = 'lock.kitchen_door' - acc = Lock(self.hass, kitchen_lock, 'Lock', aid=2) + acc = Lock(self.hass, 'Lock', kitchen_lock, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 46f886c4d35..ec538ce4b50 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -35,8 +35,8 @@ class TestHomekitSecuritySystems(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" acp = 'alarm_control_panel.test' - acc = SecuritySystem(self.hass, acp, 'SecuritySystem', - alarm_code='1234', aid=2) + acc = SecuritySystem(self.hass, 'SecuritySystem', acp, + 2, config={ATTR_CODE: '1234'}) acc.run() self.assertEqual(acc.aid, 2) @@ -107,8 +107,8 @@ class TestHomekitSecuritySystems(unittest.TestCase): """Test accessory if security_system doesn't require a alarm_code.""" acp = 'alarm_control_panel.test' - acc = SecuritySystem(self.hass, acp, 'SecuritySystem', - alarm_code=None, aid=2) + acc = SecuritySystem(self.hass, 'SecuritySystem', acp, + 2, config={ATTR_CODE: None}) acc.run() # Set from HomeKit diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index a6e178bb226..f9dfb04b37c 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -26,7 +26,8 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' - acc = TemperatureSensor(self.hass, entity_id, 'Temperature', aid=2) + acc = TemperatureSensor(self.hass, 'Temperature', entity_id, + 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -54,7 +55,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' - acc = HumiditySensor(self.hass, entity_id, 'Humidity', aid=2) + acc = HumiditySensor(self.hass, 'Humidity', entity_id, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -78,7 +79,8 @@ class TestHomekitSensors(unittest.TestCase): {ATTR_DEVICE_CLASS: "opening"}) self.hass.block_till_done() - acc = BinarySensor(self.hass, entity_id, 'Window Opening', aid=2) + acc = BinarySensor(self.hass, 'Window Opening', entity_id, + 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -118,6 +120,7 @@ class TestHomekitSensors(unittest.TestCase): {ATTR_DEVICE_CLASS: device_class}) self.hass.block_till_done() - acc = BinarySensor(self.hass, entity_id, 'Binary Sensor', aid=2) + acc = BinarySensor(self.hass, 'Binary Sensor', entity_id, + 2, config=None) self.assertEqual(acc.get_service(service).display_name, service) self.assertEqual(acc.char_detected.display_name, char) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 7f30e457308..65b107e24cd 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -34,7 +34,7 @@ class TestHomekitSwitches(unittest.TestCase): entity_id = 'switch.test' domain = split_entity_id(entity_id)[0] - acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -70,7 +70,7 @@ class TestHomekitSwitches(unittest.TestCase): entity_id = 'remote.test' domain = split_entity_id(entity_id)[0] - acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) acc.run() self.assertEqual(acc.char_on.value, False) @@ -89,7 +89,7 @@ class TestHomekitSwitches(unittest.TestCase): entity_id = 'input_boolean.test' domain = split_entity_id(entity_id)[0] - acc = Switch(self.hass, entity_id, 'Switch', aid=2) + acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) acc.run() self.assertEqual(acc.char_on.value, False) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index feea5c0d01a..adc3fb018f8 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -7,8 +7,9 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) from homeassistant.const import ( - ATTR_SERVICE, EVENT_CALL_SERVICE, ATTR_SERVICE_DATA, - ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce @@ -52,7 +53,10 @@ class TestHomekitThermostats(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = self.thermostat_cls(self.hass, climate, 'Climate', False, aid=2) + self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + self.hass.block_till_done() + acc = self.thermostat_cls(self.hass, 'Climate', climate, + 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -187,7 +191,11 @@ class TestHomekitThermostats(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = self.thermostat_cls(self.hass, climate, 'Climate', True) + # support_auto = True + self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + self.hass.block_till_done() + acc = self.thermostat_cls(self.hass, 'Climate', climate, + 2, config=None) acc.run() self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) @@ -257,7 +265,11 @@ class TestHomekitThermostats(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' - acc = self.thermostat_cls(self.hass, climate, 'Climate', True) + # support_auto = True + self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + self.hass.block_till_done() + acc = self.thermostat_cls(self.hass, 'Climate', climate, + 2, config=None) acc.run() self.hass.states.set(climate, STATE_AUTO, diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index d6ef5856f85..7465e9affab 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -58,7 +58,7 @@ class TestUtil(unittest.TestCase): """Test show setup message as persistence notification.""" bridge = HomeBridge(self.hass) - show_setup_message(bridge, self.hass) + show_setup_message(self.hass, bridge) self.hass.block_till_done() data = self.events[0].data From 9c1bc18defbc92a587d3508bb568ef1e17cddbce Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 12 Apr 2018 02:58:57 +0200 Subject: [PATCH 344/924] Fix too green color conversion (#13828) * Prepare test * Fix too green color conversion * Fix remaining tests --- homeassistant/util/color.py | 2 +- tests/components/light/test_demo.py | 8 ++-- tests/components/light/test_mqtt.py | 10 ++-- tests/components/light/test_mqtt_json.py | 2 +- tests/components/switch/test_flux.py | 58 ++++++++++++------------ tests/util/test_color.py | 20 ++++---- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index c2e4ac737e8..32e9df70a03 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -203,7 +203,7 @@ def color_RGB_to_xy_brightness( # Wide RGB D65 conversion formula X = R * 0.664511 + G * 0.154324 + B * 0.162028 - Y = R * 0.313881 + G * 0.668433 + B * 0.047685 + Y = R * 0.283881 + G * 0.668433 + B * 0.047685 Z = R * 0.000088 + G * 0.072310 + B * 0.986039 # Convert XYZ to xy diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 963cda6abc4..8ba6385166b 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -29,15 +29,15 @@ class TestDemoLight(unittest.TestCase): def test_state_attributes(self): """Test light state attributes.""" light.turn_on( - self.hass, ENTITY_LIGHT, xy_color=(.4, .6), brightness=25) + self.hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((0.378, 0.574), state.attributes.get( + self.assertEqual((0.4, 0.4), state.attributes.get( light.ATTR_XY_COLOR)) self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( - (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + (255, 234, 164), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), @@ -48,7 +48,7 @@ class TestDemoLight(unittest.TestCase): self.assertEqual( (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual( - (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR)) + (0.319, 0.326), state.attributes.get(light.ATTR_XY_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 71fe77ef6be..7f7841b1a69 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -255,7 +255,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) self.assertEqual(255, state.attributes.get('white_value')) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb/status', '0') self.hass.block_till_done() @@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual((0.652, 0.343), + self.assertEqual((0.672, 0.324), light_state.attributes.get('xy_color')) def test_brightness_controlling_scale(self): @@ -519,7 +519,7 @@ class TestLightMQTT(unittest.TestCase): mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False), + mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), ], any_order=True) state = self.hass.states.get('light.test') @@ -527,7 +527,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.32, 0.336), state.attributes['xy_color']) + self.assertEqual((0.323, 0.329), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) def test_on_command_first(self): """Test on command being sent before brightness.""" diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index a183355fbb3..d6835b00be0 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -206,7 +206,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(155, state.attributes.get('color_temp')) self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index a1e600860f9..c42061db958 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -154,8 +154,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset(self): @@ -201,8 +201,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) # pylint: disable=invalid-name def test_flux_after_sunset_before_stop(self): @@ -249,8 +249,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 153) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 146) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise(self): @@ -296,8 +296,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_with_custom_start_stop_times(self): @@ -345,8 +345,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 154) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.494, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 147) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.504, 0.385]) def test_flux_before_sunrise_stop_next_day(self): """Test the flux switch before sunrise. @@ -395,8 +395,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset_stop_next_day(self): @@ -447,8 +447,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) # pylint: disable=invalid-name def test_flux_after_sunset_before_midnight_stop_next_day(self): @@ -498,8 +498,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 126) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.574, 0.401]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.588, 0.386]) # pylint: disable=invalid-name def test_flux_after_sunset_after_midnight_stop_next_day(self): @@ -549,8 +549,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 122) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.586, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 114) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.601, 0.382]) # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise_stop_next_day(self): @@ -600,8 +600,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_with_custom_colortemps(self): @@ -650,8 +650,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 167) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.461, 0.389]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 159) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.469, 0.378]) # pylint: disable=invalid-name def test_flux_with_custom_brightness(self): @@ -700,7 +700,7 @@ class TestSwitchFlux(unittest.TestCase): self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) def test_flux_with_multiple_lights(self): """Test the flux switch with multiple light entities.""" @@ -762,14 +762,14 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) call = turn_on_calls[-2] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) call = turn_on_calls[-3] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) def test_flux_with_mired(self): """Test the flux switch´s mode mired.""" diff --git a/tests/util/test_color.py b/tests/util/test_color.py index b64cf0acf80..74ba72cd3d1 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -14,7 +14,7 @@ class TestColorUtil(unittest.TestCase): """Test color_RGB_to_xy_brightness.""" self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy_brightness(0, 0, 0)) - self.assertEqual((0.32, 0.336, 255), + self.assertEqual((0.323, 0.329, 255), color_util.color_RGB_to_xy_brightness(255, 255, 255)) self.assertEqual((0.136, 0.04, 12), @@ -23,17 +23,17 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.172, 0.747, 170), color_util.color_RGB_to_xy_brightness(0, 255, 0)) - self.assertEqual((0.679, 0.321, 80), + self.assertEqual((0.701, 0.299, 72), color_util.color_RGB_to_xy_brightness(255, 0, 0)) - self.assertEqual((0.679, 0.321, 17), + self.assertEqual((0.701, 0.299, 16), color_util.color_RGB_to_xy_brightness(128, 0, 0)) def test_color_RGB_to_xy(self): """Test color_RGB_to_xy.""" self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0)) - self.assertEqual((0.32, 0.336), + self.assertEqual((0.323, 0.329), color_util.color_RGB_to_xy(255, 255, 255)) self.assertEqual((0.136, 0.04), @@ -42,10 +42,10 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.172, 0.747), color_util.color_RGB_to_xy(0, 255, 0)) - self.assertEqual((0.679, 0.321), + self.assertEqual((0.701, 0.299), color_util.color_RGB_to_xy(255, 0, 0)) - self.assertEqual((0.679, 0.321), + self.assertEqual((0.701, 0.299), color_util.color_RGB_to_xy(128, 0, 0)) def test_color_xy_brightness_to_RGB(self): @@ -155,16 +155,16 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.151, 0.343), color_util.color_hs_to_xy(180, 100)) - self.assertEqual((0.352, 0.329), + self.assertEqual((0.356, 0.321), color_util.color_hs_to_xy(350, 12.5)) - self.assertEqual((0.228, 0.476), + self.assertEqual((0.229, 0.474), color_util.color_hs_to_xy(140, 50)) - self.assertEqual((0.465, 0.33), + self.assertEqual((0.474, 0.317), color_util.color_hs_to_xy(0, 40)) - self.assertEqual((0.32, 0.336), + self.assertEqual((0.323, 0.329), color_util.color_hs_to_xy(360, 0)) def test_rgb_hex_to_rgb_list(self): From b752ca3bef01acbb433d5f9e571e5a0601ac9f1b Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Thu, 12 Apr 2018 09:24:07 +0200 Subject: [PATCH 345/924] Rename from aurora light to nanoleaf_aurora (#13831) --- .coveragerc | 2 +- .../components/light/{aurora.py => nanoleaf_aurora.py} | 0 requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/light/{aurora.py => nanoleaf_aurora.py} (100%) diff --git a/.coveragerc b/.coveragerc index 666134488fe..2b733dd699f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -410,7 +410,6 @@ omit = homeassistant/components/image_processing/seven_segments.py homeassistant/components/keyboard_remote.py homeassistant/components/keyboard.py - homeassistant/components/light/aurora.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py @@ -425,6 +424,7 @@ omit = homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py + homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/osramlightify.py homeassistant/components/light/piglow.py homeassistant/components/light/rpi_gpio_pwm.py diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/nanoleaf_aurora.py similarity index 100% rename from homeassistant/components/light/aurora.py rename to homeassistant/components/light/nanoleaf_aurora.py diff --git a/requirements_all.txt b/requirements_all.txt index 1b3d3206c60..86cff3f9420 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ myusps==1.3.2 # homeassistant.components.media_player.nadtcp nad_receiver==0.0.9 -# homeassistant.components.light.aurora +# homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 # homeassistant.components.discovery From dd7e6edf6132560f6085a6a8c7f4bada0f618fc8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 12 Apr 2018 13:19:21 +0200 Subject: [PATCH 346/924] HomeKit type_cover fix (#13832) * Removed char_position_state * Changed service call --- homeassistant/components/homekit/const.py | 6 ++-- .../components/homekit/type_covers.py | 31 +++++++------------ tests/components/homekit/test_type_covers.py | 5 --- 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 80f2fd039e6..37ee9722bc4 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -51,6 +51,7 @@ SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' +# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### @@ -61,7 +62,7 @@ CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' @@ -77,12 +78,11 @@ CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean -CHAR_POSITION_STATE = 'PositionState' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' +CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 7c7ab3e3683..d8a6a8c2fdc 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,13 +1,15 @@ """Class to hold all cover accessories.""" import logging -from homeassistant.components.cover import ATTR_CURRENT_POSITION +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION from . import TYPES from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, - CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE) + CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION) _LOGGER = logging.getLogger(__name__) @@ -22,7 +24,6 @@ class WindowCovering(HomeAccessory): def __init__(self, *args, config): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) - self.current_position = None self.homekit_target = None serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) @@ -31,29 +32,21 @@ class WindowCovering(HomeAccessory): self.char_target_position = setup_char( CHAR_TARGET_POSITION, serv_cover, value=0, callback=self.move_cover) - self.char_position_state = setup_char( - CHAR_POSITION_STATE, serv_cover, value=0) def move_cover(self, value): """Move cover to value if call came from HomeKit.""" - if value != self.current_position: - _LOGGER.debug('%s: Set position to %d', self.entity_id, value) - self.homekit_target = value - if value > self.current_position: - self.char_position_state.set_value(1) - elif value < self.current_position: - self.char_position_state.set_value(0) - self.hass.components.cover.set_cover_position( - value, self.entity_id) + _LOGGER.debug('%s: Set position to %d', self.entity_id, value) + self.homekit_target = value + + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} + self.hass.services.call(DOMAIN, SERVICE_SET_COVER_POSITION, params) def update_state(self, new_state): """Update cover position after state changed.""" current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, int): - self.current_position = current_position - self.char_current_position.set_value(self.current_position) + self.char_current_position.set_value(current_position) if self.homekit_target is None or \ - abs(self.current_position - self.homekit_target) < 6: - self.char_target_position.set_value(self.current_position) - self.char_position_state.set_value(2) + abs(current_position - self.homekit_target) < 6: + self.char_target_position.set_value(current_position) self.homekit_target = None diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8e26ab519d1..43e82e74b1a 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -43,7 +43,6 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_current_position.value, 0) self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 0) self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_CURRENT_POSITION: None}) @@ -51,7 +50,6 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_current_position.value, 0) self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 0) self.hass.states.set(window_cover, STATE_OPEN, {ATTR_CURRENT_POSITION: 50}) @@ -59,7 +57,6 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_current_position.value, 50) self.assertEqual(acc.char_target_position.value, 50) - self.assertEqual(acc.char_position_state.value, 2) # Set from HomeKit acc.char_target_position.client_update_value(25) @@ -71,7 +68,6 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_current_position.value, 50) self.assertEqual(acc.char_target_position.value, 25) - self.assertEqual(acc.char_position_state.value, 0) # Set from HomeKit acc.char_target_position.client_update_value(75) @@ -83,4 +79,3 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_current_position.value, 50) self.assertEqual(acc.char_target_position.value, 75) - self.assertEqual(acc.char_position_state.value, 1) From bd58a0de7dab6dc8119ddea5878f27ed1fb25c1e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 Apr 2018 21:21:26 -0400 Subject: [PATCH 347/924] Remove vendor lookup for mac addresses (#13788) * Remove vendor lookup for mac addresses * Fix tests --- .../components/device_tracker/__init__.py | 61 +-------- tests/components/device_tracker/test_init.py | 128 +----------------- tests/conftest.py | 4 +- 3 files changed, 6 insertions(+), 187 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 682496335a0..45f0e51a214 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -9,8 +9,6 @@ from datetime import timedelta import logging from typing import Any, List, Sequence, Callable -import aiohttp -import async_timeout import voluptuous as vol from homeassistant.setup import async_prepare_setup_platform @@ -19,7 +17,6 @@ from homeassistant.loader import bind_hass from homeassistant.components import group, zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval @@ -76,7 +73,6 @@ ATTR_LOCATION_NAME = 'location_name' ATTR_MAC = 'mac' ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' -ATTR_VENDOR = 'vendor' ATTR_CONSIDER_HOME = 'consider_home' SOURCE_TYPE_GPS = 'gps' @@ -328,14 +324,10 @@ class DeviceTracker(object): self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) - # lookup mac vendor string to be stored in config - yield from device.set_vendor_for_mac() - self.hass.bus.async_fire(EVENT_NEW_DEVICE, { ATTR_ENTITY_ID: device.entity_id, ATTR_HOST_NAME: device.host_name, ATTR_MAC: device.mac, - ATTR_VENDOR: device.vendor, }) # update known_devices.yaml @@ -413,7 +405,6 @@ class Device(Entity): consider_home = None # type: dt_util.dt.timedelta battery = None # type: int attributes = None # type: dict - vendor = None # type: str icon = None # type: str # Track if the last update of this device was HOME. @@ -423,7 +414,7 @@ class Device(Entity): def __init__(self, hass: HomeAssistantType, consider_home: timedelta, track: bool, dev_id: str, mac: str, name: str = None, picture: str = None, gravatar: str = None, icon: str = None, - hide_if_away: bool = False, vendor: str = None) -> None: + hide_if_away: bool = False) -> None: """Initialize a device.""" self.hass = hass self.entity_id = ENTITY_ID_FORMAT.format(dev_id) @@ -451,7 +442,6 @@ class Device(Entity): self.icon = icon self.away_hide = hide_if_away - self.vendor = vendor self.source_type = None @@ -567,51 +557,6 @@ class Device(Entity): self._state = STATE_HOME self.last_update_home = True - @asyncio.coroutine - def set_vendor_for_mac(self): - """Set vendor string using api.macvendors.com.""" - self.vendor = yield from self.get_vendor_for_mac() - - @asyncio.coroutine - def get_vendor_for_mac(self): - """Try to find the vendor string for a given MAC address.""" - if not self.mac: - return None - - if '_' in self.mac: - _, mac = self.mac.split('_', 1) - else: - mac = self.mac - - if not len(mac.split(':')) == 6: - return 'unknown' - - # We only need the first 3 bytes of the MAC for a lookup - # this improves somewhat on privacy - oui_bytes = mac.split(':')[0:3] - # bytes like 00 get truncates to 0, API needs full bytes - oui = '{:02x}:{:02x}:{:02x}'.format(*[int(b, 16) for b in oui_bytes]) - url = 'http://api.macvendors.com/' + oui - try: - websession = async_get_clientsession(self.hass) - - with async_timeout.timeout(5, loop=self.hass.loop): - resp = yield from websession.get(url) - # mac vendor found, response is the string - if resp.status == 200: - vendor_string = yield from resp.text() - return vendor_string - # If vendor is not known to the API (404) or there - # was a failure during the lookup (500); set vendor - # to something other then None to prevent retry - # as the value is only relevant when it is to be stored - # in the 'known_devices.yaml' file which only happens - # the first time the device is seen. - return 'unknown' - except (asyncio.TimeoutError, aiohttp.ClientError): - # Same as above - return 'unknown' - @asyncio.coroutine def async_added_to_hass(self): """Add an entity.""" @@ -685,7 +630,6 @@ def async_load_config(path: str, hass: HomeAssistantType, vol.Optional('picture', default=None): vol.Any(None, cv.string), vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( cv.time_period, cv.positive_timedelta), - vol.Optional('vendor', default=None): vol.Any(None, cv.string), }) try: result = [] @@ -697,6 +641,8 @@ def async_load_config(path: str, hass: HomeAssistantType, return [] for dev_id, device in devices.items(): + # Deprecated option. We just ignore it to avoid breaking change + device.pop('vendor', None) try: device = dev_schema(device) device['dev_id'] = cv.slugify(dev_id) @@ -772,7 +718,6 @@ def update_config(path: str, dev_id: str, device: Device): 'picture': device.config_picture, 'track': device.track, CONF_AWAY_HIDE: device.away_hide, - 'vendor': device.vendor, }} out.write('\n') out.write(dump(device)) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index c051983d8fa..912bd315ecd 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -24,9 +24,7 @@ from homeassistant.remote import JSONEncoder from tests.common import ( get_test_home_assistant, fire_time_changed, - patch_yaml_files, assert_setup_component, mock_restore_cache, mock_coro) - -from ...test_util.aiohttp import mock_aiohttp_client + patch_yaml_files, assert_setup_component, mock_restore_cache) TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: 'test'}} @@ -111,7 +109,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): self.assertEqual(device.config_picture, config.config_picture) self.assertEqual(device.away_hide, config.away_hide) self.assertEqual(device.consider_home, config.consider_home) - self.assertEqual(device.vendor, config.vendor) self.assertEqual(device.icon, config.icon) # pylint: disable=invalid-name @@ -173,124 +170,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): "55502f40dc8b7c769880b10874abc9d0.jpg?s=80&d=wavatar") self.assertEqual(device.config_picture, gravatar_url) - def test_mac_vendor_lookup(self): - """Test if vendor string is lookup on macvendors API.""" - mac = 'B8:27:EB:00:00:00' - vendor_string = 'Raspberry Pi Foundation' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - assert aioclient_mock.call_count == 1 - - self.assertEqual(device.vendor, vendor_string) - - def test_mac_vendor_mac_formats(self): - """Verify all variations of MAC addresses are handled correctly.""" - vendor_string = 'Raspberry Pi Foundation' - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - aioclient_mock.get('http://api.macvendors.com/00:27:eb', - text=vendor_string) - - mac = 'B8:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - mac = '0:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - mac = 'PREFIXED_B8:27:EB:00:00:00' - device = device_tracker.Device( - self.hass, timedelta(seconds=180), - True, 'test', mac, 'Test name') - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - self.assertEqual(device.vendor, vendor_string) - - def test_mac_vendor_lookup_unknown(self): - """Prevent another mac vendor lookup if was not found first time.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - status=404) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_error(self): - """Prevent another lookup if failure during API call.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - status=500) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_exception(self): - """Prevent another lookup if exception during API call.""" - mac = 'B8:27:EB:00:00:00' - - device = device_tracker.Device( - self.hass, timedelta(seconds=180), True, 'test', mac, 'Test name') - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - exc=asyncio.TimeoutError()) - - run_coroutine_threadsafe(device.set_vendor_for_mac(), - self.hass.loop).result() - - self.assertEqual(device.vendor, 'unknown') - - def test_mac_vendor_lookup_on_see(self): - """Test if macvendor is looked up when device is seen.""" - mac = 'B8:27:EB:00:00:00' - vendor_string = 'Raspberry Pi Foundation' - - tracker = device_tracker.DeviceTracker( - self.hass, timedelta(seconds=60), 0, {}, []) - - with mock_aiohttp_client() as aioclient_mock: - aioclient_mock.get('http://api.macvendors.com/b8:27:eb', - text=vendor_string) - - run_coroutine_threadsafe( - tracker.async_see(mac=mac), self.hass.loop).result() - assert aioclient_mock.call_count == 1, \ - 'No http request for macvendor made!' - self.assertEqual(tracker.devices['b827eb000000'].vendor, vendor_string) - @patch( 'homeassistant.components.device_tracker.DeviceTracker.see') @patch( @@ -463,7 +342,6 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'entity_id': 'device_tracker.hello', 'host_name': 'hello', 'mac': 'MAC_1', - 'vendor': 'unknown', } # pylint: disable=invalid-name @@ -495,9 +373,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): timedelta(seconds=0)) assert len(config) == 0 - @patch('homeassistant.components.device_tracker.Device' - '.set_vendor_for_mac', return_value=mock_coro()) - def test_see_state(self, mock_set_vendor): + def test_see_state(self): """Test device tracker see records state correctly.""" self.assertTrue(setup_component(self.hass, device_tracker.DOMAIN, TEST_PLATFORM)) diff --git a/tests/conftest.py b/tests/conftest.py index 8f0ca787721..269d460ebb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -123,7 +123,5 @@ def mock_device_tracker_conf(): ), patch( 'homeassistant.components.device_tracker.async_load_config', side_effect=lambda *args: mock_coro(devices) - ), patch('homeassistant.components.device_tracker' - '.Device.set_vendor_for_mac'): - + ): yield devices From 09dbd94467490e2d780181a70f00d709913400ee Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Tue, 10 Apr 2018 14:11:00 -0400 Subject: [PATCH 348/924] iglo hs color fix (#13808) --- homeassistant/components/light/iglo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/iglo.py b/homeassistant/components/light/iglo.py index 77e3972968c..f40dc2ce84e 100644 --- a/homeassistant/components/light/iglo.py +++ b/homeassistant/components/light/iglo.py @@ -79,7 +79,7 @@ class IGloLamp(Light): @property def hs_color(self): """Return the hs value.""" - return color_util.color_RGB_to_hsv(*self._lamp.state()['rgb']) + return color_util.color_RGB_to_hs(*self._lamp.state()['rgb']) @property def effect(self): From 234495ed0549cd33490ac41329fe26bf0099c645 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 12 Apr 2018 02:58:57 +0200 Subject: [PATCH 349/924] Fix too green color conversion (#13828) * Prepare test * Fix too green color conversion * Fix remaining tests --- homeassistant/util/color.py | 2 +- tests/components/light/test_demo.py | 8 ++-- tests/components/light/test_mqtt.py | 10 ++-- tests/components/light/test_mqtt_json.py | 2 +- tests/components/switch/test_flux.py | 58 ++++++++++++------------ tests/util/test_color.py | 20 ++++---- 6 files changed, 50 insertions(+), 50 deletions(-) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index c2e4ac737e8..32e9df70a03 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -203,7 +203,7 @@ def color_RGB_to_xy_brightness( # Wide RGB D65 conversion formula X = R * 0.664511 + G * 0.154324 + B * 0.162028 - Y = R * 0.313881 + G * 0.668433 + B * 0.047685 + Y = R * 0.283881 + G * 0.668433 + B * 0.047685 Z = R * 0.000088 + G * 0.072310 + B * 0.986039 # Convert XYZ to xy diff --git a/tests/components/light/test_demo.py b/tests/components/light/test_demo.py index 963cda6abc4..8ba6385166b 100644 --- a/tests/components/light/test_demo.py +++ b/tests/components/light/test_demo.py @@ -29,15 +29,15 @@ class TestDemoLight(unittest.TestCase): def test_state_attributes(self): """Test light state attributes.""" light.turn_on( - self.hass, ENTITY_LIGHT, xy_color=(.4, .6), brightness=25) + self.hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) self.assertTrue(light.is_on(self.hass, ENTITY_LIGHT)) - self.assertEqual((0.378, 0.574), state.attributes.get( + self.assertEqual((0.4, 0.4), state.attributes.get( light.ATTR_XY_COLOR)) self.assertEqual(25, state.attributes.get(light.ATTR_BRIGHTNESS)) self.assertEqual( - (207, 255, 0), state.attributes.get(light.ATTR_RGB_COLOR)) + (255, 234, 164), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual('rainbow', state.attributes.get(light.ATTR_EFFECT)) light.turn_on( self.hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), @@ -48,7 +48,7 @@ class TestDemoLight(unittest.TestCase): self.assertEqual( (250, 252, 255), state.attributes.get(light.ATTR_RGB_COLOR)) self.assertEqual( - (0.316, 0.333), state.attributes.get(light.ATTR_XY_COLOR)) + (0.319, 0.326), state.attributes.get(light.ATTR_XY_COLOR)) light.turn_on(self.hass, ENTITY_LIGHT, color_temp=400, effect='none') self.hass.block_till_done() state = self.hass.states.get(ENTITY_LIGHT) diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 71fe77ef6be..7f7841b1a69 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -255,7 +255,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual(150, state.attributes.get('color_temp')) self.assertEqual('none', state.attributes.get('effect')) self.assertEqual(255, state.attributes.get('white_value')) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) fire_mqtt_message(self.hass, 'test_light_rgb/status', '0') self.hass.block_till_done() @@ -311,7 +311,7 @@ class TestLightMQTT(unittest.TestCase): self.hass.block_till_done() light_state = self.hass.states.get('light.test') - self.assertEqual((0.652, 0.343), + self.assertEqual((0.672, 0.324), light_state.attributes.get('xy_color')) def test_brightness_controlling_scale(self): @@ -519,7 +519,7 @@ class TestLightMQTT(unittest.TestCase): mock.call('test_light_rgb/rgb/set', '50,50,50', 2, False), mock.call('test_light_rgb/brightness/set', 50, 2, False), mock.call('test_light_rgb/white_value/set', 80, 2, False), - mock.call('test_light_rgb/xy/set', '0.32,0.336', 2, False), + mock.call('test_light_rgb/xy/set', '0.323,0.329', 2, False), ], any_order=True) state = self.hass.states.get('light.test') @@ -527,7 +527,7 @@ class TestLightMQTT(unittest.TestCase): self.assertEqual((255, 255, 255), state.attributes['rgb_color']) self.assertEqual(50, state.attributes['brightness']) self.assertEqual(80, state.attributes['white_value']) - self.assertEqual((0.32, 0.336), state.attributes['xy_color']) + self.assertEqual((0.323, 0.329), state.attributes['xy_color']) def test_sending_mqtt_rgb_command_with_template(self): """Test the sending of RGB command with template.""" @@ -679,7 +679,7 @@ class TestLightMQTT(unittest.TestCase): state = self.hass.states.get('light.test') self.assertEqual(STATE_ON, state.state) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) def test_on_command_first(self): """Test on command being sent before brightness.""" diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index a183355fbb3..d6835b00be0 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -206,7 +206,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(155, state.attributes.get('color_temp')) self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) - self.assertEqual((0.32, 0.336), state.attributes.get('xy_color')) + self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index a1e600860f9..c42061db958 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -154,8 +154,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset(self): @@ -201,8 +201,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) # pylint: disable=invalid-name def test_flux_after_sunset_before_stop(self): @@ -249,8 +249,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 153) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 146) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise(self): @@ -296,8 +296,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_with_custom_start_stop_times(self): @@ -345,8 +345,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 154) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.494, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 147) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.504, 0.385]) def test_flux_before_sunrise_stop_next_day(self): """Test the flux switch before sunrise. @@ -395,8 +395,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset_stop_next_day(self): @@ -447,8 +447,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 180) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.431, 0.38]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 173) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.439, 0.37]) # pylint: disable=invalid-name def test_flux_after_sunset_before_midnight_stop_next_day(self): @@ -498,8 +498,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 126) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.574, 0.401]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.588, 0.386]) # pylint: disable=invalid-name def test_flux_after_sunset_after_midnight_stop_next_day(self): @@ -549,8 +549,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 122) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.586, 0.397]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 114) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.601, 0.382]) # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise_stop_next_day(self): @@ -600,8 +600,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 119) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.591, 0.395]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 112) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.606, 0.379]) # pylint: disable=invalid-name def test_flux_with_custom_colortemps(self): @@ -650,8 +650,8 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 167) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.461, 0.389]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 159) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.469, 0.378]) # pylint: disable=invalid-name def test_flux_with_custom_brightness(self): @@ -700,7 +700,7 @@ class TestSwitchFlux(unittest.TestCase): self.hass.block_till_done() call = turn_on_calls[-1] self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 255) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.496, 0.397]) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.506, 0.385]) def test_flux_with_multiple_lights(self): """Test the flux switch with multiple light entities.""" @@ -762,14 +762,14 @@ class TestSwitchFlux(unittest.TestCase): fire_time_changed(self.hass, test_time) self.hass.block_till_done() call = turn_on_calls[-1] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) call = turn_on_calls[-2] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) call = turn_on_calls[-3] - self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 171) - self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.452, 0.386]) + self.assertEqual(call.data[light.ATTR_BRIGHTNESS], 163) + self.assertEqual(call.data[light.ATTR_XY_COLOR], [0.46, 0.376]) def test_flux_with_mired(self): """Test the flux switch´s mode mired.""" diff --git a/tests/util/test_color.py b/tests/util/test_color.py index b64cf0acf80..74ba72cd3d1 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -14,7 +14,7 @@ class TestColorUtil(unittest.TestCase): """Test color_RGB_to_xy_brightness.""" self.assertEqual((0, 0, 0), color_util.color_RGB_to_xy_brightness(0, 0, 0)) - self.assertEqual((0.32, 0.336, 255), + self.assertEqual((0.323, 0.329, 255), color_util.color_RGB_to_xy_brightness(255, 255, 255)) self.assertEqual((0.136, 0.04, 12), @@ -23,17 +23,17 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.172, 0.747, 170), color_util.color_RGB_to_xy_brightness(0, 255, 0)) - self.assertEqual((0.679, 0.321, 80), + self.assertEqual((0.701, 0.299, 72), color_util.color_RGB_to_xy_brightness(255, 0, 0)) - self.assertEqual((0.679, 0.321, 17), + self.assertEqual((0.701, 0.299, 16), color_util.color_RGB_to_xy_brightness(128, 0, 0)) def test_color_RGB_to_xy(self): """Test color_RGB_to_xy.""" self.assertEqual((0, 0), color_util.color_RGB_to_xy(0, 0, 0)) - self.assertEqual((0.32, 0.336), + self.assertEqual((0.323, 0.329), color_util.color_RGB_to_xy(255, 255, 255)) self.assertEqual((0.136, 0.04), @@ -42,10 +42,10 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.172, 0.747), color_util.color_RGB_to_xy(0, 255, 0)) - self.assertEqual((0.679, 0.321), + self.assertEqual((0.701, 0.299), color_util.color_RGB_to_xy(255, 0, 0)) - self.assertEqual((0.679, 0.321), + self.assertEqual((0.701, 0.299), color_util.color_RGB_to_xy(128, 0, 0)) def test_color_xy_brightness_to_RGB(self): @@ -155,16 +155,16 @@ class TestColorUtil(unittest.TestCase): self.assertEqual((0.151, 0.343), color_util.color_hs_to_xy(180, 100)) - self.assertEqual((0.352, 0.329), + self.assertEqual((0.356, 0.321), color_util.color_hs_to_xy(350, 12.5)) - self.assertEqual((0.228, 0.476), + self.assertEqual((0.229, 0.474), color_util.color_hs_to_xy(140, 50)) - self.assertEqual((0.465, 0.33), + self.assertEqual((0.474, 0.317), color_util.color_hs_to_xy(0, 40)) - self.assertEqual((0.32, 0.336), + self.assertEqual((0.323, 0.329), color_util.color_hs_to_xy(360, 0)) def test_rgb_hex_to_rgb_list(self): From f29904f1b56c2a486111374ee81d1898e1f04fd6 Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Thu, 12 Apr 2018 09:24:07 +0200 Subject: [PATCH 350/924] Rename from aurora light to nanoleaf_aurora (#13831) --- .coveragerc | 2 +- .../components/light/{aurora.py => nanoleaf_aurora.py} | 0 requirements_all.txt | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename homeassistant/components/light/{aurora.py => nanoleaf_aurora.py} (100%) diff --git a/.coveragerc b/.coveragerc index e9c69d137e2..48b45db347b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -407,7 +407,6 @@ omit = homeassistant/components/image_processing/seven_segments.py homeassistant/components/keyboard_remote.py homeassistant/components/keyboard.py - homeassistant/components/light/aurora.py homeassistant/components/light/avion.py homeassistant/components/light/blinksticklight.py homeassistant/components/light/blinkt.py @@ -422,6 +421,7 @@ omit = homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py homeassistant/components/light/mystrom.py + homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/osramlightify.py homeassistant/components/light/piglow.py homeassistant/components/light/rpi_gpio_pwm.py diff --git a/homeassistant/components/light/aurora.py b/homeassistant/components/light/nanoleaf_aurora.py similarity index 100% rename from homeassistant/components/light/aurora.py rename to homeassistant/components/light/nanoleaf_aurora.py diff --git a/requirements_all.txt b/requirements_all.txt index da2373443cb..8fe9c7e1c13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -536,7 +536,7 @@ myusps==1.3.2 # homeassistant.components.media_player.nadtcp nad_receiver==0.0.9 -# homeassistant.components.light.aurora +# homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 # homeassistant.components.discovery From 9bd29589d50a7832a63378cdcaa5674d1fe4ef2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Apr 2018 08:22:07 -0400 Subject: [PATCH 351/924] Version bump to 0.67.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 815562b68c5..53c72a46c3f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From f47572d3c0b81f73be41e8161e32e42be14c164f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Apr 2018 08:28:54 -0400 Subject: [PATCH 352/924] Allow platform unloading (#13784) * Allow platform unloading * Add tests * Add last test --- homeassistant/components/hue/__init__.py | 6 +++ homeassistant/components/hue/bridge.py | 35 +++++++++++-- homeassistant/components/light/__init__.py | 5 ++ homeassistant/config_entries.py | 20 +++++-- homeassistant/helpers/entity_component.py | 12 +++++ homeassistant/helpers/entity_platform.py | 11 +++- tests/components/hue/test_bridge.py | 61 +++++++++++++++++++++- tests/components/hue/test_init.py | 19 +++++++ tests/components/light/test_hue.py | 2 +- tests/helpers/test_entity_component.py | 31 +++++++++++ tests/helpers/test_entity_platform.py | 26 +++++++++ tests/test_config_entries.py | 4 +- 12 files changed, 218 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 557a47f3e05..0aed854d4e4 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -131,3 +131,9 @@ async def async_setup_entry(hass, entry): bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) hass.data[DOMAIN][host] = bridge return await bridge.async_setup() + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + bridge = hass.data[DOMAIN].pop(entry.data['host']) + return await bridge.async_reset() diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 4693a2f4dbe..5ff5e2dbf6f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -30,6 +30,7 @@ class HueBridge(object): self.allow_groups = allow_groups self.available = True self.api = None + self._cancel_retry_setup = None @property def host(self): @@ -67,8 +68,8 @@ class HueBridge(object): # This feels hacky, we should find a better way to do this self.config_entry.state = config_entries.ENTRY_STATE_LOADED - # Unhandled edge case: cancel this if we discover bridge on new IP - hass.helpers.event.async_call_later(retry_delay, retry_setup) + self._cancel_retry_setup = hass.helpers.event.async_call_later( + retry_delay, retry_setup) return False @@ -77,7 +78,7 @@ class HueBridge(object): host) return False - hass.async_add_job(hass.config_entries.async_forward_entry( + hass.async_add_job(hass.config_entries.async_forward_entry_setup( self.config_entry, 'light')) hass.services.async_register( @@ -86,6 +87,34 @@ class HueBridge(object): return True + async def async_reset(self): + """Reset this bridge to default state. + + Will cancel any scheduled setup retry and will unload + the config entry. + """ + # The bridge can be in 3 states: + # - Setup was successful, self.api is not None + # - Authentication was wrong, self.api is None, not retrying setup. + # - Host was down. self.api is None, we're retrying setup + + # If we have a retry scheduled, we were never setup. + if self._cancel_retry_setup is not None: + self._cancel_retry_setup() + self._cancel_retry_setup = None + return True + + # If the authentication was wrong. + if self.api is None: + return True + + self.hass.services.async_remove(DOMAIN, SERVICE_HUE_SCENE) + + # If setup was successful, we set api variable, forwarded entry and + # register service + return await self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'light') + async def hue_activate_scene(self, call, updated=False): """Service to call directly into bridge to set scenes.""" group_name = call.data[ATTR_GROUP_NAME] diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index d497c8f9880..30a1a800a44 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -393,6 +393,11 @@ async def async_setup_entry(hass, entry): return await hass.data[DOMAIN].async_setup_entry(entry) +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class Profiles: """Representation of available color profiles.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index fc781bd62c8..e2e45cb5819 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -203,12 +203,13 @@ class ConfigEntry: else: self.state = ENTRY_STATE_SETUP_ERROR - async def async_unload(self, hass): + async def async_unload(self, hass, *, component=None): """Unload an entry. Returns if unload is possible and was successful. """ - component = getattr(hass.components, self.domain) + if component is None: + component = getattr(hass.components, self.domain) supports_unload = hasattr(component, 'async_unload_entry') @@ -220,13 +221,13 @@ class ConfigEntry: if not isinstance(result, bool): _LOGGER.error('%s.async_unload_entry did not return boolean', - self.domain) + component.DOMAIN) result = False return result except Exception: # pylint: disable=broad-except _LOGGER.exception('Error unloading entry %s for %s', - self.title, self.domain) + self.title, component.DOMAIN) self.state = ENTRY_STATE_FAILED_UNLOAD return False @@ -326,7 +327,7 @@ class ConfigEntries: entries = await self.hass.async_add_job(load_json, path) self._entries = [ConfigEntry(**entry) for entry in entries] - async def async_forward_entry(self, entry, component): + async def async_forward_entry_setup(self, entry, component): """Forward the setup of an entry to a different component. By default an entry is setup with the component it belongs to. If that @@ -347,6 +348,15 @@ class ConfigEntries: await entry.async_setup( self.hass, component=getattr(self.hass.components, component)) + async def async_forward_entry_unload(self, entry, component): + """Forward the unloading of an entry to a different component.""" + # It was never loaded. + if component not in self.hass.config.components: + return True + + await entry.async_unload( + self.hass, component=getattr(self.hass.components, component)) + async def _async_add_entry(self, entry): """Add an entry.""" self._entries.append(entry) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 265464d548d..c82ae2a46f0 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -113,6 +113,18 @@ class EntityComponent(object): return await self._platforms[key].async_setup_entry(config_entry) + async def async_unload_entry(self, config_entry): + """Unload a config entry.""" + key = config_entry.entry_id + + platform = self._platforms.pop(key, None) + + if platform is None: + raise ValueError('Config entry was never loaded!') + + await platform.async_reset() + return True + @callback def async_extract_from_service(self, service, expand_group=True): """Extract all known and available entities from a service call. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ba8df7e01d8..00a7e49840e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -43,7 +43,10 @@ class EntityPlatform(object): self.config_entry = None self.entities = {} self._tasks = [] + # Method to cancel the state change listener self._async_unsub_polling = None + # Method to cancel the retry of setup + self._async_cancel_retry_setup = None self._process_updates = asyncio.Lock(loop=hass.loop) # Platform is None for the EntityComponent "catch-all" EntityPlatform @@ -145,10 +148,12 @@ class EntityPlatform(object): async def setup_again(now): """Run setup again.""" + self._async_cancel_retry_setup = None await self._async_setup_platform( async_create_setup_task, tries) - async_call_later(hass, wait_time, setup_again) + self._async_cancel_retry_setup = \ + async_call_later(hass, wait_time, setup_again) return False except asyncio.TimeoutError: logger.error( @@ -315,6 +320,10 @@ class EntityPlatform(object): This method must be run in the event loop. """ + if self._async_cancel_retry_setup is not None: + self._async_cancel_retry_setup() + self._async_cancel_retry_setup = None + if not self.entities: return diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 1f53d5aac14..c20cee0d0e8 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -18,8 +18,8 @@ async def test_bridge_setup(): assert await hue_bridge.async_setup() is True assert hue_bridge.api is api - assert len(hass.config_entries.async_forward_entry.mock_calls) == 1 - assert hass.config_entries.async_forward_entry.mock_calls[0][1] == \ + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1 + assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ (entry, 'light') @@ -54,3 +54,60 @@ async def test_bridge_setup_timeout(hass): assert len(hass.helpers.event.async_call_later.mock_calls) == 1 # Assert we are going to wait 2 seconds assert hass.helpers.event.async_call_later.mock_calls[0][1][0] == 2 + + +async def test_reset_cancels_retry_setup(): + """Test resetting a bridge while we're waiting to retry setup.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', side_effect=errors.CannotConnect): + assert await hue_bridge.async_setup() is False + + mock_call_later = hass.helpers.event.async_call_later + + assert len(mock_call_later.mock_calls) == 1 + + assert await hue_bridge.async_reset() + + assert len(mock_call_later.mock_calls) == 2 + assert len(mock_call_later.return_value.mock_calls) == 1 + + +async def test_reset_if_entry_had_wrong_auth(): + """Test calling reset when the entry contained wrong auth.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', + side_effect=errors.AuthenticationRequired): + assert await hue_bridge.async_setup() is False + + assert len(hass.async_add_job.mock_calls) == 1 + + assert await hue_bridge.async_reset() + + +async def test_reset_unloads_entry_if_setup(): + """Test calling reset while the entry has been setup.""" + hass = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'username': 'mock-username'} + hue_bridge = bridge.HueBridge(hass, entry, False, False) + + with patch.object(bridge, 'get_bridge', return_value=mock_coro(Mock())): + assert await hue_bridge.async_setup() is True + + assert len(hass.services.async_register.mock_calls) == 1 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1 + + hass.config_entries.async_forward_entry_unload.return_value = \ + mock_coro(True) + assert await hue_bridge.async_reset() + + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1 + assert len(hass.services.async_remove.mock_calls) == 1 diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 47e74b70e83..ea656ba8fc6 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -167,3 +167,22 @@ async def test_config_passed_to_config_entry(hass): assert p_entry is entry assert p_allow_unreachable is True assert p_allow_groups is False + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = MockConfigEntry(domain=hue.DOMAIN, data={ + 'host': '0.0.0.0', + }) + entry.add_to_hass(hass) + + with patch.object(hue, 'HueBridge') as mock_bridge: + mock_bridge.return_value.async_setup.return_value = mock_coro(True) + assert await async_setup_component(hass, hue.DOMAIN, {}) is True + + assert len(mock_bridge.return_value.mock_calls) == 1 + + mock_bridge.return_value.async_reset.return_value = mock_coro(True) + assert await hue.async_unload_entry(hass, entry) + assert len(mock_bridge.return_value.async_reset.mock_calls) == 1 + assert hass.data[hue.DOMAIN] == {} diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index dee27adfe34..712cd17a7c7 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -200,7 +200,7 @@ async def setup_bridge(hass, mock_bridge): config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', { 'host': 'mock-host' }, 'test') - await hass.config_entries.async_forward_entry(config_entry, 'light') + await hass.config_entries.async_forward_entry_setup(config_entry, 'light') # To flush out the service call to update the group await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index f53b69274ef..0bc6a7601dc 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -376,3 +376,34 @@ async def test_setup_entry_fails_duplicate(hass): with pytest.raises(ValueError): await component.async_setup_entry(entry) + + +async def test_unload_entry_resets_platform(hass): + """Test unloading an entry removes all entities.""" + mock_setup_entry = Mock(return_value=mock_coro(True)) + loader.set_component( + 'test_domain.entry_domain', + MockPlatform(async_setup_entry=mock_setup_entry)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='entry_domain') + + assert await component.async_setup_entry(entry) + assert len(mock_setup_entry.mock_calls) == 1 + add_entities = mock_setup_entry.mock_calls[0][1][2] + add_entities([MockEntity()]) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + + assert await component.async_unload_entry(entry) + assert len(hass.states.async_entity_ids()) == 0 + + +async def test_unload_entry_fails_if_never_loaded(hass): + """.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entry = MockConfigEntry(domain='entry_domain') + + with pytest.raises(ValueError): + await component.async_unload_entry(entry) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index a8394ff6a49..2018cb27541 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -555,3 +555,29 @@ async def test_setup_entry_platform_not_ready(hass, caplog): assert len(async_setup_entry.mock_calls) == 1 assert 'Platform test not ready yet' in caplog.text assert len(mock_call_later.mock_calls) == 1 + + +async def test_reset_cancels_retry_setup(hass): + """Test that resetting a platform will cancel scheduled a setup retry.""" + async_setup_entry = Mock(side_effect=PlatformNotReady) + platform = MockPlatform( + async_setup_entry=async_setup_entry + ) + config_entry = MockConfigEntry() + ent_platform = MockEntityPlatform( + hass, + platform_name=config_entry.domain, + platform=platform + ) + + with patch.object(entity_platform, 'async_call_later') as mock_call_later: + assert not await ent_platform.async_setup_entry(config_entry) + + assert len(mock_call_later.mock_calls) == 1 + assert len(mock_call_later.return_value.mock_calls) == 0 + assert ent_platform._async_cancel_retry_setup is not None + + await ent_platform.async_reset() + + assert len(mock_call_later.return_value.mock_calls) == 1 + assert ent_platform._async_cancel_retry_setup is None diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 8bbd79a7ac7..b9b39b11c13 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -405,7 +405,7 @@ async def test_forward_entry_sets_up_component(hass): 'forwarded', MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry)) - await hass.config_entries.async_forward_entry(entry, 'forwarded') + await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') assert len(mock_original_setup_entry.mock_calls) == 0 assert len(mock_forwarded_setup_entry.mock_calls) == 1 @@ -422,6 +422,6 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): async_setup_entry=mock_setup_entry, )) - await hass.config_entries.async_forward_entry(entry, 'forwarded') + await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 From c863b9614cf5d1cc132aff664530d673ac6afb3d Mon Sep 17 00:00:00 2001 From: Yonsm Date: Thu, 12 Apr 2018 21:01:41 +0800 Subject: [PATCH 353/924] Support CO2/PM2.5/Light sensors in HomeKit (#13804) * Support co2/light/air sensor in HomeKit * Add tests * Added tests * changed device_class lux to light --- homeassistant/components/homekit/__init__.py | 22 ++++- homeassistant/components/homekit/const.py | 18 +++- .../components/homekit/type_sensors.py | 78 +++++++++++++++- homeassistant/components/homekit/util.py | 13 +++ .../homekit/test_get_accessories.py | 61 +++++++++++++ tests/components/homekit/test_type_sensors.py | 91 ++++++++++++++++++- tests/components/homekit/test_util.py | 82 ++++++++++------- 7 files changed, 318 insertions(+), 47 deletions(-) mode change 100755 => 100644 homeassistant/components/homekit/type_sensors.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 02d21889f6b..1092cea0c6e 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.cover import SUPPORT_SET_POSITION from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA @@ -19,7 +19,9 @@ from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, - DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START) + DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, + DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE) from .util import ( validate_entity_config, show_setup_message) @@ -103,10 +105,22 @@ def get_accessory(hass, state, aid, config): elif state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit == TEMP_CELSIUS or unit == TEMP_FAHRENHEIT: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \ + or unit == TEMP_FAHRENHEIT: a_type = 'TemperatureSensor' - elif unit == '%': + elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%': a_type = 'HumiditySensor' + elif device_class == DEVICE_CLASS_PM25 \ + or DEVICE_CLASS_PM25 in state.entity_id: + a_type = 'AirQualitySensor' + elif device_class == DEVICE_CLASS_CO2 \ + or DEVICE_CLASS_CO2 in state.entity_id: + a_type = 'CarbonDioxideSensor' + elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \ + unit == 'lux': + a_type = 'LightSensor' elif state.domain == 'switch' or state.domain == 'remote' \ or state.domain == 'input_boolean' or state.domain == 'script': diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 37ee9722bc4..7cde51b5416 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -34,13 +34,13 @@ CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' +SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' -# CurrentRelativeHumidity | StatusActive, StatusFault, StatusTampered, -# StatusLowBattery, Name +SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity SERV_LEAK_SENSOR = 'LeakSensor' +SERV_LIGHT_SENSOR = 'LightSensor' SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' @@ -50,17 +50,21 @@ SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' -SERV_WINDOW_COVERING = 'WindowCovering' -# CurrentPosition, TargetPosition, PositionState +SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition # #### Characteristics #### +CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' +CHAR_AIR_QUALITY = 'AirQuality' CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' +CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' +CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' CHAR_CARBON_MONOXIDE_DETECTED = 'CarbonMonoxideDetected' CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' +CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent @@ -93,8 +97,12 @@ PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Class #### DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_GAS = 'gas' +DEVICE_CLASS_HUMIDITY = 'humidity' +DEVICE_CLASS_LIGHT = 'light' DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OPENING = 'opening' +DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' +DEVICE_CLASS_TEMPERATURE = 'temperature' diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py old mode 100755 new mode 100644 index 790f0de6103..6aa8d92c0af --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -10,6 +10,9 @@ from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, + SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, + CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, + SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED, @@ -18,7 +21,8 @@ from .const import ( DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) -from .util import convert_to_float, temperature_to_homekit +from .util import ( + convert_to_float, temperature_to_homekit, density_to_air_quality) _LOGGER = logging.getLogger(__name__) @@ -81,6 +85,78 @@ class HumiditySensor(HomeAccessory): self.entity_id, humidity) +@TYPES.register('AirQualitySensor') +class AirQualitySensor(HomeAccessory): + """Generate a AirQualitySensor accessory as air quality sensor.""" + + def __init__(self, *args, config): + """Initialize a AirQualitySensor accessory object.""" + super().__init__(*args, category=CATEGORY_SENSOR) + + serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR, + [CHAR_AIR_PARTICULATE_DENSITY]) + self.char_quality = setup_char( + CHAR_AIR_QUALITY, serv_air_quality, value=0) + self.char_density = setup_char( + CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0) + + def update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if density is not None: + self.char_density.set_value(density) + self.char_quality.set_value(density_to_air_quality(density)) + _LOGGER.debug('%s: Set to %d', self.entity_id, density) + + +@TYPES.register('CarbonDioxideSensor') +class CarbonDioxideSensor(HomeAccessory): + """Generate a CarbonDioxideSensor accessory as CO2 sensor.""" + + def __init__(self, *args, config): + """Initialize a CarbonDioxideSensor accessory object.""" + super().__init__(*args, category=CATEGORY_SENSOR) + + serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [ + CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL]) + self.char_co2 = setup_char( + CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0) + self.char_peak = setup_char( + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0) + self.char_detected = setup_char( + CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0) + + def update_state(self, new_state): + """Update accessory after state change.""" + co2 = convert_to_float(new_state.state) + if co2 is not None: + self.char_co2.set_value(co2) + if co2 > self.char_peak.value: + self.char_peak.set_value(co2) + self.char_detected.set_value(co2 > 1000) + _LOGGER.debug('%s: Set to %d', self.entity_id, co2) + + +@TYPES.register('LightSensor') +class LightSensor(HomeAccessory): + """Generate a LightSensor accessory as light sensor.""" + + def __init__(self, *args, config): + """Initialize a LightSensor accessory object.""" + super().__init__(*args, category=CATEGORY_SENSOR) + + serv_light = add_preload_service(self, SERV_LIGHT_SENSOR) + self.char_light = setup_char( + CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0) + + def update_state(self, new_state): + """Update accessory after state change.""" + luminance = convert_to_float(new_state.state) + if luminance is not None: + self.char_light.set_value(luminance) + _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) + + @TYPES.register('BinarySensor') class BinarySensor(HomeAccessory): """Generate a BinarySensor accessory as binary sensor.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index e14b6c47bc8..29fe3c8f265 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -64,3 +64,16 @@ def temperature_to_homekit(temperature, unit): def temperature_to_states(temperature, unit): """Convert temperature back from Celsius to Home Assistant unit.""" return round(temp_util.convert(temperature, TEMP_CELSIUS, unit), 1) + + +def density_to_air_quality(density): + """Map PM2.5 density to HomeKit AirQuality level.""" + if density <= 35: + return 1 + elif density <= 75: + return 2 + elif density <= 115: + return 3 + elif density <= 150: + return 4 + return 5 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6f2521fc4e5..052b7557c11 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -41,6 +41,13 @@ class TestGetAccessories(unittest.TestCase): """Test if mock type was called.""" self.assertTrue(self.mock_type.called) + def test_sensor_temperature(self): + """Test temperature sensor with device class temperature.""" + with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): + state = State('sensor.temperature', '23', + {ATTR_DEVICE_CLASS: 'temperature'}) + get_accessory(None, state, 2, {}) + def test_sensor_temperature_celsius(self): """Test temperature sensor with Celsius as unit.""" with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): @@ -56,12 +63,66 @@ class TestGetAccessories(unittest.TestCase): get_accessory(None, state, 2, {}) def test_sensor_humidity(self): + """Test humidity sensor with device class humidity.""" + with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): + state = State('sensor.humidity', '20', + {ATTR_DEVICE_CLASS: 'humidity'}) + get_accessory(None, state, 2, {}) + + def test_sensor_humidity_unit(self): """Test humidity sensor with % as unit.""" with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): state = State('sensor.humidity', '20', {ATTR_UNIT_OF_MEASUREMENT: '%'}) get_accessory(None, state, 2, {}) + def test_air_quality_sensor(self): + """Test air quality sensor with pm25 class.""" + with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): + state = State('sensor.air_quality', '40', + {ATTR_DEVICE_CLASS: 'pm25'}) + get_accessory(None, state, 2, {}) + + def test_air_quality_sensor_entity_id(self): + """Test air quality sensor with entity_id contains pm25.""" + with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): + state = State('sensor.air_quality_pm25', '40', {}) + get_accessory(None, state, 2, {}) + + def test_co2_sensor(self): + """Test co2 sensor with device class co2.""" + with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): + state = State('sensor.airmeter', '500', + {ATTR_DEVICE_CLASS: 'co2'}) + get_accessory(None, state, 2, {}) + + def test_co2_sensor_entity_id(self): + """Test co2 sensor with entity_id contains co2.""" + with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): + state = State('sensor.airmeter_co2', '500', {}) + get_accessory(None, state, 2, {}) + + def test_light_sensor(self): + """Test light sensor with device class lux.""" + with patch.dict(TYPES, {'LightSensor': self.mock_type}): + state = State('sensor.light', '900', + {ATTR_DEVICE_CLASS: 'light'}) + get_accessory(None, state, 2, {}) + + def test_light_sensor_unit_lm(self): + """Test light sensor with lm as unit.""" + with patch.dict(TYPES, {'LightSensor': self.mock_type}): + state = State('sensor.light', '900', + {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) + get_accessory(None, state, 2, {}) + + def test_light_sensor_unit_lux(self): + """Test light sensor with lux as unit.""" + with patch.dict(TYPES, {'LightSensor': self.mock_type}): + state = State('sensor.light', '900', + {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) + get_accessory(None, state, 2, {}) + def test_binary_sensor(self): """Test binary sensor with opening class.""" with patch.dict(TYPES, {'BinarySensor': self.mock_type}): diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index f9dfb04b37c..77bfc0c8901 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -3,7 +3,8 @@ import unittest from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) + TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, + LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -40,6 +41,7 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) self.hass.block_till_done() + self.assertEqual(acc.char_temp.value, 0.0) self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) @@ -63,14 +65,95 @@ class TestHomekitSensors(unittest.TestCase): self.assertEqual(acc.char_humidity.value, 0) - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.states.set(entity_id, STATE_UNKNOWN) self.hass.block_till_done() + self.assertEqual(acc.char_humidity.value, 0) - self.hass.states.set(entity_id, '20', {ATTR_UNIT_OF_MEASUREMENT: "%"}) + self.hass.states.set(entity_id, '20') self.hass.block_till_done() self.assertEqual(acc.char_humidity.value, 20) + def test_air_quality(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.air_quality' + + acc = AirQualitySensor(self.hass, 'Air Quality', entity_id, + 2, config=None) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_density.value, 0) + self.assertEqual(acc.char_quality.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_density.value, 0) + self.assertEqual(acc.char_quality.value, 0) + + self.hass.states.set(entity_id, '34') + self.hass.block_till_done() + self.assertEqual(acc.char_density.value, 34) + self.assertEqual(acc.char_quality.value, 1) + + self.hass.states.set(entity_id, '200') + self.hass.block_till_done() + self.assertEqual(acc.char_density.value, 200) + self.assertEqual(acc.char_quality.value, 5) + + def test_co2(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.co2' + + acc = CarbonDioxideSensor(self.hass, 'CO2', entity_id, 2, config=None) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_co2.value, 0) + self.assertEqual(acc.char_peak.value, 0) + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_co2.value, 0) + self.assertEqual(acc.char_peak.value, 0) + self.assertEqual(acc.char_detected.value, 0) + + self.hass.states.set(entity_id, '1100') + self.hass.block_till_done() + self.assertEqual(acc.char_co2.value, 1100) + self.assertEqual(acc.char_peak.value, 1100) + self.assertEqual(acc.char_detected.value, 1) + + self.hass.states.set(entity_id, '800') + self.hass.block_till_done() + self.assertEqual(acc.char_co2.value, 800) + self.assertEqual(acc.char_peak.value, 1100) + self.assertEqual(acc.char_detected.value, 0) + + def test_light(self): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.light' + + acc = LightSensor(self.hass, 'Light', entity_id, 2, config=None) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 10) # Sensor + + self.assertEqual(acc.char_light.value, 0.0001) + + self.hass.states.set(entity_id, STATE_UNKNOWN) + self.hass.block_till_done() + self.assertEqual(acc.char_light.value, 0.0001) + + self.hass.states.set(entity_id, '300') + self.hass.block_till_done() + self.assertEqual(acc.char_light.value, 300) + def test_binary(self): """Test if accessory is updated after state change.""" entity_id = 'binary_sensor.opening' diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 7465e9affab..4a9521384bd 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,13 +2,15 @@ import unittest import voluptuous as vol +import pytest from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, ATTR_CODE) + temperature_to_homekit, temperature_to_states, ATTR_CODE, + density_to_air_quality) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( @@ -20,6 +22,52 @@ from homeassistant.const import ( from tests.common import get_test_home_assistant +def test_validate_entity_config(): + """Test validate entities.""" + configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, + {'demo.test': 'test'}, {'demo.test': [1, 2]}, + {'demo.test': None}] + + for conf in configs: + with pytest.raises(vol.Invalid): + vec(conf) + + assert vec({}) == {} + assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ + {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} + + +def test_convert_to_float(): + """Test convert_to_float method.""" + assert convert_to_float(12) == 12 + assert convert_to_float(12.4) == 12.4 + assert convert_to_float(STATE_UNKNOWN) is None + assert convert_to_float(None) is None + + +def test_temperature_to_homekit(): + """Test temperature conversion from HA to HomeKit.""" + assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5 + assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4 + + +def test_temperature_to_states(): + """Test temperature conversion from HomeKit to HA.""" + assert temperature_to_states(20, TEMP_CELSIUS) == 20.0 + assert temperature_to_states(20.2, TEMP_FAHRENHEIT) == 68.4 + + +def test_density_to_air_quality(): + """Test map PM2.5 density to HomeKit AirQuality level.""" + assert density_to_air_quality(0) == 1 + assert density_to_air_quality(35) == 1 + assert density_to_air_quality(35.1) == 2 + assert density_to_air_quality(75) == 2 + assert density_to_air_quality(115) == 3 + assert density_to_air_quality(150) == 4 + assert density_to_air_quality(300) == 5 + + class TestUtil(unittest.TestCase): """Test all HomeKit util methods.""" @@ -39,21 +87,6 @@ class TestUtil(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() - def test_validate_entity_config(self): - """Test validate entities.""" - configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, - {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}] - - for conf in configs: - with self.assertRaises(vol.Invalid): - vec(conf) - - self.assertEqual(vec({}), {}) - self.assertEqual( - vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}), - {'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) - def test_show_setup_msg(self): """Test show setup message as persistence notification.""" bridge = HomeBridge(self.hass) @@ -83,20 +116,3 @@ class TestUtil(unittest.TestCase): self.assertEqual( data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), HOMEKIT_NOTIFY_ID) - - def test_convert_to_float(self): - """Test convert_to_float method.""" - self.assertEqual(convert_to_float(12), 12) - self.assertEqual(convert_to_float(12.4), 12.4) - self.assertIsNone(convert_to_float(STATE_UNKNOWN)) - self.assertIsNone(convert_to_float(None)) - - def test_temperature_to_homekit(self): - """Test temperature conversion from HA to HomeKit.""" - self.assertEqual(temperature_to_homekit(20.46, TEMP_CELSIUS), 20.5) - self.assertEqual(temperature_to_homekit(92.1, TEMP_FAHRENHEIT), 33.4) - - def test_temperature_to_states(self): - """Test temperature conversion from HomeKit to HA.""" - self.assertEqual(temperature_to_states(20, TEMP_CELSIUS), 20.0) - self.assertEqual(temperature_to_states(20.2, TEMP_FAHRENHEIT), 68.4) From d2804b0a27a0f73ab912217e4e2be4412c7ad68a Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Thu, 12 Apr 2018 15:44:56 +0200 Subject: [PATCH 354/924] Channel up/down for LiveTV and next/previous for other apps (#13829) --- homeassistant/components/media_player/webostv.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 860d69e22c3..ae9d259a47c 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -35,6 +35,7 @@ CONF_SOURCES = 'sources' CONF_ON_ACTION = 'turn_on_action' DEFAULT_NAME = 'LG webOS Smart TV' +LIVETV_APP_ID = 'com.webos.app.livetv' WEBOSTV_CONFIG_FILE = 'webostv.conf' @@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._client.channel_up() + current_input = self._client.get_input() + if current_input == LIVETV_APP_ID: + self._client.channel_up() + else: + self._client.fast_forward() def media_previous_track(self): """Send the previous track command.""" - self._client.channel_down() + current_input = self._client.get_input() + if current_input == LIVETV_APP_ID: + self._client.channel_down() + else: + self._client.rewind() From 51bdd06d1f075169282dc44eef7689a16ee52d31 Mon Sep 17 00:00:00 2001 From: xTCx Date: Thu, 12 Apr 2018 17:13:31 +0300 Subject: [PATCH 355/924] Clicksend: Added support for multiple recipients (#13812) * Clicksend: Added support for multiple recipients * Removed whitespace --- homeassistant/components/notify/clicksend.py | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/notify/clicksend.py b/homeassistant/components/notify/clicksend.py index 2b2cb4e7f22..c028da2c579 100644 --- a/homeassistant/components/notify/clicksend.py +++ b/homeassistant/components/notify/clicksend.py @@ -37,7 +37,8 @@ PLATFORM_SCHEMA = vol.Schema( vol.All(PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_RECIPIENT): cv.string, + vol.Required(CONF_RECIPIENT, default=[]): + vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_SENDER): cv.string, }), validate_sender)) @@ -59,21 +60,19 @@ class ClicksendNotificationService(BaseNotificationService): """Initialize the service.""" self.username = config.get(CONF_USERNAME) self.api_key = config.get(CONF_API_KEY) - self.recipient = config.get(CONF_RECIPIENT) + self.recipients = config.get(CONF_RECIPIENT) self.sender = config.get(CONF_SENDER, CONF_RECIPIENT) def send_message(self, message="", **kwargs): """Send a message to a user.""" - data = ({ - 'messages': [ - { - 'source': 'hass.notify', - 'from': self.sender, - 'to': self.recipient, - 'body': message, - } - ] - }) + data = {"messages": []} + for recipient in self.recipients: + data["messages"].append({ + 'source': 'hass.notify', + 'from': self.sender, + 'to': recipient, + 'body': message, + }) api_url = "{}/sms/send".format(BASE_API_URL) From 993866a31435d286d56aad8d4a5316d4d3007de1 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 12 Apr 2018 12:08:48 -0400 Subject: [PATCH 356/924] Support Garage Doors in HomeKit (#13796) --- homeassistant/components/homekit/__init__.py | 11 +++- homeassistant/components/homekit/const.py | 4 ++ .../components/homekit/type_covers.py | 50 ++++++++++++++- .../homekit/test_get_accessories.py | 11 ++++ tests/components/homekit/test_type_covers.py | 63 ++++++++++++++++++- 5 files changed, 132 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1092cea0c6e..306f399092a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,7 +8,8 @@ from zlib import adler32 import voluptuous as vol -from homeassistant.components.cover import SUPPORT_SET_POSITION +from homeassistant.components.cover import ( + SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -92,9 +93,13 @@ def get_accessory(hass, state, aid, config): a_type = 'Thermostat' elif state.domain == 'cover': - # Only add covers that support set_cover_position features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & SUPPORT_SET_POSITION: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + if device_class == 'garage' and \ + features & (SUPPORT_OPEN | SUPPORT_CLOSE): + a_type = 'GarageDoorOpener' + elif features & SUPPORT_SET_POSITION: a_type = 'WindowCovering' elif state.domain == 'light': diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 7cde51b5416..79466cd9ff0 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -24,6 +24,7 @@ MANUFACTURER = 'HomeAssistant' # #### Categories #### CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' +CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER' CATEGORY_LIGHT = 'LIGHTBULB' CATEGORY_LOCK = 'DOOR_LOCK' CATEGORY_SENSOR = 'SENSOR' @@ -38,6 +39,7 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' +SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' @@ -65,6 +67,7 @@ CHAR_COLOR_TEMPERATURE = 'ColorTemperature' CHAR_CONTACT_SENSOR_STATE = 'ContactSensorState' CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' +CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent @@ -85,6 +88,7 @@ CHAR_ON = 'On' # boolean CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' +CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index d8a6a8c2fdc..9c852bb4d86 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -3,17 +3,63 @@ import logging from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION +from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED) from . import TYPES from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, - CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION) + CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, + CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER, + CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) _LOGGER = logging.getLogger(__name__) +@TYPES.register('GarageDoorOpener') +class GarageDoorOpener(HomeAccessory): + """Generate a Garage Door Opener accessory for a cover entity. + + The cover entity must be in the 'garage' device class + and support no more than open, close, and stop. + """ + + def __init__(self, *args, config): + """Initialize a GarageDoorOpener accessory object.""" + super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) + self.flag_target_state = False + + serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER) + self.char_current_state = setup_char( + CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0) + self.char_target_state = setup_char( + CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0, + callback=self.set_state) + + def set_state(self, value): + """Change garage state if call came from HomeKit.""" + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + self.flag_target_state = True + + if value == 0: + self.char_current_state.set_value(3) + self.hass.components.cover.open_cover(self.entity_id) + elif value == 1: + self.char_current_state.set_value(2) + self.hass.components.cover.close_cover(self.entity_id) + + def update_state(self, new_state): + """Update cover state after state changed.""" + hass_state = new_state.state + if hass_state in (STATE_OPEN, STATE_CLOSED): + current_state = 0 if hass_state == STATE_OPEN else 1 + self.char_current_state.set_value(current_state) + if not self.flag_target_state: + self.char_target_state.set_value(current_state) + self.flag_target_state = False + + @TYPES.register('WindowCovering') class WindowCovering(HomeAccessory): """Generate a Window accessory for a cover entity. diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 052b7557c11..8333f1fb893 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -4,6 +4,8 @@ import unittest from unittest.mock import patch, Mock from homeassistant.core import State +from homeassistant.components.cover import ( + SUPPORT_OPEN, SUPPORT_CLOSE) from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES @@ -136,6 +138,15 @@ class TestGetAccessories(unittest.TestCase): state = State('device_tracker.someone', 'not_home', {}) get_accessory(None, state, 2, {}) + def test_garage_door(self): + """Test cover with device_class: 'garage' and required features.""" + with patch.dict(TYPES, {'GarageDoorOpener': self.mock_type}): + state = State('cover.garage_door', 'open', { + ATTR_DEVICE_CLASS: 'garage', + ATTR_SUPPORTED_FEATURES: + SUPPORT_OPEN | SUPPORT_CLOSE}) + get_accessory(None, state, 2, {}) + def test_cover_set_position(self): """Test cover with support for set_cover_position.""" with patch.dict(TYPES, {'WindowCovering': self.mock_type}): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 43e82e74b1a..f9889b1bdd8 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,9 +4,10 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION) -from homeassistant.components.homekit.type_covers import WindowCovering +from homeassistant.components.homekit.type_covers import ( + GarageDoorOpener, WindowCovering) from homeassistant.const import ( - STATE_UNKNOWN, STATE_OPEN, + STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE) from tests.common import get_test_home_assistant @@ -31,6 +32,64 @@ class TestHomekitSensors(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() + def test_garage_door_open_close(self): + """Test if accessory and HA are updated accordingly.""" + garage_door = 'cover.garage_door' + + acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 4) # GarageDoorOpener + + self.assertEqual(acc.char_current_state.value, 0) + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.set(garage_door, STATE_CLOSED) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 1) + self.assertEqual(acc.char_target_state.value, 1) + + self.hass.states.set(garage_door, STATE_OPEN) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 0) + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.set(garage_door, STATE_UNAVAILABLE) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 0) + self.assertEqual(acc.char_target_state.value, 0) + + self.hass.states.set(garage_door, STATE_UNKNOWN) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 0) + self.assertEqual(acc.char_target_state.value, 0) + + # Set closed from HomeKit + acc.char_target_state.client_update_value(1) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 2) + self.assertEqual(acc.char_target_state.value, 1) + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'close_cover') + + self.hass.states.set(garage_door, STATE_CLOSED) + self.hass.block_till_done() + + # Set open from HomeKit + acc.char_target_state.client_update_value(0) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_target_state.value, 0) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'open_cover') + def test_window_set_cover_position(self): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' From 62dc737ea313f5298e032d81f89dd6f2434726e0 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Thu, 12 Apr 2018 13:27:23 -0700 Subject: [PATCH 357/924] Abode better events (#13809) * Push abodepy version to 0.13.0 * Bump to 0.13.1. Now uses a cache to store the generated UUID. * Reorganize to not be a dumb dumb. --- homeassistant/components/abode.py | 11 +++++++---- requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 08918c77f01..2f56bb7c2b5 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -19,7 +19,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['abodepy==0.12.3'] +REQUIREMENTS = ['abodepy==0.13.1'] _LOGGER = logging.getLogger(__name__) @@ -27,6 +27,7 @@ CONF_ATTRIBUTION = "Data provided by goabode.com" CONF_POLLING = 'polling' DOMAIN = 'abode' +DEFAULT_CACHEDB = './abodepy_cache.pickle' NOTIFICATION_ID = 'abode_notification' NOTIFICATION_TITLE = 'Abode Security Setup' @@ -87,12 +88,13 @@ ABODE_PLATFORMS = [ class AbodeSystem(object): """Abode System class.""" - def __init__(self, username, password, name, polling, exclude, lights): + def __init__(self, username, password, cache, + name, polling, exclude, lights): """Initialize the system.""" import abodepy self.abode = abodepy.Abode( username, password, auto_login=True, get_devices=True, - get_automations=True) + get_automations=True, cache_path=cache) self.name = name self.polling = polling self.exclude = exclude @@ -129,8 +131,9 @@ def setup(hass, config): lights = conf.get(CONF_LIGHTS) try: + cache = hass.config.path(DEFAULT_CACHEDB) hass.data[DOMAIN] = AbodeSystem( - username, password, name, polling, exclude, lights) + username, password, cache, name, polling, exclude, lights) except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) diff --git a/requirements_all.txt b/requirements_all.txt index 86cff3f9420..8fe360df8e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -64,7 +64,7 @@ WazeRouteCalculator==0.5 YesssSMS==0.1.1b3 # homeassistant.components.abode -abodepy==0.12.3 +abodepy==0.13.1 # homeassistant.components.media_player.frontier_silicon afsapi==0.0.3 From 22a1b99e57ba31197fcc4a9233803ba65a66ed49 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 12 Apr 2018 23:22:52 +0100 Subject: [PATCH 358/924] UPnP async (#13666) * moved from miniupnpc to pyupnp-async * update requirements * Tests added * hound * update requirements_test_all.txt * update gen_requirements_all.py * addresses @pvizeli requested changes * address review comments --- homeassistant/components/sensor/upnp.py | 52 ++++---- homeassistant/components/upnp.py | 71 +++++++---- requirements_all.txt | 6 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/test_upnp.py | 160 +++++++++++++++--------- 6 files changed, 180 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index e5acae67916..e0c57ca9ac6 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -6,38 +6,44 @@ https://home-assistant.io/components/sensor.upnp/ """ import logging -from homeassistant.components.upnp import DATA_UPNP, UNITS +from homeassistant.components.upnp import DATA_UPNP, UNITS, CIC_SERVICE from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) +BYTES_RECEIVED = 1 +BYTES_SENT = 2 +PACKETS_RECEIVED = 3 +PACKETS_SENT = 4 + # sensor_type: [friendly_name, convert_unit, icon] SENSOR_TYPES = { - 'byte_received': ['received bytes', True, 'mdi:server-network'], - 'byte_sent': ['sent bytes', True, 'mdi:server-network'], - 'packets_in': ['packets received', False, 'mdi:server-network'], - 'packets_out': ['packets sent', False, 'mdi:server-network'], + BYTES_RECEIVED: ['received bytes', True, 'mdi:server-network'], + BYTES_SENT: ['sent bytes', True, 'mdi:server-network'], + PACKETS_RECEIVED: ['packets received', False, 'mdi:server-network'], + PACKETS_SENT: ['packets sent', False, 'mdi:server-network'], } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, add_devices, discovery_info=None): """Set up the IGD sensors.""" - upnp = hass.data[DATA_UPNP] + device = hass.data[DATA_UPNP] + service = device.find_first_service(CIC_SERVICE) unit = discovery_info['unit'] add_devices([ - IGDSensor(upnp, t, unit if SENSOR_TYPES[t][1] else None) + IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#') for t in SENSOR_TYPES], True) class IGDSensor(Entity): """Representation of a UPnP IGD sensor.""" - def __init__(self, upnp, sensor_type, unit=""): + def __init__(self, service, sensor_type, unit=None): """Initialize the IGD sensor.""" - self._upnp = upnp + self._service = service self.type = sensor_type self.unit = unit - self.unit_factor = UNITS[unit] if unit is not None else 1 + self.unit_factor = UNITS[unit] if unit in UNITS else 1 self._name = 'IGD {}'.format(SENSOR_TYPES[sensor_type][0]) self._state = None @@ -49,9 +55,9 @@ class IGDSensor(Entity): @property def state(self): """Return the state of the device.""" - if self._state is None: - return None - return format(self._state / self.unit_factor, '.1f') + if self._state: + return format(float(self._state) / self.unit_factor, '.1f') + return self._state @property def icon(self): @@ -63,13 +69,13 @@ class IGDSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self.unit - def update(self): + async def async_update(self): """Get the latest information from the IGD.""" - if self.type == "byte_received": - self._state = self._upnp.totalbytereceived() - elif self.type == "byte_sent": - self._state = self._upnp.totalbytesent() - elif self.type == "packets_in": - self._state = self._upnp.totalpacketreceived() - elif self.type == "packets_out": - self._state = self._upnp.totalpacketsent() + if self.type == BYTES_RECEIVED: + self._state = await self._service.get_total_bytes_received() + elif self.type == BYTES_SENT: + self._state = await self._service.get_total_bytes_sent() + elif self.type == PACKETS_RECEIVED: + self._state = await self._service.get_total_packets_received() + elif self.type == PACKETS_SENT: + self._state = await self._service.get_total_packets_sent() diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index 960d8f3780e..dd611090c22 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/upnp/ """ from ipaddress import ip_address import logging +import asyncio import voluptuous as vol @@ -14,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import get_local_ip -REQUIREMENTS = ['miniupnpc==2.0.2'] +REQUIREMENTS = ['pyupnp-async==0.1.0.1'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) @@ -22,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['api'] DOMAIN = 'upnp' -DATA_UPNP = 'UPNP' +DATA_UPNP = 'upnp_device' CONF_LOCAL_IP = 'local_ip' CONF_ENABLE_PORT_MAPPING = 'port_mapping' @@ -33,6 +34,11 @@ CONF_HASS = 'hass' NOTIFICATION_ID = 'upnp_notification' NOTIFICATION_TITLE = 'UPnP Setup' +IGD_DEVICE = 'urn:schemas-upnp-org:device:InternetGatewayDevice:1' +PPP_SERVICE = 'urn:schemas-upnp-org:service:WANPPPConnection:1' +IP_SERVICE = 'urn:schemas-upnp-org:service:WANIPConnection:1' +CIC_SERVICE = 'urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1' + UNITS = { "Bytes": 1, "KBytes": 1024, @@ -51,8 +57,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -# pylint: disable=import-error, no-member, broad-except, c-extension-no-member -def setup(hass, config): +async def async_setup(hass, config): """Register a port mapping for Home Assistant via UPnP.""" config = config[DOMAIN] host = config.get(CONF_LOCAL_IP) @@ -67,21 +72,35 @@ def setup(hass, config): 'Unable to determine local IP. Add it to your configuration.') return False - import miniupnpc + import pyupnp_async + from pyupnp_async.error import UpnpSoapError - upnp = miniupnpc.UPnP() - hass.data[DATA_UPNP] = upnp - - upnp.discoverdelay = 200 - upnp.discover() - try: - upnp.selectigd() - except Exception: - _LOGGER.exception("Error when attempting to discover an UPnP IGD") + service = None + resp = await pyupnp_async.msearch_first(search_target=IGD_DEVICE) + if not resp: return False - unit = config.get(CONF_UNITS) - discovery.load_platform(hass, 'sensor', DOMAIN, {'unit': unit}, config) + try: + device = await resp.get_device() + hass.data[DATA_UPNP] = device + for _service in device.services: + if _service['serviceType'] == PPP_SERVICE: + service = device.find_first_service(PPP_SERVICE) + if _service['serviceType'] == IP_SERVICE: + service = device.find_first_service(IP_SERVICE) + if _service['serviceType'] == CIC_SERVICE: + unit = config.get(CONF_UNITS) + discovery.load_platform(hass, 'sensor', + DOMAIN, + {'unit': unit}, + config) + except UpnpSoapError as error: + _LOGGER.error(error) + return False + + if not service: + _LOGGER.warning("Could not find any UPnP IGD") + return False port_mapping = config.get(CONF_ENABLE_PORT_MAPPING) if not port_mapping: @@ -98,12 +117,12 @@ def setup(hass, config): if internal == CONF_HASS: internal = internal_port try: - upnp.addportmapping( - external, 'TCP', host, internal, 'Home Assistant', '') + await service.add_port_mapping(internal, external, host, 'TCP', + desc='Home Assistant') registered.append(external) - except Exception: - _LOGGER.exception("UPnP failed to configure port mapping for %s", - external) + _LOGGER.debug("external %s -> %s @ %s", external, internal, host) + except UpnpSoapError as error: + _LOGGER.error(error) hass.components.persistent_notification.create( 'ERROR: tcp port {} is already mapped in your router.' '
Please disable port_mapping in the upnp ' @@ -113,11 +132,13 @@ def setup(hass, config): title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - def deregister_port(event): + async def deregister_port(event): """De-register the UPnP port mapping.""" - for external in registered: - upnp.deleteportmapping(external, 'TCP') + tasks = [service.delete_port_mapping(external, 'TCP') + for external in registered] + if tasks: + await asyncio.wait(tasks) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deregister_port) return True diff --git a/requirements_all.txt b/requirements_all.txt index 8fe360df8e8..d26f8717384 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -517,9 +517,6 @@ mficlient==0.3.0 # homeassistant.components.sensor.miflora miflora==0.3.0 -# homeassistant.components.upnp -miniupnpc==2.0.2 - # homeassistant.components.sensor.mopar motorparts==1.0.2 @@ -1055,6 +1052,9 @@ pytradfri[async]==5.4.2 # homeassistant.components.device_tracker.unifi pyunifi==2.13 +# homeassistant.components.upnp +pyupnp-async==0.1.0.1 + # homeassistant.components.keyboard # pyuserinput==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 484fd1c39f5..e17cbffe8d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -158,6 +158,9 @@ pythonwhois==2.4.3 # homeassistant.components.device_tracker.unifi pyunifi==2.13 +# homeassistant.components.upnp +pyupnp-async==0.1.0.1 + # homeassistant.components.notify.html5 pywebpush==1.6.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 708d9dbd30b..27b972dcefa 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -76,6 +76,7 @@ TEST_REQUIREMENTS = ( 'pyqwikswitch', 'python-forecastio', 'pyunifi', + 'pyupnp-async', 'pywebpush', 'restrictedpython', 'rflink', diff --git a/tests/components/test_upnp.py b/tests/components/test_upnp.py index e2096d28e58..4956b8a6278 100644 --- a/tests/components/test_upnp.py +++ b/tests/components/test_upnp.py @@ -1,5 +1,4 @@ """Test the UPNP component.""" -import asyncio from collections import OrderedDict from unittest.mock import patch, MagicMock @@ -7,15 +6,64 @@ import pytest from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component +from homeassistant.components.upnp import IP_SERVICE, DATA_UPNP + + +class MockService(MagicMock): + """Mock upnp IP service.""" + + async def add_port_mapping(self, *args, **kwargs): + """Original function.""" + self.mock_add_port_mapping(*args, **kwargs) + + async def delete_port_mapping(self, *args, **kwargs): + """Original function.""" + self.mock_delete_port_mapping(*args, **kwargs) + + +class MockDevice(MagicMock): + """Mock upnp device.""" + + def find_first_service(self, *args, **kwargs): + """Original function.""" + self._service = MockService() + return self._service + + def peep_first_service(self): + """Access Mock first service.""" + return self._service + + +class MockResp(MagicMock): + """Mock upnp msearch response.""" + + async def get_device(self, *args, **kwargs): + """Original function.""" + device = MockDevice() + service = {'serviceType': IP_SERVICE} + device.services = [service] + return device @pytest.fixture -def mock_miniupnpc(): - """Mock miniupnpc.""" - mock = MagicMock() +def mock_msearch_first(*args, **kwargs): + """Wrapper to async mock function.""" + async def async_mock_msearch_first(*args, **kwargs): + """Mock msearch_first.""" + return MockResp(*args, **kwargs) - with patch.dict('sys.modules', {'miniupnpc': mock}): - yield mock.UPnP() + with patch('pyupnp_async.msearch_first', new=async_mock_msearch_first): + yield + + +@pytest.fixture +def mock_async_exception(*args, **kwargs): + """Wrapper to async mock function with exception.""" + async def async_mock_exception(*args, **kwargs): + return Exception + + with patch('pyupnp_async.msearch_first', new=async_mock_exception): + yield @pytest.fixture @@ -26,75 +74,66 @@ def mock_local_ip(): yield -@pytest.fixture(autouse=True) -def mock_discovery(): - """Mock discovery of upnp sensor.""" - with patch('homeassistant.components.upnp.discovery'): - yield - - -@asyncio.coroutine -def test_setup_fail_if_no_ip(hass): +async def test_setup_fail_if_no_ip(hass): """Test setup fails if we can't find a local IP.""" with patch('homeassistant.components.upnp.get_local_ip', return_value='127.0.0.1'): - result = yield from async_setup_component(hass, 'upnp', { + result = await async_setup_component(hass, 'upnp', { 'upnp': {} }) assert not result -@asyncio.coroutine -def test_setup_fail_if_cannot_select_igd(hass, mock_local_ip, mock_miniupnpc): +async def test_setup_fail_if_cannot_select_igd(hass, + mock_local_ip, + mock_async_exception): """Test setup fails if we can't find an UPnP IGD.""" - mock_miniupnpc.selectigd.side_effect = Exception - - result = yield from async_setup_component(hass, 'upnp', { + result = await async_setup_component(hass, 'upnp', { 'upnp': {} }) assert not result -@asyncio.coroutine -def test_setup_succeeds_if_specify_ip(hass, mock_miniupnpc): +async def test_setup_succeeds_if_specify_ip(hass, mock_msearch_first): """Test setup succeeds if we specify IP and can't find a local IP.""" with patch('homeassistant.components.upnp.get_local_ip', return_value='127.0.0.1'): - result = yield from async_setup_component(hass, 'upnp', { + result = await async_setup_component(hass, 'upnp', { 'upnp': { 'local_ip': '192.168.0.10' } }) assert result + mock_service = hass.data[DATA_UPNP].peep_first_service() + assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 + mock_service.mock_add_port_mapping.assert_called_once_with( + 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') -@asyncio.coroutine -def test_no_config_maps_hass_local_to_remote_port(hass, mock_miniupnpc): +async def test_no_config_maps_hass_local_to_remote_port(hass, + mock_local_ip, + mock_msearch_first): """Test by default we map local to remote port.""" - result = yield from async_setup_component(hass, 'upnp', { - 'upnp': { - 'local_ip': '192.168.0.10' - } + result = await async_setup_component(hass, 'upnp', { + 'upnp': {} }) assert result - assert len(mock_miniupnpc.addportmapping.mock_calls) == 1 - external, _, host, internal, _, _ = \ - mock_miniupnpc.addportmapping.mock_calls[0][1] - assert host == '192.168.0.10' - assert external == 8123 - assert internal == 8123 + mock_service = hass.data[DATA_UPNP].peep_first_service() + assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 + mock_service.mock_add_port_mapping.assert_called_once_with( + 8123, 8123, '192.168.0.10', 'TCP', desc='Home Assistant') -@asyncio.coroutine -def test_map_hass_to_remote_port(hass, mock_miniupnpc): +async def test_map_hass_to_remote_port(hass, + mock_local_ip, + mock_msearch_first): """Test mapping hass to remote port.""" - result = yield from async_setup_component(hass, 'upnp', { + result = await async_setup_component(hass, 'upnp', { 'upnp': { - 'local_ip': '192.168.0.10', 'ports': { 'hass': 1000 } @@ -102,41 +141,38 @@ def test_map_hass_to_remote_port(hass, mock_miniupnpc): }) assert result - assert len(mock_miniupnpc.addportmapping.mock_calls) == 1 - external, _, host, internal, _, _ = \ - mock_miniupnpc.addportmapping.mock_calls[0][1] - assert external == 1000 - assert internal == 8123 + mock_service = hass.data[DATA_UPNP].peep_first_service() + assert len(mock_service.mock_add_port_mapping.mock_calls) == 1 + mock_service.mock_add_port_mapping.assert_called_once_with( + 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') -@asyncio.coroutine -def test_map_internal_to_remote_ports(hass, mock_miniupnpc): +async def test_map_internal_to_remote_ports(hass, + mock_local_ip, + mock_msearch_first): """Test mapping local to remote ports.""" ports = OrderedDict() ports['hass'] = 1000 ports[1883] = 3883 - result = yield from async_setup_component(hass, 'upnp', { + result = await async_setup_component(hass, 'upnp', { 'upnp': { - 'local_ip': '192.168.0.10', 'ports': ports } }) assert result - assert len(mock_miniupnpc.addportmapping.mock_calls) == 2 - external, _, host, internal, _, _ = \ - mock_miniupnpc.addportmapping.mock_calls[0][1] - assert external == 1000 - assert internal == 8123 + mock_service = hass.data[DATA_UPNP].peep_first_service() + assert len(mock_service.mock_add_port_mapping.mock_calls) == 2 - external, _, host, internal, _, _ = \ - mock_miniupnpc.addportmapping.mock_calls[1][1] - assert external == 3883 - assert internal == 1883 + mock_service.mock_add_port_mapping.assert_any_call( + 8123, 1000, '192.168.0.10', 'TCP', desc='Home Assistant') + mock_service.mock_add_port_mapping.assert_any_call( + 1883, 3883, '192.168.0.10', 'TCP', desc='Home Assistant') hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - yield from hass.async_block_till_done() - assert len(mock_miniupnpc.deleteportmapping.mock_calls) == 2 - assert mock_miniupnpc.deleteportmapping.mock_calls[0][1][0] == 1000 - assert mock_miniupnpc.deleteportmapping.mock_calls[1][1][0] == 3883 + await hass.async_block_till_done() + assert len(mock_service.mock_delete_port_mapping.mock_calls) == 2 + + mock_service.mock_delete_port_mapping.assert_any_call(1000, 'TCP') + mock_service.mock_delete_port_mapping.assert_any_call(3883, 'TCP') From 3906250c9e1e7a314f0bf62cce0bf5155cddb300 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 13 Apr 2018 08:50:58 +0200 Subject: [PATCH 359/924] Update example (fixes #13834) (#13839) --- homeassistant/components/vacuum/services.yaml | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index fea365ac7c7..863157074bc 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -4,93 +4,93 @@ turn_on: description: Start a new cleaning task. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' turn_off: description: Stop the current cleaning task and return to home. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' stop: description: Stop the current cleaning task. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' locate: description: Locate the vacuum cleaner robot. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' start_pause: description: Start, pause, or resume the cleaning task. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' return_to_base: description: Tell the vacuum cleaner to return to its dock. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' clean_spot: description: Tell the vacuum cleaner to do a spot clean-up. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' send_command: description: Send a raw command to the vacuum cleaner. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' command: description: Command to execute. example: 'set_dnd_timer' params: description: Parameters for the command. - example: '[22,0,6,0]' + example: '{ "key": "value" }' set_fan_speed: description: Set the fan speed of the vacuum cleaner. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' fan_speed: - description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium', or by percentage, between 0 and 100. + description: Platform dependent vacuum cleaner fan speed, with speed steps, like 'medium' or by percentage, between 0 and 100. example: 'low' xiaomi_remote_control_start: description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' xiaomi_remote_control_stop: description: Stop remote control mode of the vacuum cleaner. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' xiaomi_remote_control_move: description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' velocity: description: Speed, between -0.29 and 0.29. @@ -106,7 +106,7 @@ xiaomi_remote_control_move_step: description: Remote control the vacuum cleaner, only makes one move and then stops. fields: entity_id: - description: Name of the botvac entity. + description: Name of the vacuum entity. example: 'vacuum.xiaomi_vacuum_cleaner' velocity: description: Speed, between -0.29 and 0.29. From d3b261a25d6b8b67b1c7de9028c31b660799bb8b Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 13 Apr 2018 02:58:57 -0400 Subject: [PATCH 360/924] Add support for deCONZ daylight sensor (#13479) * Add support for deCONZ daylight sensor Bump pydeconz to 34 * Remove 'daylight' reason from async u --- homeassistant/components/sensor/deconz.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 081b304dc55..e569c5578ac 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -16,6 +16,7 @@ from homeassistant.util import slugify DEPENDENCIES = ['deconz'] ATTR_CURRENT = 'current' +ATTR_DAYLIGHT = 'daylight' ATTR_EVENT_ID = 'event_id' @@ -113,6 +114,8 @@ class DeconzSensor(Entity): if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage + if self._sensor.sensor_class == 'daylight': + attr[ATTR_DAYLIGHT] = self._sensor.daylight return attr From 20ababec3e28ced91fc3d3e79b229612a3d9c99d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 07:32:05 -0400 Subject: [PATCH 361/924] Add authentication to error log endpoint (#13836) --- homeassistant/components/api.py | 16 +++++++++++++--- tests/components/test_api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index d272ebcb1c0..6fdf0c027a4 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -52,9 +52,8 @@ def setup(hass, config): hass.http.register_view(APIComponentsView) hass.http.register_view(APITemplateView) - log_path = hass.data.get(DATA_LOGGING, None) - if log_path: - hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False) + if DATA_LOGGING in hass.data: + hass.http.register_view(APIErrorLog) return True @@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView): HTTP_BAD_REQUEST) +class APIErrorLog(HomeAssistantView): + """View to fetch the error log.""" + + url = URL_API_ERROR_LOG + name = "api:error_log" + + async def get(self, request): + """Retrieve API error log.""" + return await self.file(request, request.app['hass'].data[DATA_LOGGING]) + + @asyncio.coroutine def async_services_json(hass): """Generate services data to JSONify.""" diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 6d5bec046f1..c9dae27d14c 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -2,13 +2,18 @@ # pylint: disable=protected-access import asyncio import json +from unittest.mock import patch +from aiohttp import web import pytest from homeassistant import const +from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component +from tests.common import mock_coro + @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -398,3 +403,31 @@ def _stream_next_event(stream): def _listen_count(hass): """Return number of event listeners.""" return sum(hass.bus.async_listeners().values()) + + +async def test_api_error_log(hass, aiohttp_client): + """Test if we can fetch the error log.""" + hass.data[DATA_LOGGING] = '/some/path' + await async_setup_component(hass, 'api', { + 'http': { + 'api_password': 'yolo' + } + }) + client = await aiohttp_client(hass.http.app) + + resp = await client.get(const.URL_API_ERROR_LOG) + # Verufy auth required + assert resp.status == 401 + + with patch( + 'homeassistant.components.http.view.HomeAssistantView.file', + return_value=mock_coro(web.Response(status=200, text='Hello')) + ) as mock_file: + resp = await client.get(const.URL_API_ERROR_LOG, headers={ + 'x-ha-access': 'yolo' + }) + + assert len(mock_file.mock_calls) == 1 + assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING] + assert resp.status == 200 + assert await resp.text() == 'Hello' From ddd20036292853c1436f60a3d3ec7726a5db6714 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 13 Apr 2018 13:25:03 +0100 Subject: [PATCH 362/924] initialize queue before filtering (#13842) --- homeassistant/components/sensor/filter.py | 2 +- tests/components/sensor/test_filter.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 27730a8f63e..5b28faf78ca 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -341,7 +341,7 @@ class OutlierFilter(Filter): def _filter_state(self, new_state): """Implement the outlier filter.""" - if (self.states and + if (len(self.states) == self.states.maxlen and abs(new_state.state - statistics.median([s.state for s in self.states])) > self._radius): diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 8b8e7607b07..43432f3304c 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -95,11 +95,11 @@ class TestFilterSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get('sensor.test') - self.assertEqual('19.25', state.state) + self.assertEqual('17.05', state.state) def test_outlier(self): """Test if outlier filter works.""" - filt = OutlierFilter(window_size=10, + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) @@ -107,6 +107,17 @@ class TestFilterSensor(unittest.TestCase): filtered = filt.filter_state(state) self.assertEqual(22, filtered.state) + def test_initial_outlier(self): + """Test issue #13363.""" + filt = OutlierFilter(window_size=3, + precision=2, + entity=None, + radius=4.0) + out = ha.State('sensor.test_monitored', 4000) + for state in [out]+self.values: + filtered = filt.filter_state(state) + self.assertEqual(22, filtered.state) + def test_lowpass(self): """Test if lowpass filter works.""" filt = LowPassFilter(window_size=10, From 60508f72158bcf5fd1b553f03a2d8a15917bc085 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 10:14:53 -0400 Subject: [PATCH 363/924] Extract config flow to own module (#13840) * Extract config flow to own module * Lint * fix lint * fix typo * ConfigFlowHandler -> FlowHandler * Rename to data_entry_flow --- .../components/config/config_entries.py | 20 +- homeassistant/components/deconz/__init__.py | 4 +- homeassistant/components/discovery.py | 4 +- homeassistant/components/hue/config_flow.py | 4 +- homeassistant/config_entries.py | 199 +++--------------- homeassistant/data_entry_flow.py | 180 ++++++++++++++++ tests/common.py | 4 +- .../components/config/test_config_entries.py | 21 +- tests/components/test_discovery.py | 4 +- tests/test_config_entries.py | 190 +---------------- tests/test_data_entry_flow.py | 186 ++++++++++++++++ 11 files changed, 428 insertions(+), 388 deletions(-) create mode 100644 homeassistant/data_entry_flow.py create mode 100644 tests/test_data_entry_flow.py diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index aa42325b75b..967317134c2 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -3,7 +3,7 @@ import asyncio import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator @@ -24,7 +24,7 @@ def async_setup(hass): def _prepare_json(result): """Convert result for JSON.""" - if result['type'] != config_entries.RESULT_TYPE_FORM: + if result['type'] != data_entry_flow.RESULT_TYPE_FORM: return result import voluptuous_serialize @@ -94,8 +94,8 @@ class ConfigManagerFlowIndexView(HomeAssistantView): hass = request.app['hass'] return self.json([ - flow for flow in hass.config_entries.flow.async_progress() - if flow['source'] != config_entries.SOURCE_USER]) + flw for flw in hass.config_entries.flow.async_progress() + if flw['source'] != data_entry_flow.SOURCE_USER]) @RequestDataValidator(vol.Schema({ vol.Required('domain'): str, @@ -108,9 +108,9 @@ class ConfigManagerFlowIndexView(HomeAssistantView): try: result = yield from hass.config_entries.flow.async_init( data['domain']) - except config_entries.UnknownHandler: + except data_entry_flow.UnknownHandler: return self.json_message('Invalid handler specified', 404) - except config_entries.UnknownStep: + except data_entry_flow.UnknownStep: return self.json_message('Handler does not support init', 400) result = _prepare_json(result) @@ -126,13 +126,13 @@ class ConfigManagerFlowResourceView(HomeAssistantView): @asyncio.coroutine def get(self, request, flow_id): - """Get the current state of a flow.""" + """Get the current state of a data_entry_flow.""" hass = request.app['hass'] try: result = yield from hass.config_entries.flow.async_configure( flow_id) - except config_entries.UnknownFlow: + except data_entry_flow.UnknownFlow: return self.json_message('Invalid flow specified', 404) result = _prepare_json(result) @@ -148,7 +148,7 @@ class ConfigManagerFlowResourceView(HomeAssistantView): try: result = yield from hass.config_entries.flow.async_configure( flow_id, data) - except config_entries.UnknownFlow: + except data_entry_flow.UnknownFlow: return self.json_message('Invalid flow specified', 404) except vol.Invalid: return self.json_message('User input malformed', 400) @@ -164,7 +164,7 @@ class ConfigManagerFlowResourceView(HomeAssistantView): try: hass.config_entries.flow.async_abort(flow_id) - except config_entries.UnknownFlow: + except data_entry_flow.UnknownFlow: return self.json_message('Invalid flow specified', 404) return self.json_message('Flow aborted') diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 85ba271ec3a..04cd42ca620 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) @@ -191,7 +191,7 @@ async def async_request_configuration(hass, config, deconz_config): @config_entries.HANDLERS.register(DOMAIN) -class DeconzFlowHandler(config_entries.ConfigFlowHandler): +class DeconzFlowHandler(data_entry_flow.FlowHandler): """Handle a deCONZ config flow.""" VERSION = 1 diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 677a13d6a9d..693cd3d90f1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -13,7 +13,7 @@ import os import voluptuous as vol -from homeassistant import config_entries +from homeassistant import data_entry_flow from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv @@ -119,7 +119,7 @@ async def async_setup(hass, config): if service in CONFIG_ENTRY_HANDLERS: await hass.config_entries.flow.async_init( CONFIG_ENTRY_HANDLERS[service], - source=config_entries.SOURCE_DISCOVERY, + source=data_entry_flow.SOURCE_DISCOVERY, data=info ) return diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 11e399c984d..af67a594495 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -6,7 +6,7 @@ import os import async_timeout import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -41,7 +41,7 @@ def _find_username_from_config(hass, filename): @config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(config_entries.ConfigFlowHandler): +class HueFlowHandler(data_entry_flow.FlowHandler): """Handle a Hue config flow.""" VERSION = 1 diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e2e45cb5819..d06bf8f1f8f 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -27,7 +27,7 @@ At a minimum, each config flow will have to define a version number and the 'init' step. @config_entries.HANDLERS.register(DOMAIN) - class ExampleConfigFlow(config_entries.ConfigFlowHandler): + class ExampleConfigFlow(config_entries.FlowHandler): VERSION = 1 @@ -117,6 +117,7 @@ import uuid from .core import callback from .exceptions import HomeAssistantError +from .data_entry_flow import FlowManager from .setup import async_setup_component, async_process_deps_reqs from .util.json import load_json, save_json from .util.decorator import Registry @@ -130,17 +131,11 @@ FLOWS = [ 'hue', ] -SOURCE_USER = 'user' -SOURCE_DISCOVERY = 'discovery' PATH_CONFIG = '.config_entries.json' SAVE_DELAY = 1 -RESULT_TYPE_FORM = 'form' -RESULT_TYPE_CREATE_ENTRY = 'create_entry' -RESULT_TYPE_ABORT = 'abort' - ENTRY_STATE_LOADED = 'loaded' ENTRY_STATE_SETUP_ERROR = 'setup_error' ENTRY_STATE_NOT_LOADED = 'not_loaded' @@ -251,18 +246,6 @@ class UnknownEntry(ConfigError): """Unknown entry specified.""" -class UnknownHandler(ConfigError): - """Unknown handler specified.""" - - -class UnknownFlow(ConfigError): - """Uknown flow specified.""" - - -class UnknownStep(ConfigError): - """Unknown step specified.""" - - class ConfigEntries: """Manage the configuration entries. @@ -272,7 +255,8 @@ class ConfigEntries: def __init__(self, hass, hass_config): """Initialize the entry manager.""" self.hass = hass - self.flow = FlowManager(hass, hass_config, self._async_add_entry) + self.flow = FlowManager(hass, HANDLERS, self._async_missing_handler, + self._async_save_entry) self._hass_config = hass_config self._entries = None self._sched_save = None @@ -357,8 +341,15 @@ class ConfigEntries: await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) - async def _async_add_entry(self, entry): + async def _async_save_entry(self, result): """Add an entry.""" + entry = ConfigEntry( + version=result['version'], + domain=result['domain'], + title=result['title'], + data=result['data'], + source=result['source'], + ) self._entries.append(entry) self._async_schedule_save() @@ -371,6 +362,18 @@ class ConfigEntries: await async_setup_component( self.hass, entry.domain, self._hass_config) + async def _async_missing_handler(self, domain): + """Called when a flow handler is not loaded.""" + # This will load the component and thus register the handler + component = getattr(self.hass.components, domain) + + if domain not in HANDLERS: + return + + # Make sure requirements and dependencies of component are resolved + await async_process_deps_reqs( + self.hass, self._hass_config, domain, component) + @callback def _async_schedule_save(self): """Schedule saving the entity registry.""" @@ -388,157 +391,3 @@ class ConfigEntries: await self.hass.async_add_job( save_json, self.hass.config.path(PATH_CONFIG), data) - - -class FlowManager: - """Manage all the config flows that are in progress.""" - - def __init__(self, hass, hass_config, async_add_entry): - """Initialize the flow manager.""" - self.hass = hass - self._hass_config = hass_config - self._progress = {} - self._async_add_entry = async_add_entry - - @callback - def async_progress(self): - """Return the flows in progress.""" - return [{ - 'flow_id': flow.flow_id, - 'domain': flow.domain, - 'source': flow.source, - } for flow in self._progress.values()] - - async def async_init(self, domain, *, source=SOURCE_USER, data=None): - """Start a configuration flow.""" - handler = HANDLERS.get(domain) - - if handler is None: - # This will load the component and thus register the handler - component = getattr(self.hass.components, domain) - handler = HANDLERS.get(domain) - - if handler is None: - raise UnknownHandler - - # Make sure requirements and dependencies of component are resolved - await async_process_deps_reqs( - self.hass, self._hass_config, domain, component) - - flow_id = uuid.uuid4().hex - flow = self._progress[flow_id] = handler() - flow.hass = self.hass - flow.domain = domain - flow.flow_id = flow_id - flow.source = source - - if source == SOURCE_USER: - step = 'init' - else: - step = source - - return await self._async_handle_step(flow, step, data) - - async def async_configure(self, flow_id, user_input=None): - """Start or continue a configuration flow.""" - flow = self._progress.get(flow_id) - - if flow is None: - raise UnknownFlow - - step_id, data_schema = flow.cur_step - - if data_schema is not None and user_input is not None: - user_input = data_schema(user_input) - - return await self._async_handle_step( - flow, step_id, user_input) - - @callback - def async_abort(self, flow_id): - """Abort a flow.""" - if self._progress.pop(flow_id, None) is None: - raise UnknownFlow - - async def _async_handle_step(self, flow, step_id, user_input): - """Handle a step of a flow.""" - method = "async_step_{}".format(step_id) - - if not hasattr(flow, method): - self._progress.pop(flow.flow_id) - raise UnknownStep("Handler {} doesn't support step {}".format( - flow.__class__.__name__, step_id)) - - result = await getattr(flow, method)(user_input) - - if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_ABORT): - raise ValueError( - 'Handler returned incorrect type: {}'.format(result['type'])) - - if result['type'] == RESULT_TYPE_FORM: - flow.cur_step = (result['step_id'], result['data_schema']) - return result - - # Abort and Success results both finish the flow - self._progress.pop(flow.flow_id) - - if result['type'] == RESULT_TYPE_ABORT: - return result - - entry = ConfigEntry( - version=flow.VERSION, - domain=flow.domain, - title=result['title'], - data=result.pop('data'), - source=flow.source - ) - await self._async_add_entry(entry) - return result - - -class ConfigFlowHandler: - """Handle the configuration flow of a component.""" - - # Set by flow manager - flow_id = None - hass = None - domain = None - source = SOURCE_USER - cur_step = None - - # Set by dev - # VERSION - - @callback - def async_show_form(self, *, step_id, data_schema=None, errors=None): - """Return the definition of a form to gather user input.""" - return { - 'type': RESULT_TYPE_FORM, - 'flow_id': self.flow_id, - 'domain': self.domain, - 'step_id': step_id, - 'data_schema': data_schema, - 'errors': errors, - } - - @callback - def async_create_entry(self, *, title, data): - """Finish config flow and create a config entry.""" - return { - 'type': RESULT_TYPE_CREATE_ENTRY, - 'flow_id': self.flow_id, - 'domain': self.domain, - 'title': title, - 'data': data, - } - - @callback - def async_abort(self, *, reason): - """Abort the config flow.""" - return { - 'type': RESULT_TYPE_ABORT, - 'flow_id': self.flow_id, - 'domain': self.domain, - 'reason': reason - } diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py new file mode 100644 index 00000000000..5644481210c --- /dev/null +++ b/homeassistant/data_entry_flow.py @@ -0,0 +1,180 @@ +"""Classes to help gather user submissions.""" +import logging +import uuid + +from .core import callback +from .exceptions import HomeAssistantError + +_LOGGER = logging.getLogger(__name__) + +SOURCE_USER = 'user' +SOURCE_DISCOVERY = 'discovery' + +RESULT_TYPE_FORM = 'form' +RESULT_TYPE_CREATE_ENTRY = 'create_entry' +RESULT_TYPE_ABORT = 'abort' + + +class FlowError(HomeAssistantError): + """Error while configuring an account.""" + + +class UnknownHandler(FlowError): + """Unknown handler specified.""" + + +class UnknownFlow(FlowError): + """Uknown flow specified.""" + + +class UnknownStep(FlowError): + """Unknown step specified.""" + + +class FlowManager: + """Manage all the flows that are in progress.""" + + def __init__(self, hass, handlers, async_missing_handler, + async_save_entry): + """Initialize the flow manager.""" + self.hass = hass + self._handlers = handlers + self._progress = {} + self._async_missing_handler = async_missing_handler + self._async_save_entry = async_save_entry + + @callback + def async_progress(self): + """Return the flows in progress.""" + return [{ + 'flow_id': flow.flow_id, + 'domain': flow.domain, + 'source': flow.source, + } for flow in self._progress.values()] + + async def async_init(self, domain, *, source=SOURCE_USER, data=None): + """Start a configuration flow.""" + handler = self._handlers.get(domain) + + if handler is None: + await self._async_missing_handler(domain) + handler = self._handlers.get(domain) + + if handler is None: + raise UnknownHandler + + flow_id = uuid.uuid4().hex + flow = self._progress[flow_id] = handler() + flow.hass = self.hass + flow.domain = domain + flow.flow_id = flow_id + flow.source = source + + if source == SOURCE_USER: + step = 'init' + else: + step = source + + return await self._async_handle_step(flow, step, data) + + async def async_configure(self, flow_id, user_input=None): + """Start or continue a configuration flow.""" + flow = self._progress.get(flow_id) + + if flow is None: + raise UnknownFlow + + step_id, data_schema = flow.cur_step + + if data_schema is not None and user_input is not None: + user_input = data_schema(user_input) + + return await self._async_handle_step( + flow, step_id, user_input) + + @callback + def async_abort(self, flow_id): + """Abort a flow.""" + if self._progress.pop(flow_id, None) is None: + raise UnknownFlow + + async def _async_handle_step(self, flow, step_id, user_input): + """Handle a step of a flow.""" + method = "async_step_{}".format(step_id) + + if not hasattr(flow, method): + self._progress.pop(flow.flow_id) + raise UnknownStep("Handler {} doesn't support step {}".format( + flow.__class__.__name__, step_id)) + + result = await getattr(flow, method)(user_input) + + if result['type'] not in (RESULT_TYPE_FORM, RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_ABORT): + raise ValueError( + 'Handler returned incorrect type: {}'.format(result['type'])) + + if result['type'] == RESULT_TYPE_FORM: + flow.cur_step = (result['step_id'], result['data_schema']) + return result + + # Abort and Success results both finish the flow + self._progress.pop(flow.flow_id) + + if result['type'] == RESULT_TYPE_ABORT: + return result + + # We pass a copy of the result because we're going to mutate our + # version afterwards and don't want to cause unexpected bugs. + await self._async_save_entry(dict(result)) + result.pop('data') + return result + + +class FlowHandler: + """Handle the configuration flow of a component.""" + + # Set by flow manager + flow_id = None + hass = None + domain = None + source = SOURCE_USER + cur_step = None + + # Set by developer + VERSION = 1 + + @callback + def async_show_form(self, *, step_id, data_schema=None, errors=None): + """Return the definition of a form to gather user input.""" + return { + 'type': RESULT_TYPE_FORM, + 'flow_id': self.flow_id, + 'domain': self.domain, + 'step_id': step_id, + 'data_schema': data_schema, + 'errors': errors, + } + + @callback + def async_create_entry(self, *, title, data): + """Finish config flow and create a config entry.""" + return { + 'version': self.VERSION, + 'type': RESULT_TYPE_CREATE_ENTRY, + 'flow_id': self.flow_id, + 'domain': self.domain, + 'title': title, + 'data': data, + 'source': self.source, + } + + @callback + def async_abort(self, *, reason): + """Abort the config flow.""" + return { + 'type': RESULT_TYPE_ABORT, + 'flow_id': self.flow_id, + 'domain': self.domain, + 'reason': reason + } diff --git a/tests/common.py b/tests/common.py index 54c214da4e9..67fd8bab23f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,7 +10,7 @@ import logging import threading from contextlib import contextmanager -from homeassistant import core as ha, loader, config_entries +from homeassistant import core as ha, loader, data_entry_flow, config_entries from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( @@ -455,7 +455,7 @@ class MockConfigEntry(config_entries.ConfigEntry): """Helper for creating config entries that adds some defaults.""" def __init__(self, *, domain='test', data=None, version=0, entry_id=None, - source=config_entries.SOURCE_USER, title='Mock Title', + source=data_entry_flow.SOURCE_USER, title='Mock Title', state=None): """Initialize a mock config entry.""" kwargs = { diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index cfe6b12baac..d6490763951 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -8,7 +8,8 @@ import pytest import voluptuous as vol from homeassistant import config_entries as core_ce -from homeassistant.config_entries import ConfigFlowHandler, HANDLERS +from homeassistant.config_entries import HANDLERS +from homeassistant.data_entry_flow import FlowHandler from homeassistant.setup import async_setup_component from homeassistant.components.config import config_entries from homeassistant.loader import set_component @@ -93,7 +94,7 @@ def test_available_flows(hass, client): @asyncio.coroutine def test_initialize_flow(hass, client): """Test we can initialize a flow.""" - class TestFlow(ConfigFlowHandler): + class TestFlow(FlowHandler): @asyncio.coroutine def async_step_init(self, user_input=None): schema = OrderedDict() @@ -142,7 +143,7 @@ def test_initialize_flow(hass, client): @asyncio.coroutine def test_abort(hass, client): """Test a flow that aborts.""" - class TestFlow(ConfigFlowHandler): + class TestFlow(FlowHandler): @asyncio.coroutine def async_step_init(self, user_input=None): return self.async_abort(reason='bla') @@ -167,7 +168,7 @@ def test_create_account(hass, client): set_component( 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) - class TestFlow(ConfigFlowHandler): + class TestFlow(FlowHandler): VERSION = 1 @asyncio.coroutine @@ -187,7 +188,9 @@ def test_create_account(hass, client): assert data == { 'domain': 'test', 'title': 'Test Entry', - 'type': 'create_entry' + 'type': 'create_entry', + 'source': 'user', + 'version': 1, } @@ -197,7 +200,7 @@ def test_two_step_flow(hass, client): set_component( 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) - class TestFlow(ConfigFlowHandler): + class TestFlow(FlowHandler): VERSION = 1 @asyncio.coroutine @@ -245,13 +248,15 @@ def test_two_step_flow(hass, client): 'domain': 'test', 'type': 'create_entry', 'title': 'user-title', + 'version': 1, + 'source': 'user', } @asyncio.coroutine def test_get_progress_index(hass, client): """Test querying for the flows that are in progress.""" - class TestFlow(ConfigFlowHandler): + class TestFlow(FlowHandler): VERSION = 5 @asyncio.coroutine @@ -283,7 +288,7 @@ def test_get_progress_index(hass, client): @asyncio.coroutine def test_get_progress_flow(hass, client): """Test we can query the API for same result as we get from init a flow.""" - class TestFlow(ConfigFlowHandler): + class TestFlow(FlowHandler): @asyncio.coroutine def async_step_init(self, user_input=None): schema = OrderedDict() diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index b4c80bf3210..f3f63654e8b 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -5,7 +5,7 @@ from unittest.mock import patch, MagicMock import pytest -from homeassistant import config_entries +from homeassistant import data_entry_flow from homeassistant.bootstrap import async_setup_component from homeassistant.components import discovery from homeassistant.util.dt import utcnow @@ -174,5 +174,5 @@ async def test_discover_config_flow(hass): assert len(m_init.mock_calls) == 1 args, kwargs = m_init.mock_calls[0][1:] assert args == ('mock-component',) - assert kwargs['source'] == config_entries.SOURCE_DISCOVERY + assert kwargs['source'] == data_entry_flow.SOURCE_DISCOVERY assert kwargs['data'] == discovery_info diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b9b39b11c13..94b1dcb47da 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3,9 +3,8 @@ import asyncio from unittest.mock import MagicMock, patch, mock_open import pytest -import voluptuous as vol -from homeassistant import config_entries, loader +from homeassistant import config_entries, loader, data_entry_flow from homeassistant.setup import async_setup_component from tests.common import MockModule, mock_coro, MockConfigEntry @@ -100,7 +99,7 @@ def test_add_entry_calls_setup_entry(hass, manager): 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) - class TestFlow(config_entries.ConfigFlowHandler): + class TestFlow(data_entry_flow.FlowHandler): VERSION = 1 @@ -112,7 +111,7 @@ def test_add_entry_calls_setup_entry(hass, manager): 'token': 'supersecret' }) - with patch.dict(config_entries.HANDLERS, {'comp': TestFlow}): + with patch.dict(config_entries.HANDLERS, {'comp': TestFlow, 'beer': 5}): yield from manager.flow.async_init('comp') yield from hass.async_block_till_done() @@ -152,7 +151,7 @@ def test_domains_gets_uniques(manager): @asyncio.coroutine def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" - class TestFlow(config_entries.ConfigFlowHandler): + class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @asyncio.coroutine @@ -167,7 +166,7 @@ def test_saving_and_loading(hass): with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): yield from hass.config_entries.flow.async_init('test') - class Test2Flow(config_entries.ConfigFlowHandler): + class Test2Flow(data_entry_flow.FlowHandler): VERSION = 3 @asyncio.coroutine @@ -212,185 +211,6 @@ def test_saving_and_loading(hass): assert orig.source == loaded.source -####################### -# FLOW MANAGER TESTS # -####################### - -@asyncio.coroutine -def test_configure_reuses_handler_instance(manager): - """Test that we reuse instances.""" - class TestFlow(config_entries.ConfigFlowHandler): - handle_count = 0 - - @asyncio.coroutine - def async_step_init(self, user_input=None): - self.handle_count += 1 - return self.async_show_form( - errors={'base': str(self.handle_count)}, - step_id='init') - - with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - form = yield from manager.flow.async_init('test') - assert form['errors']['base'] == '1' - form = yield from manager.flow.async_configure(form['flow_id']) - assert form['errors']['base'] == '2' - assert len(manager.flow.async_progress()) == 1 - assert len(manager.async_entries()) == 0 - - -@asyncio.coroutine -def test_configure_two_steps(manager): - """Test that we reuse instances.""" - class TestFlow(config_entries.ConfigFlowHandler): - VERSION = 1 - - @asyncio.coroutine - def async_step_init(self, user_input=None): - if user_input is not None: - self.init_data = user_input - return self.async_step_second() - return self.async_show_form( - step_id='init', - data_schema=vol.Schema([str]) - ) - - @asyncio.coroutine - def async_step_second(self, user_input=None): - if user_input is not None: - return self.async_create_entry( - title='Test Entry', - data=self.init_data + user_input - ) - return self.async_show_form( - step_id='second', - data_schema=vol.Schema([str]) - ) - - with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - form = yield from manager.flow.async_init('test') - - with pytest.raises(vol.Invalid): - form = yield from manager.flow.async_configure( - form['flow_id'], 'INCORRECT-DATA') - - form = yield from manager.flow.async_configure( - form['flow_id'], ['INIT-DATA']) - form = yield from manager.flow.async_configure( - form['flow_id'], ['SECOND-DATA']) - assert form['type'] == config_entries.RESULT_TYPE_CREATE_ENTRY - assert len(manager.flow.async_progress()) == 0 - assert len(manager.async_entries()) == 1 - entry = manager.async_entries()[0] - assert entry.domain == 'test' - assert entry.data == ['INIT-DATA', 'SECOND-DATA'] - - -@asyncio.coroutine -def test_show_form(manager): - """Test that abort removes the flow from progress.""" - schema = vol.Schema({ - vol.Required('username'): str, - vol.Required('password'): str - }) - - class TestFlow(config_entries.ConfigFlowHandler): - @asyncio.coroutine - def async_step_init(self, user_input=None): - return self.async_show_form( - step_id='init', - data_schema=schema, - errors={ - 'username': 'Should be unique.' - } - ) - - with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - form = yield from manager.flow.async_init('test') - assert form['type'] == 'form' - assert form['data_schema'] is schema - assert form['errors'] == { - 'username': 'Should be unique.' - } - - -@asyncio.coroutine -def test_abort_removes_instance(manager): - """Test that abort removes the flow from progress.""" - class TestFlow(config_entries.ConfigFlowHandler): - is_new = True - - @asyncio.coroutine - def async_step_init(self, user_input=None): - old = self.is_new - self.is_new = False - return self.async_abort(reason=str(old)) - - with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - form = yield from manager.flow.async_init('test') - assert form['reason'] == 'True' - assert len(manager.flow.async_progress()) == 0 - assert len(manager.async_entries()) == 0 - form = yield from manager.flow.async_init('test') - assert form['reason'] == 'True' - assert len(manager.flow.async_progress()) == 0 - assert len(manager.async_entries()) == 0 - - -@asyncio.coroutine -def test_create_saves_data(manager): - """Test creating a config entry.""" - class TestFlow(config_entries.ConfigFlowHandler): - VERSION = 5 - - @asyncio.coroutine - def async_step_init(self, user_input=None): - return self.async_create_entry( - title='Test Title', - data='Test Data' - ) - - with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - yield from manager.flow.async_init('test') - assert len(manager.flow.async_progress()) == 0 - assert len(manager.async_entries()) == 1 - - entry = manager.async_entries()[0] - assert entry.version == 5 - assert entry.domain == 'test' - assert entry.title == 'Test Title' - assert entry.data == 'Test Data' - assert entry.source == config_entries.SOURCE_USER - - -@asyncio.coroutine -def test_discovery_init_flow(manager): - """Test a flow initialized by discovery.""" - class TestFlow(config_entries.ConfigFlowHandler): - VERSION = 5 - - @asyncio.coroutine - def async_step_discovery(self, info): - return self.async_create_entry(title=info['id'], data=info) - - data = { - 'id': 'hello', - 'token': 'secret' - } - - with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): - yield from manager.flow.async_init( - 'test', source=config_entries.SOURCE_DISCOVERY, data=data) - assert len(manager.flow.async_progress()) == 0 - assert len(manager.async_entries()) == 1 - - entry = manager.async_entries()[0] - assert entry.version == 5 - assert entry.domain == 'test' - assert entry.title == 'hello' - assert entry.data == data - assert entry.source == config_entries.SOURCE_DISCOVERY - - async def test_forward_entry_sets_up_component(hass): """Test we setup the component entry is forwarded to.""" entry = MockConfigEntry(domain='original') diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py new file mode 100644 index 00000000000..f7067871174 --- /dev/null +++ b/tests/test_data_entry_flow.py @@ -0,0 +1,186 @@ +"""Test the flow classes.""" +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.util.decorator import Registry + +from tests.common import mock_coro + + +@pytest.fixture +def manager(): + """Return a flow manager.""" + handlers = Registry() + entries = [] + + async def async_add_entry(result): + entries.append(result) + + manager = data_entry_flow.FlowManager( + None, handlers, mock_coro, async_add_entry) + manager.mock_created_entries = entries + manager.mock_reg_handler = handlers.register + return manager + + +async def test_configure_reuses_handler_instance(manager): + """Test that we reuse instances.""" + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + handle_count = 0 + + async def async_step_init(self, user_input=None): + self.handle_count += 1 + return self.async_show_form( + errors={'base': str(self.handle_count)}, + step_id='init') + + form = await manager.async_init('test') + assert form['errors']['base'] == '1' + form = await manager.async_configure(form['flow_id']) + assert form['errors']['base'] == '2' + assert len(manager.async_progress()) == 1 + assert len(manager.mock_created_entries) == 0 + + +async def test_configure_two_steps(manager): + """Test that we reuse instances.""" + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 1 + + async def async_step_init(self, user_input=None): + if user_input is not None: + self.init_data = user_input + return await self.async_step_second() + return self.async_show_form( + step_id='init', + data_schema=vol.Schema([str]) + ) + + async def async_step_second(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title='Test Entry', + data=self.init_data + user_input + ) + return self.async_show_form( + step_id='second', + data_schema=vol.Schema([str]) + ) + + form = await manager.async_init('test') + + with pytest.raises(vol.Invalid): + form = await manager.async_configure( + form['flow_id'], 'INCORRECT-DATA') + + form = await manager.async_configure( + form['flow_id'], ['INIT-DATA']) + form = await manager.async_configure( + form['flow_id'], ['SECOND-DATA']) + assert form['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 1 + result = manager.mock_created_entries[0] + assert result['domain'] == 'test' + assert result['data'] == ['INIT-DATA', 'SECOND-DATA'] + + +async def test_show_form(manager): + """Test that abort removes the flow from progress.""" + schema = vol.Schema({ + vol.Required('username'): str, + vol.Required('password'): str + }) + + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + async def async_step_init(self, user_input=None): + return self.async_show_form( + step_id='init', + data_schema=schema, + errors={ + 'username': 'Should be unique.' + } + ) + + form = await manager.async_init('test') + assert form['type'] == 'form' + assert form['data_schema'] is schema + assert form['errors'] == { + 'username': 'Should be unique.' + } + + +async def test_abort_removes_instance(manager): + """Test that abort removes the flow from progress.""" + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + is_new = True + + async def async_step_init(self, user_input=None): + old = self.is_new + self.is_new = False + return self.async_abort(reason=str(old)) + + form = await manager.async_init('test') + assert form['reason'] == 'True' + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 0 + form = await manager.async_init('test') + assert form['reason'] == 'True' + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 0 + + +async def test_create_saves_data(manager): + """Test creating a config entry.""" + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_init(self, user_input=None): + return self.async_create_entry( + title='Test Title', + data='Test Data' + ) + + await manager.async_init('test') + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 1 + + entry = manager.mock_created_entries[0] + assert entry['version'] == 5 + assert entry['domain'] == 'test' + assert entry['title'] == 'Test Title' + assert entry['data'] == 'Test Data' + assert entry['source'] == data_entry_flow.SOURCE_USER + + +async def test_discovery_init_flow(manager): + """Test a flow initialized by discovery.""" + @manager.mock_reg_handler('test') + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, info): + return self.async_create_entry(title=info['id'], data=info) + + data = { + 'id': 'hello', + 'token': 'secret' + } + + await manager.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY, data=data) + assert len(manager.async_progress()) == 0 + assert len(manager.mock_created_entries) == 1 + + entry = manager.mock_created_entries[0] + assert entry['version'] == 5 + assert entry['domain'] == 'test' + assert entry['title'] == 'hello' + assert entry['data'] == data + assert entry['source'] == data_entry_flow.SOURCE_DISCOVERY From ac2298189e9ba4b65fe310cb8ce61f289cd678de Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Fri, 13 Apr 2018 10:25:35 -0700 Subject: [PATCH 364/924] Add support for controlling homekit lights and switches (#13346) * Add support for controlling homekit lights and switches This adds support for controlling lights and switches that expose a HomeKit control interface, avoiding the requirement to implement protocol-specific components. * Comment out the homekit requirement This needs to build native code, so leave it commented for now * Review updates * Make HomeKit auto-discovery optional Add an "enable" argument to the discovery component and add a list of optional devices types (currently just HomeKit) to discover * Further review comments * Update requirements_all.txt * Fix houndci complaints * Further review updates * Final review fixup * Lint fixups * Fix discovery tests * Further review updates --- .coveragerc | 3 + homeassistant/components/discovery.py | 16 +- .../components/homekit_controller/__init__.py | 228 ++++++++++++++++++ .../components/light/homekit_controller.py | 134 ++++++++++ .../components/switch/homekit_controller.py | 68 ++++++ requirements_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/test_discovery.py | 3 +- 8 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/homekit_controller/__init__.py create mode 100644 homeassistant/components/light/homekit_controller.py create mode 100644 homeassistant/components/switch/homekit_controller.py diff --git a/.coveragerc b/.coveragerc index 2b733dd699f..3009eed24f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -109,6 +109,9 @@ omit = homeassistant/components/hive.py homeassistant/components/*/hive.py + homeassistant/components/homekit_controller/__init__.py + homeassistant/components/*/homekit_controller.py + homeassistant/components/homematic/__init__.py homeassistant/components/*/homematic.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 693cd3d90f1..7a343018db5 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -40,6 +40,7 @@ SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' +SERVICE_HOMEKIT = 'homekit' CONFIG_ENTRY_HANDLERS = { SERVICE_HUE: 'hue', @@ -79,13 +80,20 @@ SERVICE_HANDLERS = { 'songpal': ('media_player', 'songpal'), } +OPTIONAL_SERVICE_HANDLERS = { + SERVICE_HOMEKIT: ('homekit_controller', None), +} + CONF_IGNORE = 'ignore' +CONF_ENABLE = 'enable' CONFIG_SCHEMA = vol.Schema({ vol.Required(DOMAIN): vol.Schema({ vol.Optional(CONF_IGNORE, default=[]): vol.All(cv.ensure_list, [ - vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]) + vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]), + vol.Optional(CONF_ENABLE, default=[]): + vol.All(cv.ensure_list, [vol.In(OPTIONAL_SERVICE_HANDLERS)]) }), }, extra=vol.ALLOW_EXTRA) @@ -104,6 +112,9 @@ async def async_setup(hass, config): # Platforms ignore by config ignored_platforms = config[DOMAIN][CONF_IGNORE] + # Optional platforms enabled by config + enabled_platforms = config[DOMAIN][CONF_ENABLE] + async def new_service_found(service, info): """Handle a new service if one is found.""" if service in ignored_platforms: @@ -126,6 +137,9 @@ async def async_setup(hass, config): comp_plat = SERVICE_HANDLERS.get(service) + if not comp_plat and service in enabled_platforms: + comp_plat = OPTIONAL_SERVICE_HANDLERS[service] + # We do not know how to handle this service. if not comp_plat: logger.info("Unknown service discovered: %s %s", service, info) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py new file mode 100644 index 00000000000..c33edd07918 --- /dev/null +++ b/homeassistant/components/homekit_controller/__init__.py @@ -0,0 +1,228 @@ +""" +Support for Homekit device discovery. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/homekit_controller/ +""" +import http +import json +import logging +import os +import uuid + +from homeassistant.components.discovery import SERVICE_HOMEKIT +from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['homekit==0.5'] + +DOMAIN = 'homekit_controller' +HOMEKIT_DIR = '.homekit' + +# Mapping from Homekit type to component. +HOMEKIT_ACCESSORY_DISPATCH = { + 'lightbulb': 'light', + 'outlet': 'switch', +} + +KNOWN_ACCESSORIES = "{}-accessories".format(DOMAIN) +KNOWN_DEVICES = "{}-devices".format(DOMAIN) + +_LOGGER = logging.getLogger(__name__) + + +def homekit_http_send(self, message_body=None): + r"""Send the currently buffered request and clear the buffer. + + Appends an extra \r\n to the buffer. + A message_body may be specified, to be appended to the request. + """ + self._buffer.extend((b"", b"")) + msg = b"\r\n".join(self._buffer) + del self._buffer[:] + + if message_body is not None: + msg = msg + message_body + + self.send(msg) + + +def get_serial(accessory): + """Obtain the serial number of a HomeKit device.""" + # pylint: disable=import-error + import homekit + for service in accessory['services']: + if homekit.ServicesTypes.get_short(service['type']) != \ + 'accessory-information': + continue + for characteristic in service['characteristics']: + ctype = homekit.CharacteristicsTypes.get_short( + characteristic['type']) + if ctype != 'serial-number': + continue + return characteristic['value'] + return None + + +class HKDevice(): + """HomeKit device.""" + + def __init__(self, hass, host, port, model, hkid, config_num, config): + """Initialise a generic HomeKit device.""" + # pylint: disable=import-error + import homekit + + _LOGGER.info("Setting up Homekit device %s", model) + self.hass = hass + self.host = host + self.port = port + self.model = model + self.hkid = hkid + self.config_num = config_num + self.config = config + self.configurator = hass.components.configurator + + data_dir = os.path.join(hass.config.path(), HOMEKIT_DIR) + if not os.path.isdir(data_dir): + os.mkdir(data_dir) + + self.pairing_file = os.path.join(data_dir, 'hk-{}'.format(hkid)) + self.pairing_data = homekit.load_pairing(self.pairing_file) + + # Monkey patch httpclient for increased compatibility + # pylint: disable=protected-access + http.client.HTTPConnection._send_output = homekit_http_send + + self.conn = http.client.HTTPConnection(self.host, port=self.port) + if self.pairing_data is not None: + self.accessory_setup() + else: + self.configure() + + def accessory_setup(self): + """Handle setup of a HomeKit accessory.""" + # pylint: disable=import-error + import homekit + self.controllerkey, self.accessorykey = \ + homekit.get_session_keys(self.conn, self.pairing_data) + self.securecon = homekit.SecureHttp(self.conn.sock, + self.accessorykey, + self.controllerkey) + response = self.securecon.get('/accessories') + data = json.loads(response.read().decode()) + for accessory in data['accessories']: + serial = get_serial(accessory) + if serial in self.hass.data[KNOWN_ACCESSORIES]: + continue + self.hass.data[KNOWN_ACCESSORIES][serial] = self + aid = accessory['aid'] + for service in accessory['services']: + service_info = {'serial': serial, + 'aid': aid, + 'iid': service['iid']} + devtype = homekit.ServicesTypes.get_short(service['type']) + _LOGGER.debug("Found %s", devtype) + component = HOMEKIT_ACCESSORY_DISPATCH.get(devtype, None) + if component is not None: + discovery.load_platform(self.hass, component, DOMAIN, + service_info, self.config) + + def device_config_callback(self, callback_data): + """Handle initial pairing.""" + # pylint: disable=import-error + import homekit + pairing_id = str(uuid.uuid4()) + code = callback_data.get('code').strip() + self.pairing_data = homekit.perform_pair_setup( + self.conn, code, pairing_id) + if self.pairing_data is not None: + homekit.save_pairing(self.pairing_file, self.pairing_data) + self.accessory_setup() + else: + error_msg = "Unable to pair, please try again" + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + + def configure(self): + """Obtain the pairing code for a HomeKit device.""" + description = "Please enter the HomeKit code for your {}".format( + self.model) + self.hass.data[DOMAIN+self.hkid] = \ + self.configurator.request_config(self.model, + self.device_config_callback, + description=description, + submit_caption="submit", + fields=[{'id': 'code', + 'name': 'HomeKit code', + 'type': 'string'}]) + + +class HomeKitEntity(Entity): + """Representation of a Home Assistant HomeKit device.""" + + def __init__(self, accessory, devinfo): + """Initialise a generic HomeKit device.""" + self._name = accessory.model + self._securecon = accessory.securecon + self._aid = devinfo['aid'] + self._iid = devinfo['iid'] + self._address = "homekit-{}-{}".format(devinfo['serial'], self._iid) + self._features = 0 + self._chars = {} + + def update(self): + """Obtain a HomeKit device's state.""" + response = self._securecon.get('/accessories') + data = json.loads(response.read().decode()) + for accessory in data['accessories']: + if accessory['aid'] != self._aid: + continue + for service in accessory['services']: + if service['iid'] != self._iid: + continue + self.update_characteristics(service['characteristics']) + break + + @property + def unique_id(self): + """Return the ID of this device.""" + return self._address + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + def update_characteristics(self, characteristics): + """Synchronise a HomeKit device state with Home Assistant.""" + raise NotImplementedError + + +# pylint: too-many-function-args +def setup(hass, config): + """Set up for Homekit devices.""" + def discovery_dispatch(service, discovery_info): + """Dispatcher for Homekit discovery events.""" + # model, id + host = discovery_info['host'] + port = discovery_info['port'] + model = discovery_info['properties']['md'] + hkid = discovery_info['properties']['id'] + config_num = int(discovery_info['properties']['c#']) + + # Only register a device once, but rescan if the config has changed + if hkid in hass.data[KNOWN_DEVICES]: + device = hass.data[KNOWN_DEVICES][hkid] + if config_num > device.config_num and \ + device.pairing_info is not None: + device.accessory_setup() + return + + _LOGGER.debug('Discovered unique device %s', hkid) + device = HKDevice(hass, host, port, model, hkid, config_num, config) + hass.data[KNOWN_DEVICES][hkid] = device + + hass.data[KNOWN_ACCESSORIES] = {} + hass.data[KNOWN_DEVICES] = {} + discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch) + return True diff --git a/homeassistant/components/light/homekit_controller.py b/homeassistant/components/light/homekit_controller.py new file mode 100644 index 00000000000..e6dc09e455c --- /dev/null +++ b/homeassistant/components/light/homekit_controller.py @@ -0,0 +1,134 @@ +""" +Support for Homekit lights. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.homekit_controller/ +""" +import json +import logging + +from homeassistant.components.homekit_controller import ( + HomeKitEntity, KNOWN_ACCESSORIES) +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_COLOR_TEMP, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light) + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Homekit lighting.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_devices([HomeKitLight(accessory, discovery_info)], True) + + +class HomeKitLight(HomeKitEntity, Light): + """Representation of a Homekit light.""" + + def __init__(self, *args): + """Initialise the light.""" + super().__init__(*args) + self._on = None + self._brightness = None + self._color_temperature = None + self._hue = None + self._saturation = None + + def update_characteristics(self, characteristics): + """Synchronise light state with Home Assistant.""" + # pylint: disable=import-error + import homekit + + for characteristic in characteristics: + ctype = characteristic['type'] + ctype = homekit.CharacteristicsTypes.get_short(ctype) + if ctype == "on": + self._chars['on'] = characteristic['iid'] + self._on = characteristic['value'] + elif ctype == 'brightness': + self._chars['brightness'] = characteristic['iid'] + self._features |= SUPPORT_BRIGHTNESS + self._brightness = characteristic['value'] + elif ctype == 'color-temperature': + self._chars['color_temperature'] = characteristic['iid'] + self._features |= SUPPORT_COLOR_TEMP + self._color_temperature = characteristic['value'] + elif ctype == "hue": + self._chars['hue'] = characteristic['iid'] + self._features |= SUPPORT_COLOR + self._hue = characteristic['value'] + elif ctype == "saturation": + self._chars['saturation'] = characteristic['iid'] + self._features |= SUPPORT_COLOR + self._saturation = characteristic['value'] + + @property + def is_on(self): + """Return true if device is on.""" + return self._on + + @property + def brightness(self): + """Return the brightness of this light between 0..255.""" + if self._features & SUPPORT_BRIGHTNESS: + return self._brightness * 255 / 100 + return None + + @property + def hs_color(self): + """Return the color property.""" + if self._features & SUPPORT_COLOR: + return (self._hue, self._saturation) + return None + + @property + def color_temp(self): + """Return the color temperature.""" + if self._features & SUPPORT_COLOR_TEMP: + return self._color_temperature + return None + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + def turn_on(self, **kwargs): + """Turn the specified light on.""" + hs_color = kwargs.get(ATTR_HS_COLOR) + temperature = kwargs.get(ATTR_COLOR_TEMP) + brightness = kwargs.get(ATTR_BRIGHTNESS) + + characteristics = [] + if hs_color is not None: + characteristics.append({'aid': self._aid, + 'iid': self._chars['hue'], + 'value': hs_color[0]}) + characteristics.append({'aid': self._aid, + 'iid': self._chars['saturation'], + 'value': hs_color[1]}) + if brightness is not None: + characteristics.append({'aid': self._aid, + 'iid': self._chars['brightness'], + 'value': int(brightness * 100 / 255)}) + + if temperature is not None: + characteristics.append({'aid': self._aid, + 'iid': self._chars['color-temperature'], + 'value': int(temperature)}) + characteristics.append({'aid': self._aid, + 'iid': self._chars['on'], + 'value': True}) + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) + + def turn_off(self, **kwargs): + """Turn the specified light off.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['on'], + 'value': False}] + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) diff --git a/homeassistant/components/switch/homekit_controller.py b/homeassistant/components/switch/homekit_controller.py new file mode 100644 index 00000000000..6b97200ba49 --- /dev/null +++ b/homeassistant/components/switch/homekit_controller.py @@ -0,0 +1,68 @@ +""" +Support for Homekit switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.homekit_controller/ +""" +import json +import logging + +from homeassistant.components.homekit_controller import (HomeKitEntity, + KNOWN_ACCESSORIES) +from homeassistant.components.switch import SwitchDevice + +DEPENDENCIES = ['homekit_controller'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up Homekit switch support.""" + if discovery_info is not None: + accessory = hass.data[KNOWN_ACCESSORIES][discovery_info['serial']] + add_devices([HomeKitSwitch(accessory, discovery_info)], True) + + +class HomeKitSwitch(HomeKitEntity, SwitchDevice): + """Representation of a Homekit switch.""" + + def __init__(self, *args): + """Initialise the switch.""" + super().__init__(*args) + self._on = None + + def update_characteristics(self, characteristics): + """Synchronise the switch state with Home Assistant.""" + # pylint: disable=import-error + import homekit + + for characteristic in characteristics: + ctype = characteristic['type'] + ctype = homekit.CharacteristicsTypes.get_short(ctype) + if ctype == "on": + self._chars['on'] = characteristic['iid'] + self._on = characteristic['value'] + elif ctype == "outlet-in-use": + self._chars['outlet-in-use'] = characteristic['iid'] + + @property + def is_on(self): + """Return true if device is on.""" + return self._on + + def turn_on(self, **kwargs): + """Turn the specified switch on.""" + self._on = True + characteristics = [{'aid': self._aid, + 'iid': self._chars['on'], + 'value': True}] + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) + + def turn_off(self, **kwargs): + """Turn the specified switch off.""" + characteristics = [{'aid': self._aid, + 'iid': self._chars['on'], + 'value': False}] + body = json.dumps({'characteristics': characteristics}) + self._securecon.put('/characteristics', body) diff --git a/requirements_all.txt b/requirements_all.txt index d26f8717384..290f538f4a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -381,6 +381,9 @@ holidays==0.9.4 # homeassistant.components.frontend home-assistant-frontend==20180404.0 +# homeassistant.components.homekit_controller +# homekit==0.5 + # homeassistant.components.homematicip_cloud homematicip==0.8 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 27b972dcefa..f15425063b4 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -33,6 +33,7 @@ COMMENT_REQUIREMENTS = ( 'i2csense', 'credstash', 'bme680', + 'homekit', ) TEST_REQUIREMENTS = ( diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index f3f63654e8b..a956b672ec5 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -25,7 +25,8 @@ UNKNOWN_SERVICE = 'this_service_will_never_be_supported' BASE_CONFIG = { discovery.DOMAIN: { - 'ignore': [] + 'ignore': [], + 'enable': [] } } From b9306a5e521b855b6dddf4b92136a2c3c24b82e2 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Thu, 12 Apr 2018 15:44:56 +0200 Subject: [PATCH 365/924] Channel up/down for LiveTV and next/previous for other apps (#13829) --- homeassistant/components/media_player/webostv.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index 860d69e22c3..ae9d259a47c 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -35,6 +35,7 @@ CONF_SOURCES = 'sources' CONF_ON_ACTION = 'turn_on_action' DEFAULT_NAME = 'LG webOS Smart TV' +LIVETV_APP_ID = 'com.webos.app.livetv' WEBOSTV_CONFIG_FILE = 'webostv.conf' @@ -357,8 +358,16 @@ class LgWebOSDevice(MediaPlayerDevice): def media_next_track(self): """Send next track command.""" - self._client.channel_up() + current_input = self._client.get_input() + if current_input == LIVETV_APP_ID: + self._client.channel_up() + else: + self._client.fast_forward() def media_previous_track(self): """Send the previous track command.""" - self._client.channel_down() + current_input = self._client.get_input() + if current_input == LIVETV_APP_ID: + self._client.channel_down() + else: + self._client.rewind() From 598f093bf0fecdefaa3d95d1ddae71317a05321e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 07:32:05 -0400 Subject: [PATCH 366/924] Add authentication to error log endpoint (#13836) --- homeassistant/components/api.py | 16 +++++++++++++--- tests/components/test_api.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index d272ebcb1c0..6fdf0c027a4 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -52,9 +52,8 @@ def setup(hass, config): hass.http.register_view(APIComponentsView) hass.http.register_view(APITemplateView) - log_path = hass.data.get(DATA_LOGGING, None) - if log_path: - hass.http.register_static_path(URL_API_ERROR_LOG, log_path, False) + if DATA_LOGGING in hass.data: + hass.http.register_view(APIErrorLog) return True @@ -356,6 +355,17 @@ class APITemplateView(HomeAssistantView): HTTP_BAD_REQUEST) +class APIErrorLog(HomeAssistantView): + """View to fetch the error log.""" + + url = URL_API_ERROR_LOG + name = "api:error_log" + + async def get(self, request): + """Retrieve API error log.""" + return await self.file(request, request.app['hass'].data[DATA_LOGGING]) + + @asyncio.coroutine def async_services_json(hass): """Generate services data to JSONify.""" diff --git a/tests/components/test_api.py b/tests/components/test_api.py index 6d5bec046f1..c9dae27d14c 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -2,13 +2,18 @@ # pylint: disable=protected-access import asyncio import json +from unittest.mock import patch +from aiohttp import web import pytest from homeassistant import const +from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component +from tests.common import mock_coro + @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -398,3 +403,31 @@ def _stream_next_event(stream): def _listen_count(hass): """Return number of event listeners.""" return sum(hass.bus.async_listeners().values()) + + +async def test_api_error_log(hass, aiohttp_client): + """Test if we can fetch the error log.""" + hass.data[DATA_LOGGING] = '/some/path' + await async_setup_component(hass, 'api', { + 'http': { + 'api_password': 'yolo' + } + }) + client = await aiohttp_client(hass.http.app) + + resp = await client.get(const.URL_API_ERROR_LOG) + # Verufy auth required + assert resp.status == 401 + + with patch( + 'homeassistant.components.http.view.HomeAssistantView.file', + return_value=mock_coro(web.Response(status=200, text='Hello')) + ) as mock_file: + resp = await client.get(const.URL_API_ERROR_LOG, headers={ + 'x-ha-access': 'yolo' + }) + + assert len(mock_file.mock_calls) == 1 + assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING] + assert resp.status == 200 + assert await resp.text() == 'Hello' From c36c2be37218a1e97a6c4c1b9d3c5dd23e873c6b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 16:52:50 -0400 Subject: [PATCH 367/924] Version bump to 0.67.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 53c72a46c3f..5364fe6951e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 0daf38d18c2171455865ca4d3fc7e14fb3449d62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 13 Apr 2018 18:02:51 -0400 Subject: [PATCH 368/924] Version bump to 0.68.0.dev0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5364fe6951e..43380d00a2d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 67 -PATCH_VERSION = '0' +MINOR_VERSION = 68 +PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c6c166645d56d7d94380ddf0f51b3bf858b69c22 Mon Sep 17 00:00:00 2001 From: geekofweek Date: Fri, 13 Apr 2018 20:36:46 -0500 Subject: [PATCH 369/924] bump python-ecobee-api version to 0.0.18 (#13854) * bump python-ecobee-api version to 0.0.18 * Update requirements_all.txt --- homeassistant/components/ecobee.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee.py b/homeassistant/components/ecobee.py index d1503dc74dc..9c29cea704c 100644 --- a/homeassistant/components/ecobee.py +++ b/homeassistant/components/ecobee.py @@ -16,7 +16,7 @@ from homeassistant.const import CONF_API_KEY from homeassistant.util import Throttle from homeassistant.util.json import save_json -REQUIREMENTS = ['python-ecobee-api==0.0.17'] +REQUIREMENTS = ['python-ecobee-api==0.0.18'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 290f538f4a1..0729b0e39d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -942,7 +942,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.0.17 +python-ecobee-api==0.0.18 # homeassistant.components.climate.eq3btsmart # python-eq3bt==0.1.9 From 99ded8a0a662513ab0c50f9a3f000369c6a190da Mon Sep 17 00:00:00 2001 From: Mohamad Tarbin Date: Fri, 13 Apr 2018 21:54:23 -0400 Subject: [PATCH 370/924] Adding USCIS component (#13764) * Adding USCIS component * Adding Line after the class DOC * Update : Extract USCIS logic code to Component * Update : Extract USCIS logic code to Component * Adding CURRENT_STATUS * Change Error handling, remove date from attributes * Update the Version for USCIS * Update uscis.py --- .coveragerc | 1 + homeassistant/components/sensor/uscis.py | 87 ++++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 91 insertions(+) create mode 100644 homeassistant/components/sensor/uscis.py diff --git a/.coveragerc b/.coveragerc index 3009eed24f0..70cfbded98f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -675,6 +675,7 @@ omit = homeassistant/components/sensor/uber.py homeassistant/components/sensor/upnp.py homeassistant/components/sensor/ups.py + homeassistant/components/sensor/uscis.py homeassistant/components/sensor/vasttrafik.py homeassistant/components/sensor/viaggiatreno.py homeassistant/components/sensor/waqi.py diff --git a/homeassistant/components/sensor/uscis.py b/homeassistant/components/sensor/uscis.py new file mode 100644 index 00000000000..ed3c9ca8587 --- /dev/null +++ b/homeassistant/components/sensor/uscis.py @@ -0,0 +1,87 @@ +""" +Support for USCIS Case Status. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.uscis/ +""" + +import logging +from datetime import timedelta +import voluptuous as vol + +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_FRIENDLY_NAME + + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['uscisstatus==0.1.1'] + +DEFAULT_NAME = "USCIS" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, + vol.Required('case_id'): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setting the platform in HASS and Case Information.""" + uscis = UscisSensor(config['case_id'], config[CONF_FRIENDLY_NAME]) + uscis.update() + if uscis.valid_case_id: + add_devices([uscis]) + else: + _LOGGER.error("Setup USCIS Sensor Fail" + " check if your Case ID is Valid") + + +class UscisSensor(Entity): + """USCIS Sensor will check case status on daily basis.""" + + MIN_TIME_BETWEEN_UPDATES = timedelta(hours=24) + + CURRENT_STATUS = "current_status" + LAST_CASE_UPDATE = "last_update_date" + + def __init__(self, case, name): + """Initialize the sensor.""" + self._state = None + self._case_id = case + self._attributes = None + self.valid_case_id = None + self._name = name + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Using Request to access USCIS website and fetch data.""" + import uscisstatus + try: + status = uscisstatus.get_case_status(self._case_id) + self._attributes = { + self.CURRENT_STATUS: status['status'] + } + self._state = status['date'] + self.valid_case_id = True + + except ValueError: + _LOGGER("Please Check that you have valid USCIS case id") + self.valid_case_id = False diff --git a/requirements_all.txt b/requirements_all.txt index 0729b0e39d9..90420444feb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1268,6 +1268,9 @@ upcloud-api==0.4.2 # homeassistant.components.sensor.ups upsmychoice==1.0.6 +# homeassistant.components.sensor.uscis +uscisstatus==0.1.1 + # homeassistant.components.camera.uvc uvcclient==0.10.1 From 80a3220b88f0febc0a060b656c44c212dd973b19 Mon Sep 17 00:00:00 2001 From: dersger Date: Sat, 14 Apr 2018 04:22:02 +0200 Subject: [PATCH 371/924] Avoid unnecessary cast state updates (#13770) * Avoid unnecessary cast state updates * Add test * Fixed bad syntax * Fixed imports * Fixed test --- homeassistant/components/media_player/cast.py | 46 +++++++--- tests/components/media_player/test_cast.py | 85 ++++++++++++++++++- 2 files changed, 119 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 2edda0645b0..30d4bd166d0 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -288,7 +288,8 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None - self.media_status_received = None + self.media_status_position = None + self.media_status_position_received = None self._available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] @@ -361,7 +362,8 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None self.cast_status = None self.media_status = None - self.media_status_received = None + self.media_status_position = None + self.media_status_position_received = None self._status_listener.invalidate() self._status_listener = None @@ -388,8 +390,36 @@ class CastDevice(MediaPlayerDevice): def new_media_status(self, media_status): """Handle updates of the media status.""" + # Only use media position for playing/paused, + # and for normal playback rate + if (media_status is None or + abs(media_status.playback_rate - 1) > 0.01 or + not (media_status.player_is_playing or + media_status.player_is_paused)): + self.media_status_position = None + self.media_status_position_received = None + else: + # Avoid unnecessary state attribute updates if player_state and + # calculated position stay the same + now = dt_util.utcnow() + do_update = \ + (self.media_status is None or + self.media_status_position is None or + self.media_status.player_state != media_status.player_state) + if not do_update: + if media_status.player_is_playing: + elapsed = now - self.media_status_position_received + do_update = abs(media_status.current_time - + (self.media_status_position + + elapsed.total_seconds())) > 1 + else: + do_update = \ + self.media_status_position != media_status.current_time + if do_update: + self.media_status_position = media_status.current_time + self.media_status_position_received = now + self.media_status = media_status - self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() def new_connection_status(self, connection_status): @@ -595,13 +625,7 @@ class CastDevice(MediaPlayerDevice): @property def media_position(self): """Position of current playing media in seconds.""" - if self.media_status is None or \ - not (self.media_status.player_is_playing or - self.media_status.player_is_paused or - self.media_status.player_is_idle): - return None - - return self.media_status.current_time + return self.media_status_position @property def media_position_updated_at(self): @@ -609,7 +633,7 @@ class CastDevice(MediaPlayerDevice): Returns value from homeassistant.util.dt.utcnow(). """ - return self.media_status_received + return self.media_status_position_received @property def unique_id(self) -> Optional[str]: diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index ee69ec1c85d..0c0f3906dc2 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,6 +1,7 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access import asyncio +import datetime as dt from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID @@ -14,7 +15,8 @@ from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send -from homeassistant.components.media_player import cast +from homeassistant.components.media_player import cast, \ + ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT from homeassistant.setup import async_setup_component @@ -286,6 +288,8 @@ async def test_entity_media_states(hass: HomeAssistantType): assert entity.unique_id == full_info.uuid media_status = MagicMock(images=None) + media_status.current_time = 0 + media_status.playback_rate = 1 media_status.player_is_playing = True entity.new_media_status(media_status) await hass.async_block_till_done() @@ -320,6 +324,85 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == 'unknown' +async def test_entity_media_position(hass: HomeAssistantType): + """Test various entity media states.""" + info = get_fake_chromecast_info() + full_info = attr.evolve(info, model_name='google home', + friendly_name='Speaker', uuid=FakeUUID) + + with patch('pychromecast.dial.get_device_status', + return_value=full_info): + chromecast, entity = await async_setup_media_player_cast(hass, info) + + media_status = MagicMock(images=None) + media_status.current_time = 10 + media_status.playback_rate = 1 + media_status.player_is_playing = True + media_status.player_is_paused = False + media_status.player_is_idle = False + now = dt.datetime.now(dt.timezone.utc) + with patch('homeassistant.util.dt.utcnow', return_value=now): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 10 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now + + media_status.current_time = 15 + now_plus_5 = now + dt.timedelta(seconds=5) + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 10 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now + + media_status.current_time = 20 + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 20 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5 + + media_status.current_time = 25 + now_plus_10 = now + dt.timedelta(seconds=10) + media_status.player_is_playing = False + media_status.player_is_paused = True + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_10): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 25 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 + + now_plus_15 = now + dt.timedelta(seconds=15) + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_15): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 25 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 + + media_status.current_time = 30 + now_plus_20 = now + dt.timedelta(seconds=20) + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert state.attributes[ATTR_MEDIA_POSITION] == 30 + assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20 + + media_status.player_is_paused = False + media_status.player_is_idle = True + with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): + entity.new_media_status(media_status) + await hass.async_block_till_done() + state = hass.states.get('media_player.speaker') + assert ATTR_MEDIA_POSITION not in state.attributes + assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes + + async def test_switched_host(hass: HomeAssistantType): """Test cast device listens for changed hosts and disconnects old cast.""" info = get_fake_chromecast_info() From ee6acadae20507afa6ed13523ea674610b43e79d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Apr 2018 04:31:03 -0400 Subject: [PATCH 372/924] Prevent vesync doing I/O in event loop (#13862) --- homeassistant/components/switch/vesync.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/vesync.py b/homeassistant/components/switch/vesync.py index fbc73545e19..d8579a508e2 100644 --- a/homeassistant/components/switch/vesync.py +++ b/homeassistant/components/switch/vesync.py @@ -60,6 +60,8 @@ class VeSyncSwitchHA(SwitchDevice): def __init__(self, plug): """Initialize the VeSync switch device.""" self.smartplug = plug + self._current_power_w = None + self._today_energy_kwh = None @property def unique_id(self): @@ -74,12 +76,12 @@ class VeSyncSwitchHA(SwitchDevice): @property def current_power_w(self): """Return the current power usage in W.""" - return self.smartplug.get_power() + return self._current_power_w @property def today_energy_kwh(self): """Return the today total energy usage in kWh.""" - return self.smartplug.get_kwh_today() + return self._today_energy_kwh @property def available(self) -> bool: @@ -102,3 +104,5 @@ class VeSyncSwitchHA(SwitchDevice): def update(self): """Handle data changes for node values.""" self.smartplug.update() + self._current_power_w = self.smartplug.get_power() + self._today_energy_kwh = self.smartplug.get_kwh_today() From c3388d63a1929a6c197644d1ae1f622c4d58c793 Mon Sep 17 00:00:00 2001 From: TheCellMC Date: Sat, 14 Apr 2018 08:32:44 +0000 Subject: [PATCH 373/924] Update yweather.py (#13851) * Update yweather.py * Update yweather.py * Update yweather.py * Update yweather.py --- homeassistant/components/weather/yweather.py | 31 ++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 5987cf7621f..8e638895660 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -32,20 +32,27 @@ DEFAULT_NAME = 'Yweather' SCAN_INTERVAL = timedelta(minutes=10) CONDITION_CLASSES = { - 'clear-night': [31], - 'cloudy': [26, 27, 28, 29, 30], + 'clear-night': [31, 33], + 'sunny': [32, 34, 25, 36], + 'windy': [24], + 'fair': [34], + 'fair-night': [33], + 'cloudy': [26], + 'mostly-cloudy': [28], + 'mostly-cloudy-night': [27], + 'partly-cloudy': [30, 44], + 'partly-cloudy-night': [29], 'fog': [19, 20, 21, 22, 23], 'hail': [17, 18, 35], - 'lightning': [37], - 'lightning-rainy': [3, 4, 38, 39, 47], - 'partlycloudy': [44], - 'pouring': [40, 45], - 'rainy': [9, 11, 12], - 'snowy': [8, 13, 14, 15, 16, 41, 42, 43], - 'snowy-rainy': [5, 6, 7, 10, 46], - 'sunny': [32, 33, 34, 25, 36], - 'windy': [24], - 'windy-variant': [], + 'light-rain': [8, 9], + 'light-snow': [14], + 'heavy-rain': [11, 12, 45, 40], + 'heavy-snow': [41, 42, 43, 46], + 'snowy': [13, 15, 16], + 'rainy': [10], + 'snowy-rainy': [5, 6, 7, 10], + 'lightning': [3, 4, 37, 38, 39], + 'lightning-rainy': [45, 47], 'exceptional': [0, 1, 2], } From 5a5dad689b3421522b8cfa473c111d155eb2ecb6 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 14 Apr 2018 14:31:12 +0200 Subject: [PATCH 374/924] add support for Kodi discovery (#13790) * add support for Kodi discovery * remove "too many blank lines" * register service only once * optimize "workflow" --- homeassistant/components/discovery.py | 1 + homeassistant/components/media_player/kodi.py | 38 ++++++++++++++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 7a343018db5..31ec3f2f60a 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -78,6 +78,7 @@ SERVICE_HANDLERS = { 'bose_soundtouch': ('media_player', 'soundtouch'), 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), + 'kodi': ('media_player', 'kodi'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 9f2a653b8ee..770d57b5b8e 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -8,6 +8,7 @@ import asyncio from collections import OrderedDict from functools import wraps import logging +import socket import urllib import re @@ -157,13 +158,29 @@ def _check_deprecated_turn_off(hass, turn_off_action): def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Kodi platform.""" if DATA_KODI not in hass.data: - hass.data[DATA_KODI] = [] - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - tcp_port = config.get(CONF_TCP_PORT) - encryption = config.get(CONF_PROXY_SSL) - websocket = config.get(CONF_ENABLE_WEBSOCKET) + hass.data[DATA_KODI] = dict() + + # Is this a manual configuration? + if discovery_info is None: + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + tcp_port = config.get(CONF_TCP_PORT) + encryption = config.get(CONF_PROXY_SSL) + websocket = config.get(CONF_ENABLE_WEBSOCKET) + else: + name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname')) + host = discovery_info.get('host') + port = discovery_info.get('port') + tcp_port = DEFAULT_TCP_PORT + encryption = DEFAULT_PROXY_SSL + websocket = DEFAULT_ENABLE_WEBSOCKET + + # Only add a device once, so discovered devices do not override manual + # config. + ip_addr = socket.gethostbyname(host) + if ip_addr in hass.data[DATA_KODI]: + return entity = KodiDevice( hass, @@ -175,7 +192,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): turn_off_action=config.get(CONF_TURN_OFF_ACTION), timeout=config.get(CONF_TIMEOUT), websocket=websocket) - hass.data[DATA_KODI].append(entity) + hass.data[DATA_KODI][ip_addr] = entity async_add_devices([entity], update_before_add=True) @asyncio.coroutine @@ -189,10 +206,11 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): if key != 'entity_id'} entity_ids = service.data.get('entity_id') if entity_ids: - target_players = [player for player in hass.data[DATA_KODI] + target_players = [player + for player in hass.data[DATA_KODI].values() if player.entity_id in entity_ids] else: - target_players = hass.data[DATA_KODI] + target_players = hass.data[DATA_KODI].values() update_tasks = [] for player in target_players: From 4d44c0feff03ddf1c0c57bd130cd1bdfa8c6621a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Apr 2018 14:38:24 -0400 Subject: [PATCH 375/924] Further untangle data entry flow (#13855) * Further untangle data entry flow * Fix test * Remove helper class --- homeassistant/config_entries.py | 27 ++++++++------ homeassistant/data_entry_flow.py | 35 +++++++------------ .../components/config/test_config_entries.py | 12 +++---- tests/components/test_discovery.py | 2 +- tests/test_data_entry_flow.py | 18 ++++++---- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d06bf8f1f8f..e143c94197e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -115,9 +115,9 @@ import logging import os import uuid +from . import data_entry_flow from .core import callback from .exceptions import HomeAssistantError -from .data_entry_flow import FlowManager from .setup import async_setup_component, async_process_deps_reqs from .util.json import load_json, save_json from .util.decorator import Registry @@ -255,8 +255,8 @@ class ConfigEntries: def __init__(self, hass, hass_config): """Initialize the entry manager.""" self.hass = hass - self.flow = FlowManager(hass, HANDLERS, self._async_missing_handler, - self._async_save_entry) + self.flow = data_entry_flow.FlowManager( + hass, self._async_create_flow, self._async_save_entry) self._hass_config = hass_config self._entries = None self._sched_save = None @@ -345,7 +345,7 @@ class ConfigEntries: """Add an entry.""" entry = ConfigEntry( version=result['version'], - domain=result['domain'], + domain=result['handler'], title=result['title'], data=result['data'], source=result['source'], @@ -362,17 +362,22 @@ class ConfigEntries: await async_setup_component( self.hass, entry.domain, self._hass_config) - async def _async_missing_handler(self, domain): - """Called when a flow handler is not loaded.""" - # This will load the component and thus register the handler - component = getattr(self.hass.components, domain) + async def _async_create_flow(self, handler): + """Create a flow for specified handler. - if domain not in HANDLERS: - return + Handler key is the domain of the component that we want to setup. + """ + component = getattr(self.hass.components, handler) + handler = HANDLERS.get(handler) + + if handler is None: + raise data_entry_flow.UnknownHandler # Make sure requirements and dependencies of component are resolved await async_process_deps_reqs( - self.hass, self._hass_config, domain, component) + self.hass, self._hass_config, handler, component) + + return handler() @callback def _async_schedule_save(self): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 5644481210c..361b6653cfd 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -34,13 +34,11 @@ class UnknownStep(FlowError): class FlowManager: """Manage all the flows that are in progress.""" - def __init__(self, hass, handlers, async_missing_handler, - async_save_entry): + def __init__(self, hass, async_create_flow, async_save_entry): """Initialize the flow manager.""" self.hass = hass - self._handlers = handlers self._progress = {} - self._async_missing_handler = async_missing_handler + self._async_create_flow = async_create_flow self._async_save_entry = async_save_entry @callback @@ -48,27 +46,18 @@ class FlowManager: """Return the flows in progress.""" return [{ 'flow_id': flow.flow_id, - 'domain': flow.domain, + 'handler': flow.handler, 'source': flow.source, } for flow in self._progress.values()] - async def async_init(self, domain, *, source=SOURCE_USER, data=None): + async def async_init(self, handler, *, source=SOURCE_USER, data=None): """Start a configuration flow.""" - handler = self._handlers.get(domain) - - if handler is None: - await self._async_missing_handler(domain) - handler = self._handlers.get(domain) - - if handler is None: - raise UnknownHandler - - flow_id = uuid.uuid4().hex - flow = self._progress[flow_id] = handler() + flow = await self._async_create_flow(handler) flow.hass = self.hass - flow.domain = domain - flow.flow_id = flow_id + flow.handler = handler + flow.flow_id = uuid.uuid4().hex flow.source = source + self._progress[flow.flow_id] = flow if source == SOURCE_USER: step = 'init' @@ -137,7 +126,7 @@ class FlowHandler: # Set by flow manager flow_id = None hass = None - domain = None + handler = None source = SOURCE_USER cur_step = None @@ -150,7 +139,7 @@ class FlowHandler: return { 'type': RESULT_TYPE_FORM, 'flow_id': self.flow_id, - 'domain': self.domain, + 'handler': self.handler, 'step_id': step_id, 'data_schema': data_schema, 'errors': errors, @@ -163,7 +152,7 @@ class FlowHandler: 'version': self.VERSION, 'type': RESULT_TYPE_CREATE_ENTRY, 'flow_id': self.flow_id, - 'domain': self.domain, + 'handler': self.handler, 'title': title, 'data': data, 'source': self.source, @@ -175,6 +164,6 @@ class FlowHandler: return { 'type': RESULT_TYPE_ABORT, 'flow_id': self.flow_id, - 'domain': self.domain, + 'handler': self.handler, 'reason': reason } diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index d6490763951..70cb6c3fbaa 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -120,7 +120,7 @@ def test_initialize_flow(hass, client): assert data == { 'type': 'form', - 'domain': 'test', + 'handler': 'test', 'step_id': 'init', 'data_schema': [ { @@ -156,7 +156,7 @@ def test_abort(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { - 'domain': 'test', + 'handler': 'test', 'reason': 'bla', 'type': 'abort' } @@ -186,7 +186,7 @@ def test_create_account(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { - 'domain': 'test', + 'handler': 'test', 'title': 'Test Entry', 'type': 'create_entry', 'source': 'user', @@ -226,7 +226,7 @@ def test_two_step_flow(hass, client): flow_id = data.pop('flow_id') assert data == { 'type': 'form', - 'domain': 'test', + 'handler': 'test', 'step_id': 'account', 'data_schema': [ { @@ -245,7 +245,7 @@ def test_two_step_flow(hass, client): data = yield from resp.json() data.pop('flow_id') assert data == { - 'domain': 'test', + 'handler': 'test', 'type': 'create_entry', 'title': 'user-title', 'version': 1, @@ -279,7 +279,7 @@ def test_get_progress_index(hass, client): assert data == [ { 'flow_id': form['flow_id'], - 'domain': 'test', + 'handler': 'test', 'source': 'hassio' } ] diff --git a/tests/components/test_discovery.py b/tests/components/test_discovery.py index a956b672ec5..dd22c87cb18 100644 --- a/tests/components/test_discovery.py +++ b/tests/components/test_discovery.py @@ -169,7 +169,7 @@ async def test_discover_config_flow(hass): with patch.dict(discovery.CONFIG_ENTRY_HANDLERS, { 'mock-service': 'mock-component'}), patch( - 'homeassistant.config_entries.FlowManager.async_init') as m_init: + 'homeassistant.data_entry_flow.FlowManager.async_init') as m_init: await mock_discovery(hass, discover) assert len(m_init.mock_calls) == 1 diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index f7067871174..2767e206c30 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -5,8 +5,6 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.util.decorator import Registry -from tests.common import mock_coro - @pytest.fixture def manager(): @@ -14,11 +12,19 @@ def manager(): handlers = Registry() entries = [] + async def async_create_flow(handler_name): + handler = handlers.get(handler_name) + + if handler is None: + raise data_entry_flow.UnknownHandler + + return handler() + async def async_add_entry(result): entries.append(result) manager = data_entry_flow.FlowManager( - None, handlers, mock_coro, async_add_entry) + None, async_create_flow, async_add_entry) manager.mock_created_entries = entries manager.mock_reg_handler = handlers.register return manager @@ -84,7 +90,7 @@ async def test_configure_two_steps(manager): assert len(manager.async_progress()) == 0 assert len(manager.mock_created_entries) == 1 result = manager.mock_created_entries[0] - assert result['domain'] == 'test' + assert result['handler'] == 'test' assert result['data'] == ['INIT-DATA', 'SECOND-DATA'] @@ -153,7 +159,7 @@ async def test_create_saves_data(manager): entry = manager.mock_created_entries[0] assert entry['version'] == 5 - assert entry['domain'] == 'test' + assert entry['handler'] == 'test' assert entry['title'] == 'Test Title' assert entry['data'] == 'Test Data' assert entry['source'] == data_entry_flow.SOURCE_USER @@ -180,7 +186,7 @@ async def test_discovery_init_flow(manager): entry = manager.mock_created_entries[0] assert entry['version'] == 5 - assert entry['domain'] == 'test' + assert entry['handler'] == 'test' assert entry['title'] == 'hello' assert entry['data'] == data assert entry['source'] == data_entry_flow.SOURCE_DISCOVERY From 1617fbea4c4570bea02d7d94bbe314248a5a5a17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Apr 2018 14:41:21 -0400 Subject: [PATCH 376/924] Update frontend to 20180414.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3fc3eff0a14..80b7cdff5a8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180404.0'] +REQUIREMENTS = ['home-assistant-frontend==20180414.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 90420444feb..d5036a795fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -379,7 +379,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180404.0 +home-assistant-frontend==20180414.0 # homeassistant.components.homekit_controller # homekit==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e17cbffe8d6..901074bcad3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180404.0 +home-assistant-frontend==20180414.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bf98b793c5cfb163f587047b24cfc55e041adbe9 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 14 Apr 2018 23:53:35 +0200 Subject: [PATCH 377/924] Missing property decorator added (#13889) --- homeassistant/components/fan/xiaomi_miio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 8dc6bb54bd1..16affc08467 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -748,6 +748,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + @property def speed_list(self) -> list: """Get the list of available speeds.""" return self._speed_list From bba997e484baaf9a415352a971f8ae91e99a6a27 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Apr 2018 17:58:45 -0400 Subject: [PATCH 378/924] Fix race condition for component loaded before listening (#13887) * Fix race condition for component loaded before listening * async/await syntax --- homeassistant/components/config/__init__.py | 49 +++++++++------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 4d0295c382a..5a8800d9583 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -18,37 +18,26 @@ SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script', ON_DEMAND = ('zwave',) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the config component.""" - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'config', 'config', 'mdi:settings') - @asyncio.coroutine - def setup_panel(panel_name): + async def setup_panel(panel_name): """Set up a panel.""" - panel = yield from async_prepare_setup_platform( + panel = await async_prepare_setup_platform( hass, config, DOMAIN, panel_name) if not panel: return - success = yield from panel.async_setup(hass) + success = await panel.async_setup(hass) if success: key = '{}.{}'.format(DOMAIN, panel_name) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) hass.config.components.add(key) - tasks = [setup_panel(panel_name) for panel_name in SECTIONS] - - for panel_name in ON_DEMAND: - if panel_name in hass.config.components: - tasks.append(setup_panel(panel_name)) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - @callback def component_loaded(event): """Respond to components being loaded.""" @@ -58,6 +47,15 @@ def async_setup(hass, config): hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + tasks = [setup_panel(panel_name) for panel_name in SECTIONS] + + for panel_name in ON_DEMAND: + if panel_name in hass.config.components: + tasks.append(setup_panel(panel_name)) + + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + return True @@ -86,11 +84,10 @@ class BaseEditConfigView(HomeAssistantView): """Set value.""" raise NotImplementedError - @asyncio.coroutine - def get(self, request, config_key): + async def get(self, request, config_key): """Fetch device specific config.""" hass = request.app['hass'] - current = yield from self.read_config(hass) + current = await self.read_config(hass) value = self._get_value(hass, current, config_key) if value is None: @@ -98,11 +95,10 @@ class BaseEditConfigView(HomeAssistantView): return self.json(value) - @asyncio.coroutine - def post(self, request, config_key): + async def post(self, request, config_key): """Validate config and return results.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON specified', 400) @@ -121,10 +117,10 @@ class BaseEditConfigView(HomeAssistantView): hass = request.app['hass'] path = hass.config.path(self.path) - current = yield from self.read_config(hass) + current = await self.read_config(hass) self._write_value(hass, current, config_key, data) - yield from hass.async_add_job(_write, path, current) + await hass.async_add_job(_write, path, current) if self.post_write_hook is not None: hass.async_add_job(self.post_write_hook(hass)) @@ -133,10 +129,9 @@ class BaseEditConfigView(HomeAssistantView): 'result': 'ok', }) - @asyncio.coroutine - def read_config(self, hass): + async def read_config(self, hass): """Read the config.""" - current = yield from hass.async_add_job( + current = await hass.async_add_job( _read, hass.config.path(self.path)) if not current: current = self._empty_config() From 1c4da0c4a6c3146d4783e09614e151f4eb90cbe8 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Sat, 14 Apr 2018 18:07:55 -0400 Subject: [PATCH 379/924] Added snips service descriptions (#13883) * Added snips service descriptions. * Added snips service descriptions. --- homeassistant/components/services.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 519d3b98704..746c3c7f483 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -395,6 +395,18 @@ snips: intent_filter: description: Optional Array of Strings - A list of intents names to restrict the NLU resolution to on the first query. example: turnOnLights, turnOffLights + feedback_on: + description: Turns feedback sounds on. + fields: + site_id: + description: Site to turn sounds on, defaults to all sites (optional) + example: bedroom + feedback_off: + description: Turns feedback sounds off. + fields: + site_id: + description: Site to turn sounds on, defaults to all sites (optional) + example: bedroom input_boolean: toggle: From 9014e2684565066afadd25ea8dc980f63d01a8c4 Mon Sep 17 00:00:00 2001 From: Gerard Date: Sun, 15 Apr 2018 05:15:52 +0200 Subject: [PATCH 380/924] Add unique_id for BMW ConnectedDrive (#13888) * Add unique_id for BMW ConnectedDrive * Changed some comments --- .../components/binary_sensor/bmw_connected_drive.py | 6 ++++++ homeassistant/components/lock/bmw_connected_drive.py | 6 ++++++ homeassistant/components/sensor/bmw_connected_drive.py | 8 +++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index e7af5af988b..0abf6eb1064 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -46,6 +46,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._vehicle = vehicle self._attribute = attribute self._name = '{} {}'.format(self._vehicle.name, self._attribute) + self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._device_class = device_class self._state = None @@ -55,6 +56,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Data update is triggered from BMWConnectedDriveEntity.""" return False + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return self._unique_id + @property def name(self): """Return the name of the binary sensor.""" diff --git a/homeassistant/components/lock/bmw_connected_drive.py b/homeassistant/components/lock/bmw_connected_drive.py index c992bf1225a..52734b1259c 100644 --- a/homeassistant/components/lock/bmw_connected_drive.py +++ b/homeassistant/components/lock/bmw_connected_drive.py @@ -38,6 +38,7 @@ class BMWLock(LockDevice): self._vehicle = vehicle self._attribute = attribute self._name = '{} {}'.format(self._vehicle.name, self._attribute) + self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._state = None @@ -49,6 +50,11 @@ class BMWLock(LockDevice): """ return False + @property + def unique_id(self): + """Return the unique ID of the lock.""" + return self._unique_id + @property def name(self): """Return the name of the lock.""" diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index bd582da1ef4..ed75520c179 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -52,6 +52,7 @@ class BMWConnectedDriveSensor(Entity): self._state = None self._unit_of_measurement = None self._name = '{} {}'.format(self._vehicle.name, self._attribute) + self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._icon = icon @@ -60,6 +61,11 @@ class BMWConnectedDriveSensor(Entity): """Data update is triggered from BMWConnectedDriveEntity.""" return False + @property + def unique_id(self): + """Return the unique ID of the sensor.""" + return self._unique_id + @property def name(self) -> str: """Return the name of the sensor.""" @@ -86,7 +92,7 @@ class BMWConnectedDriveSensor(Entity): @property def device_state_attributes(self): - """Return the state attributes of the binary sensor.""" + """Return the state attributes of the sensor.""" return { 'car': self._vehicle.name } From c0180712188322142fdabc7d62c1e98065845237 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 15 Apr 2018 09:50:44 +0200 Subject: [PATCH 381/924] Revert "Update yweather.py" (#13900) * Revert "Add unique_id for BMW ConnectedDrive (#13888)" This reverts commit 9014e2684565066afadd25ea8dc980f63d01a8c4. * Revert "Added snips service descriptions (#13883)" This reverts commit 1c4da0c4a6c3146d4783e09614e151f4eb90cbe8. * Revert "Fix race condition for component loaded before listening (#13887)" This reverts commit bba997e484baaf9a415352a971f8ae91e99a6a27. * Revert "Missing property decorator added (#13889)" This reverts commit bf98b793c5cfb163f587047b24cfc55e041adbe9. * Revert "Update frontend to 20180414.0" This reverts commit 1617fbea4c4570bea02d7d94bbe314248a5a5a17. * Revert "Further untangle data entry flow (#13855)" This reverts commit 4d44c0feff03ddf1c0c57bd130cd1bdfa8c6621a. * Revert "add support for Kodi discovery (#13790)" This reverts commit 5a5dad689b3421522b8cfa473c111d155eb2ecb6. * Revert "Update yweather.py (#13851)" This reverts commit c3388d63a1929a6c197644d1ae1f622c4d58c793. --- homeassistant/components/weather/yweather.py | 31 ++++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 8e638895660..5987cf7621f 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -32,27 +32,20 @@ DEFAULT_NAME = 'Yweather' SCAN_INTERVAL = timedelta(minutes=10) CONDITION_CLASSES = { - 'clear-night': [31, 33], - 'sunny': [32, 34, 25, 36], - 'windy': [24], - 'fair': [34], - 'fair-night': [33], - 'cloudy': [26], - 'mostly-cloudy': [28], - 'mostly-cloudy-night': [27], - 'partly-cloudy': [30, 44], - 'partly-cloudy-night': [29], + 'clear-night': [31], + 'cloudy': [26, 27, 28, 29, 30], 'fog': [19, 20, 21, 22, 23], 'hail': [17, 18, 35], - 'light-rain': [8, 9], - 'light-snow': [14], - 'heavy-rain': [11, 12, 45, 40], - 'heavy-snow': [41, 42, 43, 46], - 'snowy': [13, 15, 16], - 'rainy': [10], - 'snowy-rainy': [5, 6, 7, 10], - 'lightning': [3, 4, 37, 38, 39], - 'lightning-rainy': [45, 47], + 'lightning': [37], + 'lightning-rainy': [3, 4, 38, 39, 47], + 'partlycloudy': [44], + 'pouring': [40, 45], + 'rainy': [9, 11, 12], + 'snowy': [8, 13, 14, 15, 16, 41, 42, 43], + 'snowy-rainy': [5, 6, 7, 10, 46], + 'sunny': [32, 33, 34, 25, 36], + 'windy': [24], + 'windy-variant': [], 'exceptional': [0, 1, 2], } From 390086bb7ef4988b9f67ef2c518ba022758d923b Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 15 Apr 2018 00:54:02 -0700 Subject: [PATCH 382/924] Eufy colour bulb updates (#13895) * Fix up Eufy handling of colour lights The Eufy colour lights have separate colour and temperature modes, and give much less light output when in colour mode. Brightness is also handled in a slightly confusing way, which means that state must be maintained in order to avoid switching the light between modes by accident. Add some additional handling for that. * Bump the lakeside version This version has important bugfixes for colour bulbs. * Hound fixes --- homeassistant/components/eufy.py | 2 +- homeassistant/components/light/eufy.py | 16 +++++++++++++--- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 53584be9fdc..733aa0adbfe 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.4'] +REQUIREMENTS = ['lakeside==0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py index fa6550d2682..a66e219c1a8 100644 --- a/homeassistant/components/light/eufy.py +++ b/homeassistant/components/light/eufy.py @@ -48,12 +48,14 @@ class EufyLight(Light): self._code = device['code'] self._type = device['type'] self._bulb = lakeside.bulb(self._address, self._code, self._type) + self._colormode = False if self._type == "T1011": self._features = SUPPORT_BRIGHTNESS elif self._type == "T1012": self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP elif self._type == "T1013": - self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR + self._features = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | \ + SUPPORT_COLOR self._bulb.connect() def update(self): @@ -62,9 +64,10 @@ class EufyLight(Light): self._brightness = self._bulb.brightness self._temp = self._bulb.temperature if self._bulb.colors: - self._hs = color_util.color_RGB_to_hsv(*self._bulb.colors) + self._colormode = True + self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) else: - self._hs = None + self._colormode = False self._state = self._bulb.power @property @@ -108,6 +111,8 @@ class EufyLight(Light): @property def hs_color(self): """Return the color of this light.""" + if not self._colormode: + return None return self._hs @property @@ -128,6 +133,7 @@ class EufyLight(Light): brightness = max(1, self._brightness) if colortemp is not None: + self._colormode = False temp_in_k = mired_to_kelvin(colortemp) relative_temp = temp_in_k - EUFY_MIN_KELVIN temp = int(relative_temp * 100 / @@ -138,6 +144,10 @@ class EufyLight(Light): if hs is not None: rgb = color_util.color_hsv_to_RGB( hs[0], hs[1], brightness / 255 * 100) + self._colormode = True + elif self._colormode: + rgb = color_util.color_hsv_to_RGB( + self._hs[0], self._hs[1], brightness / 255 * 100) else: rgb = None diff --git a/requirements_all.txt b/requirements_all.txt index d5036a795fe..b4056f61cd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -458,7 +458,7 @@ keyring==12.0.0 keyrings.alt==3.0 # homeassistant.components.eufy -lakeside==0.4 +lakeside==0.5 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From 2bff03836bb32ee5edd781c7e9391b64a7d2a158 Mon Sep 17 00:00:00 2001 From: Kyle Niewiada Date: Sun, 15 Apr 2018 07:59:10 -0400 Subject: [PATCH 383/924] Fix #13846 Double underscore in bluetooth address (#13884) --- homeassistant/components/device_tracker/bluetooth_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 807f6c0d0a4..2ca519d225c 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -40,7 +40,7 @@ def setup_scanner(hass, config, see, discovery_info=None): attributes = {} if rssi is not None: attributes['rssi'] = rssi - see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name, + see(mac="{}{}".format(BT_PREFIX, mac), host_name=name, attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH) def discover_devices(): From 2f26b0084f392aebda44c4ebf599fe7a906d1fc7 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 15 Apr 2018 15:19:28 +0200 Subject: [PATCH 384/924] Import operation modes from air humidifier (#13908) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 16affc08467..2acc3895f3e 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -708,7 +708,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): def __init__(self, name, device, model, unique_id): """Initialize the plug switch.""" - from miio.airpurifier import OperationMode + from miio.airhumidifier import OperationMode super().__init__(name, device, model, unique_id) From cd8935cbd245f505dbbb73f4b7ca578ae57c346b Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 15 Apr 2018 15:20:37 +0200 Subject: [PATCH 385/924] Fritzbox netmonitor name (#13903) * Addd name to netmonitor * import conf_name --- .../components/sensor/fritzbox_netmonitor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/fritzbox_netmonitor.py b/homeassistant/components/sensor/fritzbox_netmonitor.py index f4f774cad1e..857e6cc4a07 100644 --- a/homeassistant/components/sensor/fritzbox_netmonitor.py +++ b/homeassistant/components/sensor/fritzbox_netmonitor.py @@ -11,7 +11,7 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_HOST, STATE_UNAVAILABLE) +from homeassistant.const import (CONF_NAME, CONF_HOST, STATE_UNAVAILABLE) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -20,6 +20,7 @@ REQUIREMENTS = ['fritzconnection==0.6.5'] _LOGGER = logging.getLogger(__name__) +CONF_DEFAULT_NAME = 'fritz_netmonitor' CONF_DEFAULT_IP = '169.254.1.1' # This IP is valid for all FRITZ!Box routers. ATTR_BYTES_RECEIVED = 'bytes_received' @@ -42,6 +43,7 @@ STATE_OFFLINE = 'offline' ICON = 'mdi:web' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=CONF_DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=CONF_DEFAULT_IP): cv.string, }) @@ -52,6 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import fritzconnection as fc from fritzconnection.fritzconnection import FritzConnectionException + name = config.get(CONF_NAME) host = config.get(CONF_HOST) try: @@ -65,15 +68,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): else: _LOGGER.info("Successfully connected to FRITZ!Box") - add_devices([FritzboxMonitorSensor(fstatus)], True) + add_devices([FritzboxMonitorSensor(name, fstatus)], True) class FritzboxMonitorSensor(Entity): """Implementation of a fritzbox monitor sensor.""" - def __init__(self, fstatus): + def __init__(self, name, fstatus): """Initialize the sensor.""" - self._name = 'fritz_netmonitor' + self._name = name self._fstatus = fstatus self._state = STATE_UNAVAILABLE self._is_linked = self._is_connected = self._wan_access_type = None From c69f37500a9d14bf2e16f275cdc3d8d8e3a52ee7 Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Sun, 15 Apr 2018 15:25:30 +0200 Subject: [PATCH 386/924] Restore typeerror check for units sans energy tracking (#13824) --- homeassistant/components/switch/edimax.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/edimax.py b/homeassistant/components/switch/edimax.py index 49eb5d32110..40ebb54b603 100644 --- a/homeassistant/components/switch/edimax.py +++ b/homeassistant/components/switch/edimax.py @@ -84,12 +84,12 @@ class SmartPlugSwitch(SwitchDevice): """Update edimax switch.""" try: self._now_power = float(self.smartplug.now_power) - except ValueError: + except (TypeError, ValueError): self._now_power = None try: self._now_energy_day = float(self.smartplug.now_energy_day) - except ValueError: + except (TypeError, ValueError): self._now_energy_day = None self._state = self.smartplug.state == 'ON' From 9677bc081e46d631f5e6383da29711b46aaef261 Mon Sep 17 00:00:00 2001 From: Benedict Aas Date: Sun, 15 Apr 2018 17:51:45 +0100 Subject: [PATCH 387/924] Add more math functions to templates (#13915) We make `sin`, `cos`, `tan`, and `sqrt` functions, and the `pi`, `tau`, and `e` constants available in templates. --- homeassistant/helpers/template.py | 43 +++++++++++++++++++ tests/helpers/test_template.py | 68 +++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 353fda28875..3a24de6b39c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -452,6 +452,38 @@ def logarithm(value, base=math.e): return value +def sine(value): + """Filter to get sine of the value.""" + try: + return math.sin(float(value)) + except (ValueError, TypeError): + return value + + +def cosine(value): + """Filter to get cosine of the value.""" + try: + return math.cos(float(value)) + except (ValueError, TypeError): + return value + + +def tangent(value): + """Filter to get tangent of the value.""" + try: + return math.tan(float(value)) + except (ValueError, TypeError): + return value + + +def square_root(value): + """Filter to get square root of the value.""" + try: + return math.sqrt(float(value)) + except (ValueError, TypeError): + return value + + def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True): """Filter to convert given timestamp to format.""" try: @@ -571,6 +603,10 @@ ENV = TemplateEnvironment() ENV.filters['round'] = forgiving_round ENV.filters['multiply'] = multiply ENV.filters['log'] = logarithm +ENV.filters['sin'] = sine +ENV.filters['cos'] = cosine +ENV.filters['tan'] = tangent +ENV.filters['sqrt'] = square_root ENV.filters['timestamp_custom'] = timestamp_custom ENV.filters['timestamp_local'] = timestamp_local ENV.filters['timestamp_utc'] = timestamp_utc @@ -583,6 +619,13 @@ ENV.filters['regex_replace'] = regex_replace ENV.filters['regex_search'] = regex_search ENV.filters['regex_findall_index'] = regex_findall_index ENV.globals['log'] = logarithm +ENV.globals['sin'] = sine +ENV.globals['cos'] = cosine +ENV.globals['tan'] = tangent +ENV.globals['sqrt'] = square_root +ENV.globals['pi'] = math.pi +ENV.globals['tau'] = math.pi * 2 +ENV.globals['e'] = math.e ENV.globals['float'] = forgiving_float ENV.globals['now'] = dt_util.now ENV.globals['utcnow'] = dt_util.utcnow diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 650b98509d0..2dfcb2a58e5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -149,6 +149,74 @@ class TestHelpersTemplate(unittest.TestCase): '{{ log(%s, %s) | round(1) }}' % (value, base), self.hass).render()) + def test_sine(self): + """Test sine.""" + tests = [ + (0, '0.0'), + (math.pi / 2, '1.0'), + (math.pi, '0.0'), + (math.pi * 1.5, '-1.0'), + (math.pi / 10, '0.309') + ] + + for value, expected in tests: + self.assertEqual( + expected, + template.Template( + '{{ %s | sin | round(3) }}' % value, + self.hass).render()) + + def test_cos(self): + """Test cosine.""" + tests = [ + (0, '1.0'), + (math.pi / 2, '0.0'), + (math.pi, '-1.0'), + (math.pi * 1.5, '-0.0'), + (math.pi / 10, '0.951') + ] + + for value, expected in tests: + self.assertEqual( + expected, + template.Template( + '{{ %s | cos | round(3) }}' % value, + self.hass).render()) + + def test_tan(self): + """Test tangent.""" + tests = [ + (0, '0.0'), + (math.pi, '-0.0'), + (math.pi / 180 * 45, '1.0'), + (math.pi / 180 * 90, '1.633123935319537e+16'), + (math.pi / 180 * 135, '-1.0') + ] + + for value, expected in tests: + self.assertEqual( + expected, + template.Template( + '{{ %s | tan | round(3) }}' % value, + self.hass).render()) + + def test_sqrt(self): + """Test square root.""" + tests = [ + (0, '0.0'), + (1, '1.0'), + (2, '1.414'), + (10, '3.162'), + (100, '10.0'), + ] + + for value, expected in tests: + self.assertEqual( + expected, + template.Template( + '{{ %s | sqrt | round(3) }}' % value, + self.hass).render()) + def test_strptime(self): """Test the parse timestamp method.""" tests = [ From 517fb2e983d2e9060712989c9a10356801ee02ca Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 15 Apr 2018 22:19:15 +0200 Subject: [PATCH 388/924] Upgrade pyqwikswitch to 0.71 (#13920) --- homeassistant/components/qwikswitch.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 4d34ccca24a..3dc16f513dc 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.light import ATTR_BRIGHTNESS import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.7'] +REQUIREMENTS = ['pyqwikswitch==0.71'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b4056f61cd0..9772cf87a44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.7 +pyqwikswitch==0.71 # homeassistant.components.rainbird pyrainbird==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 901074bcad3..050038b034a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ pymonoprice==0.3 pynx584==0.4 # homeassistant.components.qwikswitch -pyqwikswitch==0.7 +pyqwikswitch==0.71 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky From 36a663adeb88664800ccffb3d676688861547af3 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Mon, 16 Apr 2018 08:20:58 +0200 Subject: [PATCH 389/924] Add extra attributes for device scanner, Nmap and Unifi (IP, SSID, etc.) (#13673) * Start of development * Add extra attributes from unifi scanner * Store IP of the device in the state attributes with nmap * Allow not defining get_extra_attributes method in derived classes --- .../components/device_tracker/__init__.py | 23 ++++++++++++++++++- .../components/device_tracker/nmap_tracker.py | 11 +++++++++ .../components/device_tracker/unifi.py | 6 +++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 45f0e51a214..b24f7784faf 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -605,6 +605,17 @@ class DeviceScanner(object): """ return self.hass.async_add_job(self.get_device_name, device) + def get_extra_attributes(self, device: str) -> dict: + """Get the extra attributes of a device.""" + raise NotImplementedError() + + def async_get_extra_attributes(self, device: str) -> Any: + """Get the extra attributes of a device. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.get_extra_attributes, device) + def load_config(path: str, hass: HomeAssistantType, consider_home: timedelta): """Load devices from YAML configuration file.""" @@ -690,10 +701,20 @@ def async_setup_scanner_platform(hass: HomeAssistantType, config: ConfigType, host_name = yield from scanner.async_get_device_name(mac) seen.add(mac) + try: + extra_attributes = (yield from + scanner.async_get_extra_attributes(mac)) + except NotImplementedError: + extra_attributes = dict() + kwargs = { 'mac': mac, 'host_name': host_name, - 'source_type': SOURCE_TYPE_ROUTER + 'source_type': SOURCE_TYPE_ROUTER, + 'attributes': { + 'scanner': scanner.__class__.__name__, + **extra_attributes + } } zone_home = hass.states.get(zone.ENTITY_ID_HOME) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index 23cb7ea8f9d..f62f53fe5fc 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -80,6 +80,8 @@ class NmapDeviceScanner(DeviceScanner): """Scan for new devices and return a list with found device IDs.""" self._update_info() + _LOGGER.debug("Nmap last results %s", self.last_results) + return [device.mac for device in self.last_results] def get_device_name(self, device): @@ -91,6 +93,15 @@ class NmapDeviceScanner(DeviceScanner): return filter_named[0] return None + def get_extra_attributes(self, device): + """Return the IP pf the given device.""" + filter_ip = [result.ip for result in self.last_results + if result.mac == device] + + if filter_ip: + return {'ip': filter_ip[0]} + return None + def _update_info(self): """Scan the network for devices. diff --git a/homeassistant/components/device_tracker/unifi.py b/homeassistant/components/device_tracker/unifi.py index d8a52aaaeb4..b7efe65dd01 100644 --- a/homeassistant/components/device_tracker/unifi.py +++ b/homeassistant/components/device_tracker/unifi.py @@ -122,3 +122,9 @@ class UnifiScanner(DeviceScanner): name = client.get('name') or client.get('hostname') _LOGGER.debug("Device mac %s name %s", device, name) return name + + def get_extra_attributes(self, device): + """Return the extra attributes of the device.""" + client = self._clients.get(device, {}) + _LOGGER.debug("Device mac %s attributes %s", device, client) + return client From 86709427b6025d4e5fc3165de0b068c15f666891 Mon Sep 17 00:00:00 2001 From: Marco Date: Mon, 16 Apr 2018 09:54:57 +0200 Subject: [PATCH 390/924] Fixed Capsman data not being used (#13917) --- homeassistant/components/device_tracker/mikrotik.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/mikrotik.py b/homeassistant/components/device_tracker/mikrotik.py index 154fc3d2a63..a6a67749f76 100644 --- a/homeassistant/components/device_tracker/mikrotik.py +++ b/homeassistant/components/device_tracker/mikrotik.py @@ -176,7 +176,7 @@ class MikrotikScanner(DeviceScanner): for device in device_names if device.get('mac-address')} - if self.wireless_exist: + if self.wireless_exist or self.capsman_exist: self.last_results = { device.get('mac-address'): mac_names.get(device.get('mac-address')) From ad212d8dd4153e02d26778cdfc36cebe97e0b9c3 Mon Sep 17 00:00:00 2001 From: Paxy Date: Mon, 16 Apr 2018 12:06:41 +0200 Subject: [PATCH 391/924] Broadlink Sensor - switch to connection-less mode (#13761) * Broadlink Sensor - switch to connection-less mode Solved the issue with broadlink sensor that occurs when short connection loss with RM2/3 is present on poor WiFi networks. * Update broadlink.py * Update broadlink.py * Update broadlink.py * Update broadlink.py * Update broadlink.py * Update broadlink.py * Update broadlink.py --- homeassistant/components/sensor/broadlink.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 044b77ebfe8..5182ba4530e 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -56,9 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) timeout = config.get(CONF_TIMEOUT) update_interval = config.get(CONF_UPDATE_INTERVAL) - broadlink_data = BroadlinkData(update_interval, host, mac_addr, timeout) - dev = [] for variable in config[CONF_MONITORED_CONDITIONS]: dev.append(BroadlinkSensor(name, broadlink_data, variable)) @@ -104,10 +102,11 @@ class BroadlinkData(object): def __init__(self, interval, ip_addr, mac_addr, timeout): """Initialize the data object.""" - import broadlink self.data = None - self._device = broadlink.a1((ip_addr, 80), mac_addr, None) - self._device.timeout = timeout + self.ip_addr = ip_addr + self.mac_addr = mac_addr + self.timeout = timeout + self._connect() self._schema = vol.Schema({ vol.Optional('temperature'): vol.Range(min=-50, max=150), vol.Optional('humidity'): vol.Range(min=0, max=100), @@ -119,6 +118,11 @@ class BroadlinkData(object): if not self._auth(): _LOGGER.warning("Failed to connect to device") + def _connect(self): + import broadlink + self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None) + self._device.timeout = self.timeout + def _update(self, retry=3): try: data = self._device.check_sensors_raw() @@ -140,5 +144,6 @@ class BroadlinkData(object): except socket.timeout: auth = False if not auth and retry > 0: + self._connect() return self._auth(retry-1) return auth From 595600dea5df4375e0bac5e25de8f683d3028c08 Mon Sep 17 00:00:00 2001 From: Lincoln Kirchoff Date: Mon, 16 Apr 2018 13:31:25 -0500 Subject: [PATCH 392/924] Add support for new platform: climate.modbus (#12224) * Added support for a new platform: climate.modbus * Made changes based on code review. * Made changes based on code review * Made changes that were recommended in the pull request review. * Fixed spacing line 144 * Added docstrings for the added helper functions. * Fixed set_temperature() function to use a variable local to the function for the target temp. * Fixed lint formatting error * Modified logic when checking the target temperature, as well as fixing the setup_platform function --- homeassistant/components/climate/modbus.py | 148 +++++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 homeassistant/components/climate/modbus.py diff --git a/homeassistant/components/climate/modbus.py b/homeassistant/components/climate/modbus.py new file mode 100644 index 00000000000..7d392e5a40f --- /dev/null +++ b/homeassistant/components/climate/modbus.py @@ -0,0 +1,148 @@ +""" +Platform for a Generic Modbus Thermostat. + +This uses a setpoint and process +value within the controller, so both the current temperature register and the +target temperature register need to be configured. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.modbus/ +""" +import logging +import struct + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_SLAVE, ATTR_TEMPERATURE) +from homeassistant.components.climate import ( + ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE) + +import homeassistant.components.modbus as modbus +import homeassistant.helpers.config_validation as cv + +DEPENDENCIES = ['modbus'] + +# Parameters not defined by homeassistant.const +CONF_TARGET_TEMP = 'target_temp_register' +CONF_CURRENT_TEMP = 'current_temp_register' +CONF_DATA_TYPE = 'data_type' +CONF_COUNT = 'data_count' +CONF_PRECISION = 'precision' + +DATA_TYPE_INT = 'int' +DATA_TYPE_UINT = 'uint' +DATA_TYPE_FLOAT = 'float' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_SLAVE): cv.positive_int, + vol.Required(CONF_TARGET_TEMP): cv.positive_int, + vol.Required(CONF_CURRENT_TEMP): cv.positive_int, + vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): + vol.In([DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT]), + vol.Optional(CONF_COUNT, default=2): cv.positive_int, + vol.Optional(CONF_PRECISION, default=1): cv.positive_int +}) + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Modbus Thermostat Platform.""" + name = config.get(CONF_NAME) + modbus_slave = config.get(CONF_SLAVE) + target_temp_register = config.get(CONF_TARGET_TEMP) + current_temp_register = config.get(CONF_CURRENT_TEMP) + data_type = config.get(CONF_DATA_TYPE) + count = config.get(CONF_COUNT) + precision = config.get(CONF_PRECISION) + + add_devices([ModbusThermostat(name, modbus_slave, + target_temp_register, current_temp_register, + data_type, count, precision)], True) + + +class ModbusThermostat(ClimateDevice): + """Representation of a Modbus Thermostat.""" + + def __init__(self, name, modbus_slave, target_temp_register, + current_temp_register, data_type, count, precision): + """Initialize the unit.""" + self._name = name + self._slave = modbus_slave + self._target_temperature_register = target_temp_register + self._current_temperature_register = current_temp_register + self._target_temperature = None + self._current_temperature = None + self._data_type = data_type + self._count = int(count) + self._precision = precision + self._structure = '>f' + + data_types = {DATA_TYPE_INT: {1: 'h', 2: 'i', 4: 'q'}, + DATA_TYPE_UINT: {1: 'H', 2: 'I', 4: 'Q'}, + DATA_TYPE_FLOAT: {1: 'e', 2: 'f', 4: 'd'}} + + self._structure = '>{}'.format(data_types[self._data_type] + [self._count]) + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + def update(self): + """Update Target & Current Temperature.""" + self._target_temperature = self.read_register( + self._target_temperature_register) + self._current_temperature = self.read_register( + self._current_temperature_register) + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temperature = kwargs.get(ATTR_TEMPERATURE) + if target_temperature is None: + return + byte_string = struct.pack(self._structure, target_temperature) + register_value = struct.unpack('>h', byte_string[0:2])[0] + + try: + self.write_register(self._target_temperature_register, + register_value) + except AttributeError as ex: + _LOGGER.error(ex) + + def read_register(self, register): + """Read holding register using the modbus hub slave.""" + try: + result = modbus.HUB.read_holding_registers(self._slave, register, + self._count) + except AttributeError as ex: + _LOGGER.error(ex) + byte_string = b''.join( + [x.to_bytes(2, byteorder='big') for x in result.registers]) + val = struct.unpack(self._structure, byte_string)[0] + register_value = format(val, '.{}f'.format(self._precision)) + return register_value + + def write_register(self, register, value): + """Write register using the modbus hub slave.""" + modbus.HUB.write_registers(self._slave, register, [value, 0]) From e0c5b44994772f82cd334dbcdd039269ae65fde7 Mon Sep 17 00:00:00 2001 From: Khole Date: Mon, 16 Apr 2018 20:00:13 +0100 Subject: [PATCH 393/924] Hive R3 update (#13357) * Rebase * Update version number to 0.2.14 * Remove Blank Line * Added period to docstring * Update Tox Fix * Removed Lines --- .../components/binary_sensor/hive.py | 8 +++++ homeassistant/components/climate/hive.py | 13 +++++++ homeassistant/components/hive.py | 6 +++- homeassistant/components/light/hive.py | 8 +++++ homeassistant/components/sensor/hive.py | 34 ++++++++++++++++--- homeassistant/components/switch/hive.py | 8 +++++ requirements_all.txt | 2 +- 7 files changed, 72 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/binary_sensor/hive.py b/homeassistant/components/binary_sensor/hive.py index 2d4cbd8d070..46dd1b193e8 100644 --- a/homeassistant/components/binary_sensor/hive.py +++ b/homeassistant/components/binary_sensor/hive.py @@ -32,6 +32,7 @@ class HiveBinarySensorEntity(BinarySensorDevice): self.device_type = hivedevice["HA_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession + self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) @@ -52,6 +53,11 @@ class HiveBinarySensorEntity(BinarySensorDevice): """Return the name of the binary sensor.""" return self.node_name + @property + def device_state_attributes(self): + """Show Device Attributes.""" + return self.attributes + @property def is_on(self): """Return true if the binary sensor is on.""" @@ -61,3 +67,5 @@ class HiveBinarySensorEntity(BinarySensorDevice): def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) + self.attributes = self.session.attributes.state_attributes( + self.node_id) diff --git a/homeassistant/components/climate/hive.py b/homeassistant/components/climate/hive.py index 760ef131049..eb3aecae3a1 100644 --- a/homeassistant/components/climate/hive.py +++ b/homeassistant/components/climate/hive.py @@ -38,7 +38,10 @@ class HiveClimateEntity(ClimateDevice): self.node_id = hivedevice["Hive_NodeID"] self.node_name = hivedevice["Hive_NodeName"] self.device_type = hivedevice["HA_DeviceType"] + if self.device_type == "Heating": + self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession + self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) @@ -71,6 +74,11 @@ class HiveClimateEntity(ClimateDevice): friendly_name = "Hot Water" return friendly_name + @property + def device_state_attributes(self): + """Show Device Attributes.""" + return self.attributes + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -175,4 +183,9 @@ class HiveClimateEntity(ClimateDevice): def update(self): """Update all Node data from Hive.""" + node = self.node_id + if self.device_type == "Heating": + node = self.thermostat_node_id + self.session.core.update_data(self.node_id) + self.attributes = self.session.attributes.state_attributes(node) diff --git a/homeassistant/components/hive.py b/homeassistant/components/hive.py index abe52ebe98a..aa662fc2fb6 100644 --- a/homeassistant/components/hive.py +++ b/homeassistant/components/hive.py @@ -12,7 +12,7 @@ from homeassistant.const import (CONF_PASSWORD, CONF_SCAN_INTERVAL, import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -REQUIREMENTS = ['pyhiveapi==0.2.11'] +REQUIREMENTS = ['pyhiveapi==0.2.14'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'hive' @@ -44,6 +44,8 @@ class HiveSession: light = None sensor = None switch = None + weather = None + attributes = None def setup(hass, config): @@ -70,6 +72,8 @@ def setup(hass, config): session.hotwater = Pyhiveapi.Hotwater() session.light = Pyhiveapi.Light() session.switch = Pyhiveapi.Switch() + session.weather = Pyhiveapi.Weather() + session.attributes = Pyhiveapi.Attributes() hass.data[DATA_HIVE] = session for ha_type, hive_type in DEVICETYPES.items(): diff --git a/homeassistant/components/light/hive.py b/homeassistant/components/light/hive.py index c4ecc5a9d2c..1fd9e8aaaca 100644 --- a/homeassistant/components/light/hive.py +++ b/homeassistant/components/light/hive.py @@ -34,6 +34,7 @@ class HiveDeviceLight(Light): self.device_type = hivedevice["HA_DeviceType"] self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.session = hivesession + self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) self.session.entities.append(self) @@ -48,6 +49,11 @@ class HiveDeviceLight(Light): """Return the display name of this light.""" return self.node_name + @property + def device_state_attributes(self): + """Show Device Attributes.""" + return self.attributes + @property def brightness(self): """Brightness of the light (an integer in the range 1-255).""" @@ -136,3 +142,5 @@ class HiveDeviceLight(Light): def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) + self.attributes = self.session.attributes.state_attributes( + self.node_id) diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index cae2eaf7437..8f8ce2d1681 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -4,11 +4,17 @@ Support for the Hive devices. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.hive/ """ +from homeassistant.const import TEMP_CELSIUS from homeassistant.components.hive import DATA_HIVE from homeassistant.helpers.entity import Entity DEPENDENCIES = ['hive'] +FRIENDLY_NAMES = {'Hub_OnlineStatus': 'Hub Status', + 'Hive_OutsideTemperature': 'Outside Temperature'} +DEVICETYPE_ICONS = {'Hub_OnlineStatus': 'mdi:switch', + 'Hive_OutsideTemperature': 'mdi:thermometer'} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up Hive sensor devices.""" @@ -16,7 +22,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): return session = hass.data.get(DATA_HIVE) - if discovery_info["HA_DeviceType"] == "Hub_OnlineStatus": + if (discovery_info["HA_DeviceType"] == "Hub_OnlineStatus" or + discovery_info["HA_DeviceType"] == "Hive_OutsideTemperature"): add_devices([HiveSensorEntity(session, discovery_info)]) @@ -27,6 +34,7 @@ class HiveSensorEntity(Entity): """Initialize the sensor.""" self.node_id = hivedevice["Hive_NodeID"] self.device_type = hivedevice["HA_DeviceType"] + self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) @@ -40,13 +48,29 @@ class HiveSensorEntity(Entity): @property def name(self): """Return the name of the sensor.""" - return "Hive hub status" + return FRIENDLY_NAMES.get(self.device_type) @property def state(self): """Return the state of the sensor.""" - return self.session.sensor.hub_online_status(self.node_id) + if self.device_type == "Hub_OnlineStatus": + return self.session.sensor.hub_online_status(self.node_id) + elif self.device_type == "Hive_OutsideTemperature": + return self.session.weather.temperature() + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self.device_type == "Hive_OutsideTemperature": + return TEMP_CELSIUS + + @property + def icon(self): + """Return the icon to use.""" + return DEVICETYPE_ICONS.get(self.device_type) def update(self): - """Update all Node data from Hive.""" - self.session.core.update_data(self.node_id) + """Update all Node data frome Hive.""" + if self.session.core.update_data(self.node_id): + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) diff --git a/homeassistant/components/switch/hive.py b/homeassistant/components/switch/hive.py index 67ebe95ba8e..49fc9696b5e 100644 --- a/homeassistant/components/switch/hive.py +++ b/homeassistant/components/switch/hive.py @@ -28,6 +28,7 @@ class HiveDevicePlug(SwitchDevice): self.node_name = hivedevice["Hive_NodeName"] self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession + self.attributes = {} self.data_updatesource = '{}.{}'.format(self.device_type, self.node_id) self.session.entities.append(self) @@ -42,6 +43,11 @@ class HiveDevicePlug(SwitchDevice): """Return the name of this Switch device if any.""" return self.node_name + @property + def device_state_attributes(self): + """Show Device Attributes.""" + return self.attributes + @property def current_power_w(self): """Return the current power usage in W.""" @@ -67,3 +73,5 @@ class HiveDevicePlug(SwitchDevice): def update(self): """Update all Node data from Hive.""" self.session.core.update_data(self.node_id) + self.attributes = self.session.attributes.state_attributes( + self.node_id) diff --git a/requirements_all.txt b/requirements_all.txt index 9772cf87a44..fe82942cab8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -777,7 +777,7 @@ pyharmony==1.0.20 pyhik==0.1.8 # homeassistant.components.hive -pyhiveapi==0.2.11 +pyhiveapi==0.2.14 # homeassistant.components.homematic pyhomematic==0.1.41 From acdba7a27c0a00297a1fa56535ce6f165d98832a Mon Sep 17 00:00:00 2001 From: Fabien Piuzzi Date: Mon, 16 Apr 2018 21:35:24 +0200 Subject: [PATCH 394/924] Updated foobot_async package version (#13942) Fix #13886 --- homeassistant/components/sensor/foobot.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/foobot.py b/homeassistant/components/sensor/foobot.py index 8f65a335872..d247a90e93a 100644 --- a/homeassistant/components/sensor/foobot.py +++ b/homeassistant/components/sensor/foobot.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['foobot_async==0.3.0'] +REQUIREMENTS = ['foobot_async==0.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fe82942cab8..2820d3036a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -311,7 +311,7 @@ fixerio==0.1.1 flux_led==0.21 # homeassistant.components.sensor.foobot -foobot_async==0.3.0 +foobot_async==0.3.1 # homeassistant.components.notify.free_mobile freesms==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 050038b034a..4b9470a9b29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -63,7 +63,7 @@ evohomeclient==0.2.5 feedparser==5.2.1 # homeassistant.components.sensor.foobot -foobot_async==0.3.0 +foobot_async==0.3.1 # homeassistant.components.tts.google gTTS-token==1.1.1 From 9da239178c0343f78153321e7f6b5bef571ad983 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Mon, 16 Apr 2018 20:52:56 -0400 Subject: [PATCH 395/924] Update pyhydroquebec to 2.2.2 (#13946) --- homeassistant/components/sensor/hydroquebec.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 9129ee17d80..2195153ab1e 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyhydroquebec==2.2.1'] +REQUIREMENTS = ['pyhydroquebec==2.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2820d3036a4..bb174ae74ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,7 +783,7 @@ pyhiveapi==0.2.14 pyhomematic==0.1.41 # homeassistant.components.sensor.hydroquebec -pyhydroquebec==2.2.1 +pyhydroquebec==2.2.2 # homeassistant.components.alarm_control_panel.ialarm pyialarm==0.2 From e8ad36feb66b3f1e8f4f3d67246d884b22899686 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Apr 2018 04:16:12 +0200 Subject: [PATCH 396/924] Upgrade alpha_vantage to 2.0.0 (#13943) --- homeassistant/components/sensor/alpha_vantage.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/alpha_vantage.py b/homeassistant/components/sensor/alpha_vantage.py index 896497a93d5..77d8ba9322f 100644 --- a/homeassistant/components/sensor/alpha_vantage.py +++ b/homeassistant/components/sensor/alpha_vantage.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['alpha_vantage==1.9.0'] +REQUIREMENTS = ['alpha_vantage==2.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bb174ae74ca..288ccc50c5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -98,7 +98,7 @@ aiopvapi==1.5.4 alarmdecoder==1.13.2 # homeassistant.components.sensor.alpha_vantage -alpha_vantage==1.9.0 +alpha_vantage==2.0.0 # homeassistant.components.amcrest amcrest==1.2.2 From d0d61d1b5f42b3e4808f3ef90572e38d0142aeeb Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Mon, 16 Apr 2018 22:16:28 -0400 Subject: [PATCH 397/924] Update pyfido to 2.1.1 (#13947) --- homeassistant/components/sensor/fido.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 25a104bf259..a2ee18b3659 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyfido==2.1.0'] +REQUIREMENTS = ['pyfido==2.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 288ccc50c5b..576c94135ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -759,7 +759,7 @@ pyenvisalink==2.2 pyephember==0.1.1 # homeassistant.components.sensor.fido -pyfido==2.1.0 +pyfido==2.1.1 # homeassistant.components.climate.flexit pyflexit==0.3 From 8fdeebc50d225227ab61a38f562324a4ee91ad0e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 17 Apr 2018 03:21:39 +0100 Subject: [PATCH 398/924] Cleanup on exit (#13918) * Cleanup on exit * lint * version bump * pymediaroom version bump * address @kellerza comment * avoid None in the _name --- .../components/media_player/mediaroom.py | 22 ++++++++++++++----- requirements_all.txt | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/media_player/mediaroom.py b/homeassistant/components/media_player/mediaroom.py index a6d5841bb0f..f5b7567aa34 100644 --- a/homeassistant/components/media_player/mediaroom.py +++ b/homeassistant/components/media_player/mediaroom.py @@ -8,6 +8,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.media_player import ( MEDIA_TYPE_CHANNEL, SUPPORT_PAUSE, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_STOP, PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, @@ -20,11 +21,11 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_OPTIMISTIC, STATE_OFF, CONF_TIMEOUT, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, - STATE_UNAVAILABLE + STATE_UNAVAILABLE, EVENT_HOMEASSISTANT_STOP ) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pymediaroom==0.6'] +REQUIREMENTS = ['pymediaroom==0.6.3'] _LOGGER = logging.getLogger(__name__) @@ -81,12 +82,21 @@ async def async_setup_platform(hass, config, async_add_devices, if not config[CONF_OPTIMISTIC]: from pymediaroom import install_mediaroom_protocol - already_installed = hass.data.get(DISCOVERY_MEDIAROOM, False) + already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None) if not already_installed: - await install_mediaroom_protocol( + hass.data[DISCOVERY_MEDIAROOM] = await install_mediaroom_protocol( responses_callback=callback_notify) + + @callback + def stop_discovery(event): + """Stop discovery of new mediaroom STB's.""" + _LOGGER.debug("Stopping internal pymediaroom discovery.") + hass.data[DISCOVERY_MEDIAROOM].close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, + stop_discovery) + _LOGGER.debug("Auto discovery installed") - hass.data[DISCOVERY_MEDIAROOM] = True class MediaroomDevice(MediaPlayerDevice): @@ -120,7 +130,7 @@ class MediaroomDevice(MediaPlayerDevice): self._channel = None self._optimistic = optimistic self._state = STATE_PLAYING if optimistic else STATE_STANDBY - self._name = 'Mediaroom {}'.format(device_id) + self._name = 'Mediaroom {}'.format(device_id if device_id else host) self._available = True if device_id: self._unique_id = device_id diff --git a/requirements_all.txt b/requirements_all.txt index 576c94135ee..f97f625e3f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -832,7 +832,7 @@ pylutron==0.1.0 pymailgunner==1.4 # homeassistant.components.media_player.mediaroom -pymediaroom==0.6 +pymediaroom==0.6.3 # homeassistant.components.media_player.xiaomi_tv pymitv==1.0.0 From 6e9669c18d1235c4e70ae481d057c73f5e0b1f46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Apr 2018 23:24:20 -0400 Subject: [PATCH 399/924] Upgrade somecomfort to 0.5.2 (#13940) --- homeassistant/components/climate/honeywell.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 20d93e3116a..11a507aded2 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_REGION) -REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0'] +REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f97f625e3f6..b665f8e9b00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1191,7 +1191,7 @@ smappy==0.2.15 snapcast==2.0.8 # homeassistant.components.climate.honeywell -somecomfort==0.5.0 +somecomfort==0.5.2 # homeassistant.components.sensor.speedtest speedtest-cli==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b9470a9b29..b7ce54c64c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -180,7 +180,7 @@ rxv==0.5.1 sleepyq==0.6 # homeassistant.components.climate.honeywell -somecomfort==0.5.0 +somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator From 534aa0e4b54b992ac55de0ae576276e94089ce49 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Apr 2018 05:44:32 -0400 Subject: [PATCH 400/924] Add data entry flow helper (#13935) * Extract data entry flows HTTP views into helper * Remove use of domain * Lint * Fix tests * Update doc --- .../components/config/config_entries.py | 80 ++----------- homeassistant/config_entries.py | 4 +- homeassistant/data_entry_flow.py | 10 +- homeassistant/helpers/data_entry_flow.py | 106 ++++++++++++++++++ .../components/config/test_config_entries.py | 16 ++- 5 files changed, 132 insertions(+), 84 deletions(-) create mode 100644 homeassistant/helpers/data_entry_flow.py diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 967317134c2..d2aa918eda2 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,11 +1,10 @@ """Http views to control the config manager.""" import asyncio -import voluptuous as vol - from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, FlowManagerResourceView) REQUIREMENTS = ['voluptuous-serialize==1'] @@ -16,8 +15,10 @@ def async_setup(hass): """Enable the Home Assistant views.""" hass.http.register_view(ConfigManagerEntryIndexView) hass.http.register_view(ConfigManagerEntryResourceView) - hass.http.register_view(ConfigManagerFlowIndexView) - hass.http.register_view(ConfigManagerFlowResourceView) + hass.http.register_view( + ConfigManagerFlowIndexView(hass.config_entries.flow)) + hass.http.register_view( + ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) return True @@ -78,7 +79,7 @@ class ConfigManagerEntryResourceView(HomeAssistantView): return self.json(result) -class ConfigManagerFlowIndexView(HomeAssistantView): +class ConfigManagerFlowIndexView(FlowManagerIndexView): """View to create config flows.""" url = '/api/config/config_entries/flow' @@ -97,78 +98,13 @@ class ConfigManagerFlowIndexView(HomeAssistantView): flw for flw in hass.config_entries.flow.async_progress() if flw['source'] != data_entry_flow.SOURCE_USER]) - @RequestDataValidator(vol.Schema({ - vol.Required('domain'): str, - })) - @asyncio.coroutine - def post(self, request, data): - """Handle a POST request.""" - hass = request.app['hass'] - try: - result = yield from hass.config_entries.flow.async_init( - data['domain']) - except data_entry_flow.UnknownHandler: - return self.json_message('Invalid handler specified', 404) - except data_entry_flow.UnknownStep: - return self.json_message('Handler does not support init', 400) - - result = _prepare_json(result) - - return self.json(result) - - -class ConfigManagerFlowResourceView(HomeAssistantView): +class ConfigManagerFlowResourceView(FlowManagerResourceView): """View to interact with the flow manager.""" url = '/api/config/config_entries/flow/{flow_id}' name = 'api:config:config_entries:flow:resource' - @asyncio.coroutine - def get(self, request, flow_id): - """Get the current state of a data_entry_flow.""" - hass = request.app['hass'] - - try: - result = yield from hass.config_entries.flow.async_configure( - flow_id) - except data_entry_flow.UnknownFlow: - return self.json_message('Invalid flow specified', 404) - - result = _prepare_json(result) - - return self.json(result) - - @RequestDataValidator(vol.Schema(dict), allow_empty=True) - @asyncio.coroutine - def post(self, request, flow_id, data): - """Handle a POST request.""" - hass = request.app['hass'] - - try: - result = yield from hass.config_entries.flow.async_configure( - flow_id, data) - except data_entry_flow.UnknownFlow: - return self.json_message('Invalid flow specified', 404) - except vol.Invalid: - return self.json_message('User input malformed', 400) - - result = _prepare_json(result) - - return self.json(result) - - @asyncio.coroutine - def delete(self, request, flow_id): - """Cancel a flow in progress.""" - hass = request.app['hass'] - - try: - hass.config_entries.flow.async_abort(flow_id) - except data_entry_flow.UnknownFlow: - return self.json_message('Invalid flow specified', 404) - - return self.json_message('Flow aborted') - class ConfigManagerAvailableFlowView(HomeAssistantView): """View to query available flows.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e143c94197e..46bb2f7bfe2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -338,7 +338,7 @@ class ConfigEntries: if component not in self.hass.config.components: return True - await entry.async_unload( + return await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) async def _async_save_entry(self, result): @@ -362,6 +362,8 @@ class ConfigEntries: await async_setup_component( self.hass, entry.domain, self._hass_config) + return entry + async def _async_create_flow(self, handler): """Create a flow for specified handler. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 361b6653cfd..cadec3f3d69 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -34,12 +34,12 @@ class UnknownStep(FlowError): class FlowManager: """Manage all the flows that are in progress.""" - def __init__(self, hass, async_create_flow, async_save_entry): + def __init__(self, hass, async_create_flow, async_finish_flow): """Initialize the flow manager.""" self.hass = hass self._progress = {} self._async_create_flow = async_create_flow - self._async_save_entry = async_save_entry + self._async_finish_flow = async_finish_flow @callback def async_progress(self): @@ -113,10 +113,8 @@ class FlowManager: if result['type'] == RESULT_TYPE_ABORT: return result - # We pass a copy of the result because we're going to mutate our - # version afterwards and don't want to cause unexpected bugs. - await self._async_save_entry(dict(result)) - result.pop('data') + # We pass a copy of the result because we're mutating our version + result['result'] = await self._async_finish_flow(dict(result)) return result diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py new file mode 100644 index 00000000000..a8aca2fd2e9 --- /dev/null +++ b/homeassistant/helpers/data_entry_flow.py @@ -0,0 +1,106 @@ +"""Helpers for the data entry flow.""" + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator + + +def _prepare_json(result): + """Convert result for JSON.""" + if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + data.pop('result') + data.pop('data') + return data + + elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: + return result + + import voluptuous_serialize + + data = result.copy() + + schema = data['data_schema'] + if schema is None: + data['data_schema'] = [] + else: + data['data_schema'] = voluptuous_serialize.convert(schema) + + return data + + +class FlowManagerIndexView(HomeAssistantView): + """View to create config flows.""" + + def __init__(self, flow_mgr): + """Initialize the flow manager index view.""" + self._flow_mgr = flow_mgr + + async def get(self, request): + """List flows that are in progress.""" + return self.json(self._flow_mgr.async_progress()) + + @RequestDataValidator(vol.Schema({ + vol.Required('handler'): vol.Any(str, list), + })) + async def post(self, request, data): + """Handle a POST request.""" + if isinstance(data['handler'], list): + handler = tuple(data['handler']) + else: + handler = data['handler'] + + try: + result = await self._flow_mgr.async_init(handler) + except data_entry_flow.UnknownHandler: + return self.json_message('Invalid handler specified', 404) + except data_entry_flow.UnknownStep: + return self.json_message('Handler does not support init', 400) + + result = _prepare_json(result) + + return self.json(result) + + +class FlowManagerResourceView(HomeAssistantView): + """View to interact with the flow manager.""" + + def __init__(self, flow_mgr): + """Initialize the flow manager resource view.""" + self._flow_mgr = flow_mgr + + async def get(self, request, flow_id): + """Get the current state of a data_entry_flow.""" + try: + result = await self._flow_mgr.async_configure(flow_id) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + + result = _prepare_json(result) + + return self.json(result) + + @RequestDataValidator(vol.Schema(dict), allow_empty=True) + async def post(self, request, flow_id, data): + """Handle a POST request.""" + try: + result = await self._flow_mgr.async_configure(flow_id, data) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + except vol.Invalid: + return self.json_message('User input malformed', 400) + + result = _prepare_json(result) + + return self.json(result) + + async def delete(self, request, flow_id): + """Cancel a flow in progress.""" + try: + self._flow_mgr.async_abort(flow_id) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + + return self.json_message('Flow aborted') diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 70cb6c3fbaa..f53be8818a3 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,6 +17,12 @@ from homeassistant.loader import set_component from tests.common import MockConfigEntry, MockModule, mock_coro_func +@pytest.fixture(scope='session', autouse=True) +def mock_test_component(): + """Ensure a component called 'test' exists.""" + set_component('test', MockModule('test')) + + @pytest.fixture def client(hass, aiohttp_client): """Fixture that can interact with the config manager API.""" @@ -111,7 +117,7 @@ def test_initialize_flow(hass, client): with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', - json={'domain': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() @@ -150,7 +156,7 @@ def test_abort(hass, client): with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', - json={'domain': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() @@ -180,7 +186,7 @@ def test_create_account(hass, client): with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', - json={'domain': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() @@ -220,7 +226,7 @@ def test_two_step_flow(hass, client): with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', - json={'domain': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() flow_id = data.pop('flow_id') @@ -305,7 +311,7 @@ def test_get_progress_flow(hass, client): with patch.dict(HANDLERS, {'test': TestFlow}): resp = yield from client.post('/api/config/config_entries/flow', - json={'domain': 'test'}) + json={'handler': 'test'}) assert resp.status == 200 data = yield from resp.json() From add0afe31a3dce9663999e575119c84ab3611c28 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 17 Apr 2018 11:45:19 +0200 Subject: [PATCH 401/924] Xiaomi MiIO Device Tracker: Unused variable removed (#13948) * Unused variable removed and pinning added to be in sync with all xiaomi_miio components * requirements_all.txt updated --- homeassistant/components/device_tracker/xiaomi_miio.py | 6 +++--- requirements_all.txt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/xiaomi_miio.py b/homeassistant/components/device_tracker/xiaomi_miio.py index 61568892388..c5769253657 100644 --- a/homeassistant/components/device_tracker/xiaomi_miio.py +++ b/homeassistant/components/device_tracker/xiaomi_miio.py @@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), }) -REQUIREMENTS = ['python-miio==0.3.9'] +REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] def get_scanner(hass, config): @@ -41,7 +41,7 @@ def get_scanner(hass, config): device_info.model, device_info.firmware_version, device_info.hardware_version) - scanner = XiaomiMiioDeviceScanner(hass, device) + scanner = XiaomiMiioDeviceScanner(device) except DeviceException as ex: _LOGGER.error("Device unavailable or token incorrect: %s", ex) @@ -51,7 +51,7 @@ def get_scanner(hass, config): class XiaomiMiioDeviceScanner(DeviceScanner): """This class queries a Xiaomi Mi WiFi Repeater.""" - def __init__(self, hass, device): + def __init__(self, device): """Initialize the scanner.""" self.device = device diff --git a/requirements_all.txt b/requirements_all.txt index b665f8e9b00..12d3a8a0e86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -203,6 +203,7 @@ colorlog==3.1.2 concord232==0.15 # homeassistant.components.climate.eq3btsmart +# homeassistant.components.device_tracker.xiaomi_miio # homeassistant.components.fan.xiaomi_miio # homeassistant.components.light.xiaomi_miio # homeassistant.components.remote.xiaomi_miio From 998d8c177110ae8dc33338f51a269f0e6daa41f4 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Tue, 17 Apr 2018 11:50:26 +0200 Subject: [PATCH 402/924] Implement play media to set a channel based on (by priority): (#13934) - exact channel number - exact channel name - similar channel name temp --- .../components/media_player/webostv.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index ae9d259a47c..d7682a611b9 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -344,6 +344,42 @@ class LgWebOSDevice(MediaPlayerDevice): self._current_source = source_dict['label'] self._client.set_input(source_dict['id']) + def play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + _LOGGER.debug( + "Call play media type <%s>, Id <%s>", media_type, media_id) + + if media_type == MEDIA_TYPE_CHANNEL: + _LOGGER.debug("Searching channel...") + partial_match_channel_id = None + + for channel in self._client.get_channels(): + _LOGGER.debug( + "Checking channel number <%s>, name <%s>, id <%s>...", + channel['channelNumber'], + channel['channelName'], + channel['channelId']) + if media_id == channel['channelNumber']: + _LOGGER.debug( + "Perfect match on channel number: switching!") + self._client.set_channel(channel['channelId']) + return + elif media_id.lower() == channel['channelName'].lower(): + _LOGGER.debug( + "Perfect match on channel name: switching!") + self._client.set_channel(channel['channelId']) + return + elif media_id.lower() in channel['channelName'].lower(): + _LOGGER.debug( + "Partial match on channel name: saving it...") + partial_match_channel_id = channel['channelId'] + + if partial_match_channel_id is not None: + _LOGGER.debug( + "Using partial match on channel name: switching!") + self._client.set_channel(partial_match_channel_id) + return + def media_play(self): """Send play command.""" self._playing = True From f2d4dd25f04ed7dce632bd35a55fcefd4246082a Mon Sep 17 00:00:00 2001 From: karlkar Date: Tue, 17 Apr 2018 11:55:35 +0200 Subject: [PATCH 403/924] Update of python-mpd2 (#13921) --- homeassistant/components/media_player/mpd.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/mpd.py b/homeassistant/components/media_player/mpd.py index 81a18ab93c5..04dd1ac5f2e 100644 --- a/homeassistant/components/media_player/mpd.py +++ b/homeassistant/components/media_player/mpd.py @@ -23,7 +23,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -REQUIREMENTS = ['python-mpd2==0.5.5'] +REQUIREMENTS = ['python-mpd2==1.0.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 12d3a8a0e86..c2ee5814d7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -981,7 +981,7 @@ python-juicenet==0.0.5 python-miio==0.3.9 # homeassistant.components.media_player.mpd -python-mpd2==0.5.5 +python-mpd2==1.0.0 # homeassistant.components.light.mystrom # homeassistant.components.switch.mystrom From 9487bd455a3aa3bc4a4d72e5486e1360e7f5a651 Mon Sep 17 00:00:00 2001 From: Heiko Thiery Date: Tue, 17 Apr 2018 12:40:36 +0200 Subject: [PATCH 404/924] Add AVM fritzbox smarthome component (#10688) * initial commit Signed-off-by: Heiko Thiery * fix failed flake8 tests Signed-off-by: Heiko Thiery * add fritzhome files to .coveragerc Signed-off-by: Heiko Thiery * fix wrong module import Signed-off-by: Heiko Thiery * remove too general exception Signed-off-by: Heiko Thiery * incorporate review comments Signed-off-by: Heiko Thiery * remove blank line Signed-off-by: Heiko Thiery * fix wrong import Signed-off-by: Heiko Thiery * fix issue with operations Signed-off-by: Heiko Thiery * incorporate review comments Signed-off-by: Heiko Thiery * remove unused attributes Signed-off-by: Heiko Thiery * adapt to supported_features Signed-off-by: Heiko Thiery * change checking of kwargs to canonical way Signed-off-by: Heiko Thiery * remove unused self._state Signed-off-by: Heiko Thiery * Don't overwrite the platform domain Signed-off-by: Heiko Thiery * Remove parenthesis from import without line break Signed-off-by: Heiko Thiery * Do not pass hass to the components on init Signed-off-by: Heiko Thiery * Remove check for available in current_operation Signed-off-by: Heiko Thiery * Remove redundant logging message Signed-off-by: Heiko Thiery * Add blank line between standard and hass imports Signed-off-by: Heiko Thiery * Use states from base climate component Also add the new state STATE_MANUAL to the base. Signed-off-by: Heiko Thiery * add reconnect when access failed Signed-off-by: Heiko Thiery * add device specific attributes Signed-off-by: Heiko Thiery * group the imports from the same module Signed-off-by: Heiko Thiery * change domain data to fritz instance This let us use the fritz instance to reconnect from platform without accessing protected attributes. Signed-off-by: Heiko Thiery * fix typo Signed-off-by: Heiko Thiery * rename platform from fritzhome to fritzbox Signed-off-by: Heiko Thiery * Add device_state_attributes Add attributes to have compatiblity to fritzdect. Signed-off-by: Heiko Thiery * add support for multiple fritzboxes Signed-off-by: Heiko Thiery * fix pylint issues Signed-off-by: Heiko Thiery * fixed pyfritzhome version Signed-off-by: Heiko Thiery * fix import Signed-off-by: Heiko Thiery * fix component name in requirements_all.txt Signed-off-by: Heiko Thiery * upgrade pyfritzhome to 0.3.7 Signed-off-by: Heiko Thiery * rename platform/component also in .coveragerc Signed-off-by: Heiko Thiery * use DEFAULT_HOST when no host is in dict Signed-off-by: Heiko Thiery * add config schema for dict Signed-off-by: Heiko Thiery * remove check The check since since the config scheme takes case. Signed-off-by: Heiko Thiery * add check for empty devices Signed-off-by: Heiko Thiery * use standard attribute from base class Signed-off-by: Heiko Thiery * remove STATE_MANUAL from operation list Signed-off-by: Heiko Thiery * remove set DEFAULT_HOST Signed-off-by: Heiko Thiery * don't pass hass to the SwitchDevice Signed-off-by: Heiko Thiery * remove unsed DEFAULT_HOST Signed-off-by: Heiko Thiery * refactored device attributes Signed-off-by: Heiko Thiery * add info output if no fritzbox is configured Signed-off-by: Heiko Thiery * small fixes according review comment Signed-off-by: Heiko Thiery * remove unneeded default value Signed-off-by: Heiko Thiery * remove non required code from try..except block Signed-off-by: Heiko Thiery * line break for line that is too long Signed-off-by: Heiko Thiery * remove too many empty lines Signed-off-by: Heiko Thiery --- .coveragerc | 3 + homeassistant/components/climate/__init__.py | 1 + homeassistant/components/climate/fritzbox.py | 153 +++++++++++++++++++ homeassistant/components/fritzbox.py | 83 ++++++++++ homeassistant/components/switch/fritzbox.py | 104 +++++++++++++ requirements_all.txt | 3 + 6 files changed, 347 insertions(+) create mode 100755 homeassistant/components/climate/fritzbox.py create mode 100755 homeassistant/components/fritzbox.py create mode 100755 homeassistant/components/switch/fritzbox.py diff --git a/.coveragerc b/.coveragerc index 70cfbded98f..3da28762df0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,9 @@ omit = homeassistant/components/envisalink.py homeassistant/components/*/envisalink.py + homeassistant/components/fritzbox.py + homeassistant/components/*/fritzbox.py + homeassistant/components/eufy.py homeassistant/components/*/eufy.py diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 7ea23f4fd65..550d4035ddd 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -40,6 +40,7 @@ STATE_HEAT = 'heat' STATE_COOL = 'cool' STATE_IDLE = 'idle' STATE_AUTO = 'auto' +STATE_MANUAL = 'manual' STATE_DRY = 'dry' STATE_FAN_ONLY = 'fan_only' STATE_ECO = 'eco' diff --git a/homeassistant/components/climate/fritzbox.py b/homeassistant/components/climate/fritzbox.py new file mode 100755 index 00000000000..839da8c9d53 --- /dev/null +++ b/homeassistant/components/climate/fritzbox.py @@ -0,0 +1,153 @@ +""" +Support for AVM Fritz!Box smarthome thermostate devices. + +For more details about this component, please refer to the documentation at +http://home-assistant.io/components/climate.fritzbox/ +""" +import logging + +import requests + +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN +from homeassistant.components.fritzbox import ( + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_BATTERY_LOW, ATTR_STATE_LOCKED) +from homeassistant.components.climate import ( + ATTR_OPERATION_MODE, ClimateDevice, STATE_ECO, STATE_HEAT, STATE_MANUAL, + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.const import ( + ATTR_TEMPERATURE, PRECISION_HALVES, TEMP_CELSIUS) + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE) + +OPERATION_LIST = [STATE_HEAT, STATE_ECO] + +MIN_TEMPERATURE = 8 +MAX_TEMPERATURE = 28 + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Fritzbox smarthome thermostat platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_thermostat: + devices.append(FritzboxThermostat(device, fritz)) + + add_devices(devices) + + +class FritzboxThermostat(ClimateDevice): + """The thermostat class for Fritzbox smarthome thermostates.""" + + def __init__(self, device, fritz): + """Initialize the thermostat.""" + self._device = device + self._fritz = fritz + self._current_temperature = self._device.actual_temperature + self._target_temperature = self._device.target_temperature + self._comfort_temperature = self._device.comfort_temperature + self._eco_temperature = self._device.eco_temperature + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def available(self): + """Return if thermostat is available.""" + return self._device.present + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def temperature_unit(self): + """Return the unit of measurement that is used.""" + return TEMP_CELSIUS + + @property + def precision(self): + """Return precision 0.5.""" + return PRECISION_HALVES + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + if ATTR_OPERATION_MODE in kwargs: + operation_mode = kwargs.get(ATTR_OPERATION_MODE) + self.set_operation_mode(operation_mode) + elif ATTR_TEMPERATURE in kwargs: + temperature = kwargs.get(ATTR_TEMPERATURE) + self._device.set_target_temperature(temperature) + + @property + def current_operation(self): + """Return the current operation mode.""" + if self._target_temperature == self._comfort_temperature: + return STATE_HEAT + elif self._target_temperature == self._eco_temperature: + return STATE_ECO + return STATE_MANUAL + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return OPERATION_LIST + + def set_operation_mode(self, operation_mode): + """Set new operation mode.""" + if operation_mode == STATE_HEAT: + self.set_temperature(temperature=self._comfort_temperature) + elif operation_mode == STATE_ECO: + self.set_temperature(temperature=self._eco_temperature) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return MIN_TEMPERATURE + + @property + def max_temp(self): + """Return the maximum temperature.""" + return MAX_TEMPERATURE + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attrs = { + ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, + ATTR_STATE_LOCKED: self._device.lock, + ATTR_STATE_BATTERY_LOW: self._device.battery_low, + } + return attrs + + def update(self): + """Update the data from the thermostat.""" + try: + self._device.update() + self._current_temperature = self._device.actual_temperature + self._target_temperature = self._device.target_temperature + self._comfort_temperature = self._device.comfort_temperature + self._eco_temperature = self._device.eco_temperature + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Fritzbox connection error: %s", ex) + self._fritz.login() diff --git a/homeassistant/components/fritzbox.py b/homeassistant/components/fritzbox.py new file mode 100755 index 00000000000..a3c35aaa597 --- /dev/null +++ b/homeassistant/components/fritzbox.py @@ -0,0 +1,83 @@ +""" +Support for AVM Fritz!Box smarthome devices. + +For more details about this component, please refer to the documentation at +http://home-assistant.io/components/fritzbox/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['pyfritzhome==0.3.7'] + +SUPPORTED_DOMAINS = ['climate', 'switch'] + +DOMAIN = 'fritzbox' + +ATTR_STATE_DEVICE_LOCKED = 'device_locked' +ATTR_STATE_LOCKED = 'locked' +ATTR_STATE_BATTERY_LOW = 'battery_low' + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_DEVICES): + vol.All(cv.ensure_list, [ + vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + }), + ]), + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the fritzbox component.""" + from pyfritzhome import Fritzhome, LoginError + + fritz_list = [] + + configured_devices = config[DOMAIN].get(CONF_DEVICES) + for device in configured_devices: + host = device.get(CONF_HOST) + username = device.get(CONF_USERNAME) + password = device.get(CONF_PASSWORD) + fritzbox = Fritzhome(host=host, user=username, + password=password) + try: + fritzbox.login() + _LOGGER.info("Connected to device %s", device) + except LoginError: + _LOGGER.warning("Login to Fritz!Box %s as %s failed", + host, username) + continue + + fritz_list.append(fritzbox) + + if not fritz_list: + _LOGGER.info("No fritzboxes configured") + return False + + hass.data[DOMAIN] = fritz_list + + def logout_fritzboxes(event): + """Close all connections to the fritzboxes.""" + for fritz in fritz_list: + fritz.logout() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes) + + for domain in SUPPORTED_DOMAINS: + discovery.load_platform(hass, domain, DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/switch/fritzbox.py b/homeassistant/components/switch/fritzbox.py new file mode 100755 index 00000000000..c8313b0dfef --- /dev/null +++ b/homeassistant/components/switch/fritzbox.py @@ -0,0 +1,104 @@ +""" +Support for AVM Fritz!Box smarthome switch devices. + +For more details about this component, please refer to the documentation at +http://home-assistant.io/components/switch.fritzbox/ +""" +import logging + +import requests + +from homeassistant.components.fritzbox import DOMAIN as FRITZBOX_DOMAIN +from homeassistant.components.fritzbox import ( + ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +DEPENDENCIES = ['fritzbox'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_TOTAL_CONSUMPTION = 'total_consumption' +ATTR_TOTAL_CONSUMPTION_UNIT = 'total_consumption_unit' +ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = 'kWh' + +ATTR_TEMPERATURE_UNIT = 'temperature_unit' + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Fritzbox smarthome switch platform.""" + devices = [] + fritz_list = hass.data[FRITZBOX_DOMAIN] + + for fritz in fritz_list: + device_list = fritz.get_devices() + for device in device_list: + if device.has_switch: + devices.append(FritzboxSwitch(device, fritz)) + + add_devices(devices) + + +class FritzboxSwitch(SwitchDevice): + """The switch class for Fritzbox switches.""" + + def __init__(self, device, fritz): + """Initialize the switch.""" + self._device = device + self._fritz = fritz + + @property + def available(self): + """Return if switch is available.""" + return self._device.present + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def is_on(self): + """Return true if the switch is on.""" + return self._device.switch_state + + def turn_on(self, **kwargs): + """Turn the switch on.""" + self._device.set_switch_state_on() + + def turn_off(self, **kwargs): + """Turn the switch off.""" + self._device.set_switch_state_off() + + def update(self): + """Get latest data and states from the device.""" + try: + self._device.update() + except requests.exceptions.HTTPError as ex: + _LOGGER.warning("Fritzhome connection error: %s", ex) + self._fritz.login() + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attrs = {} + attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock + attrs[ATTR_STATE_LOCKED] = self._device.lock + + if self._device.has_powermeter: + attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( + (self._device.energy or 0.0) / 100000) + attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \ + ATTR_TOTAL_CONSUMPTION_UNIT_VALUE + if self._device.has_temperature_sensor: + attrs[ATTR_TEMPERATURE] = \ + str(self.hass.config.units.temperature( + self._device.temperature, TEMP_CELSIUS)) + attrs[ATTR_TEMPERATURE_UNIT] = \ + self.hass.config.units.temperature_unit + return attrs + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.power / 1000 diff --git a/requirements_all.txt b/requirements_all.txt index c2ee5814d7b..b3cf4dbeec1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -765,6 +765,9 @@ pyfido==2.1.1 # homeassistant.components.climate.flexit pyflexit==0.3 +# homeassistant.components.fritzbox +pyfritzhome==0.3.7 + # homeassistant.components.ifttt pyfttt==0.3 From 569f5c111fc1e1064df680b8f22a8584435dc495 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 17 Apr 2018 12:08:32 +0100 Subject: [PATCH 405/924] Adds SigFox sensor (#13731) * Create sigfox.py * Create test_sigfox.py * Update .coveragerc * Fix lints * Fix logger message string * More lints * Address reviewer comments * edit exception handling * Update sigfox.py * Update sigfox.py * Update sigfox.py * Update sigfox.py --- .coveragerc | 1 + homeassistant/components/sensor/sigfox.py | 161 ++++++++++++++++++++++ tests/components/sensor/test_sigfox.py | 68 +++++++++ 3 files changed, 230 insertions(+) create mode 100644 homeassistant/components/sensor/sigfox.py create mode 100644 tests/components/sensor/test_sigfox.py diff --git a/.coveragerc b/.coveragerc index 3da28762df0..1f86a13f6ae 100644 --- a/.coveragerc +++ b/.coveragerc @@ -649,6 +649,7 @@ omit = homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial.py homeassistant/components/sensor/shodan.py + homeassistant/components/sensor/sigfox.py homeassistant/components/sensor/simulated.py homeassistant/components/sensor/skybeacon.py homeassistant/components/sensor/sma.py diff --git a/homeassistant/components/sensor/sigfox.py b/homeassistant/components/sensor/sigfox.py new file mode 100644 index 00000000000..ef47132eefc --- /dev/null +++ b/homeassistant/components/sensor/sigfox.py @@ -0,0 +1,161 @@ +""" +Sensor for SigFox devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sigfox/ +""" +import logging +import datetime +import json +from urllib.parse import urljoin + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = datetime.timedelta(seconds=30) +API_URL = 'https://backend.sigfox.com/api/' +CONF_API_LOGIN = 'api_login' +CONF_API_PASSWORD = 'api_password' +DEFAULT_NAME = 'sigfox' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_LOGIN): cv.string, + vol.Required(CONF_API_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the sigfox sensor.""" + api_login = config[CONF_API_LOGIN] + api_password = config[CONF_API_PASSWORD] + name = config[CONF_NAME] + try: + sigfox = SigfoxAPI(api_login, api_password) + except ValueError: + return False + auth = sigfox.auth + devices = sigfox.devices + + sensors = [] + for device in devices: + sensors.append(SigfoxDevice(device, auth, name)) + add_devices(sensors, True) + + +def epoch_to_datetime(epoch_time): + """Take an ms since epoch and return datetime string.""" + return datetime.datetime.fromtimestamp(epoch_time).isoformat() + + +class SigfoxAPI(object): + """Class for interacting with the SigFox API.""" + + def __init__(self, api_login, api_password): + """Initialise the API object.""" + self._auth = requests.auth.HTTPBasicAuth(api_login, api_password) + if self.check_credentials(): + device_types = self.get_device_types() + self._devices = self.get_devices(device_types) + + def check_credentials(self): + """"Check API credentials are valid.""" + url = urljoin(API_URL, 'devicetypes') + response = requests.get(url, auth=self._auth, timeout=10) + if response.status_code != 200: + if response.status_code == 401: + _LOGGER.error( + "Invalid credentials for Sigfox API") + else: + _LOGGER.error( + "Unable to login to Sigfox API, error code %s", str( + response.status_code)) + raise ValueError('Sigfox component not setup') + return True + + def get_device_types(self): + """Get a list of device types.""" + url = urljoin(API_URL, 'devicetypes') + response = requests.get(url, auth=self._auth, timeout=10) + device_types = [] + for device in json.loads(response.text)['data']: + device_types.append(device['id']) + return device_types + + def get_devices(self, device_types): + """Get the device_id of each device registered.""" + devices = [] + for unique_type in device_types: + location_url = 'devicetypes/{}/devices'.format(unique_type) + url = urljoin(API_URL, location_url) + response = requests.get(url, auth=self._auth, timeout=10) + devices_data = json.loads(response.text)['data'] + for device in devices_data: + devices.append(device['id']) + return devices + + @property + def auth(self): + """Return the API authentification.""" + return self._auth + + @property + def devices(self): + """Return the list of device_id.""" + return self._devices + + +class SigfoxDevice(Entity): + """Class for single sigfox device.""" + + def __init__(self, device_id, auth, name): + """Initialise the device object.""" + self._device_id = device_id + self._auth = auth + self._message_data = {} + self._name = '{}_{}'.format(name, device_id) + self._state = None + + def get_last_message(self): + """Return the last message from a device.""" + device_url = 'devices/{}/messages?limit=1'.format(self._device_id) + url = urljoin(API_URL, device_url) + response = requests.get(url, auth=self._auth, timeout=10) + data = json.loads(response.text)['data'][0] + payload = bytes.fromhex(data['data']).decode('utf-8') + lat = data['rinfos'][0]['lat'] + lng = data['rinfos'][0]['lng'] + snr = data['snr'] + epoch_time = data['time'] + return {'lat': lat, + 'lng': lng, + 'payload': payload, + 'snr': snr, + 'time': epoch_to_datetime(epoch_time)} + + def update(self): + """Fetch the latest device message.""" + self._message_data = self.get_last_message() + self._state = self._message_data['payload'] + + @property + def name(self): + """Return the HA name of the sensor.""" + return self._name + + @property + def state(self): + """Return the payload of the last message.""" + return self._state + + @property + def device_state_attributes(self): + """Return other details about the last message.""" + return self._message_data diff --git a/tests/components/sensor/test_sigfox.py b/tests/components/sensor/test_sigfox.py new file mode 100644 index 00000000000..dcdeef56b98 --- /dev/null +++ b/tests/components/sensor/test_sigfox.py @@ -0,0 +1,68 @@ +"""Tests for the sigfox sensor.""" +import re +import requests_mock +import unittest + +from homeassistant.components.sensor.sigfox import ( + API_URL, CONF_API_LOGIN, CONF_API_PASSWORD) +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant + +TEST_API_LOGIN = 'foo' +TEST_API_PASSWORD = 'ebcd1234' + +VALID_CONFIG = { + 'sensor': { + 'platform': 'sigfox', + CONF_API_LOGIN: TEST_API_LOGIN, + CONF_API_PASSWORD: TEST_API_PASSWORD}} + +VALID_MESSAGE = """ +{"data":[{ +"time":1521879720, +"data":"7061796c6f6164", +"rinfos":[{"lat":"0.0","lng":"0.0"}], +"snr":"50.0"}]} +""" + + +class TestSigfoxSensor(unittest.TestCase): + """Test the sigfox platform.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_credentials(self): + """Test for a invalid credentials.""" + with requests_mock.Mocker() as mock_req: + url = re.compile(API_URL + 'devicetypes') + mock_req.get(url, text='{}', status_code=401) + self.assertTrue( + setup_component(self.hass, 'sensor', VALID_CONFIG)) + assert len(self.hass.states.entity_ids()) == 0 + + def test_valid_credentials(self): + """Test for a valid credentials.""" + with requests_mock.Mocker() as mock_req: + url1 = re.compile(API_URL + 'devicetypes') + mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', + status_code=200) + + url2 = re.compile(API_URL + 'devicetypes/fake_type/devices') + mock_req.get(url2, text='{"data":[{"id":"fake_id"}]}') + + url3 = re.compile(API_URL + 'devices/fake_id/messages*') + mock_req.get(url3, text=VALID_MESSAGE) + + self.assertTrue( + setup_component(self.hass, 'sensor', VALID_CONFIG)) + + assert len(self.hass.states.entity_ids()) == 1 + state = self.hass.states.get('sensor.sigfox_fake_id') + assert state.state == 'payload' + assert state.attributes.get('snr') == '50.0' From 9fe43714c61441ed36042b996c2e178da979fa2b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Apr 2018 13:32:16 +0200 Subject: [PATCH 406/924] Upgrade aiohttp to 3.1.3 (#13938) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9e21055f0c1..6de885942fb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.2 +aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/requirements_all.txt b/requirements_all.txt index b3cf4dbeec1..83c35033858 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,7 +6,7 @@ pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 typing>=3,<4 -aiohttp==3.1.2 +aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 diff --git a/setup.py b/setup.py index 602c1d19cbd..8815b0227ad 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ REQUIRES = [ 'jinja2>=2.10', 'voluptuous==0.11.1', 'typing>=3,<4', - 'aiohttp==3.1.2', + 'aiohttp==3.1.3', 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', From cff3bed1f034bbe929ef1ff3fa418dc9c19a1892 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 17 Apr 2018 13:32:44 +0200 Subject: [PATCH 407/924] Upgrade youtube_dl to 2018.04.16 (#13937) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 85c569789a2..b5fd26b0bcb 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.04.03'] +REQUIREMENTS = ['youtube_dl==2018.04.16'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 83c35033858..eae9e132a00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1352,7 +1352,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.03 +youtube_dl==2018.04.16 # homeassistant.components.light.zengge zengge==0.2 From 3b44f91395ba67ebc1363c4e8f2e02189f218cac Mon Sep 17 00:00:00 2001 From: Dmitry Avramenko Date: Tue, 17 Apr 2018 20:23:41 +0800 Subject: [PATCH 408/924] Added FB messenger broadcast api to notify.facebook component (#12459) * Added ability to use FB messenger broadcast api. use 'BROADCAST' keyword for first target in the facebook notifiy component to enable. * Added ability to use FB messenger broadcast api. use 'BROADCAST' keyword for first target in the facebook notifiy component to enable. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Added ability for broadcast messaging for facebook messenger notify platform. * Update facebook.py * Update facebook.py * Update facebook.py * Update facebook.py --- homeassistant/components/notify/facebook.py | 72 +++++++++++++++------ 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/notify/facebook.py b/homeassistant/components/notify/facebook.py index 791440fdb5b..b73f845ea17 100644 --- a/homeassistant/components/notify/facebook.py +++ b/homeassistant/components/notify/facebook.py @@ -4,6 +4,7 @@ Facebook platform for notify component. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.facebook/ """ +import json import logging from aiohttp.hdrs import CONTENT_TYPE @@ -19,6 +20,8 @@ _LOGGER = logging.getLogger(__name__) CONF_PAGE_ACCESS_TOKEN = 'page_access_token' BASE_URL = 'https://graph.facebook.com/v2.6/me/messages' +CREATE_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/message_creatives' +SEND_BROADCAST_URL = 'https://graph.facebook.com/v2.11/me/broadcast_messages' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PAGE_ACCESS_TOKEN): cv.string, @@ -55,27 +58,60 @@ class FacebookNotificationService(BaseNotificationService): _LOGGER.error("At least 1 target is required") return - for target in targets: - # If the target starts with a "+", we suppose it's a phone number, - # otherwise it's a user id. - if target.startswith('+'): - recipient = {"phone_number": target} - else: - recipient = {"id": target} + # broadcast message + if targets[0].lower() == 'broadcast': + broadcast_create_body = {"messages": [body_message]} + _LOGGER.debug("Broadcast body %s : ", broadcast_create_body) - body = { - "recipient": recipient, - "message": body_message + resp = requests.post(CREATE_BROADCAST_URL, + data=json.dumps(broadcast_create_body), + params=payload, + headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, + timeout=10) + _LOGGER.debug("FB Messager broadcast id %s : ", resp.json()) + + # at this point we get broadcast id + broadcast_body = { + "message_creative_id": resp.json().get('message_creative_id'), + "notification_type": "REGULAR", } - import json - resp = requests.post(BASE_URL, data=json.dumps(body), + + resp = requests.post(SEND_BROADCAST_URL, + data=json.dumps(broadcast_body), params=payload, headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, timeout=10) if resp.status_code != 200: - obj = resp.json() - error_message = obj['error']['message'] - error_code = obj['error']['code'] - _LOGGER.error( - "Error %s : %s (Code %s)", resp.status_code, error_message, - error_code) + log_error(resp) + + # non-broadcast message + else: + for target in targets: + # If the target starts with a "+", it's a phone number, + # otherwise it's a user id. + if target.startswith('+'): + recipient = {"phone_number": target} + else: + recipient = {"id": target} + + body = { + "recipient": recipient, + "message": body_message + } + resp = requests.post(BASE_URL, data=json.dumps(body), + params=payload, + headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, + timeout=10) + if resp.status_code != 200: + log_error(resp) + + +def log_error(response): + """Log error message.""" + obj = response.json() + error_message = obj['error']['message'] + error_code = obj['error']['code'] + + _LOGGER.error( + "Error %s : %s (Code %s)", response.status_code, error_message, + error_code) From f4b1a8e42d4f142151b252cd1ac214793fe01ba0 Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Tue, 17 Apr 2018 09:24:54 -0400 Subject: [PATCH 409/924] Added web view for TTS to get url (#13882) * Added web view for to get url * Added web view for TTS to get url * Added web view for TTS to get url * Added web view for TTS to get url * Fixed test * added auth * Update __init__.py --- homeassistant/components/tts/__init__.py | 115 ++++++++++++++--------- tests/components/tts/test_init.py | 46 ++++++++- 2 files changed, 117 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 17aa66ea825..999b584360c 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -37,6 +37,7 @@ ATTR_CACHE = 'cache' ATTR_LANGUAGE = 'language' ATTR_MESSAGE = 'message' ATTR_OPTIONS = 'options' +ATTR_PLATFORM = 'platform' CONF_CACHE = 'cache' CONF_CACHE_DIR = 'cache_dir' @@ -77,8 +78,7 @@ SCHEMA_SERVICE_SAY = vol.Schema({ SCHEMA_SERVICE_CLEAR_CACHE = vol.Schema({}) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up TTS.""" tts = SpeechManager(hass) @@ -88,27 +88,27 @@ def async_setup(hass, config): cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR) time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY) - yield from tts.async_init_cache(use_cache, cache_dir, time_memory) + await tts.async_init_cache(use_cache, cache_dir, time_memory) except (HomeAssistantError, KeyError) as err: _LOGGER.error("Error on cache init %s", err) return False hass.http.register_view(TextToSpeechView(tts)) + hass.http.register_view(TextToSpeechUrlView(tts)) - @asyncio.coroutine - def async_setup_platform(p_type, p_config, disc_info=None): + async def async_setup_platform(p_type, p_config, disc_info=None): """Set up a TTS platform.""" - platform = yield from async_prepare_setup_platform( + platform = await async_prepare_setup_platform( hass, config, DOMAIN, p_type) if platform is None: return try: if hasattr(platform, 'async_get_engine'): - provider = yield from platform.async_get_engine( + provider = await platform.async_get_engine( hass, p_config) else: - provider = yield from hass.async_add_job( + provider = await hass.async_add_job( platform.get_engine, hass, p_config) if provider is None: @@ -120,8 +120,7 @@ def async_setup(hass, config): _LOGGER.exception("Error setting up platform %s", p_type) return - @asyncio.coroutine - def async_say_handle(service): + async def async_say_handle(service): """Service handle for say.""" entity_ids = service.data.get(ATTR_ENTITY_ID) message = service.data.get(ATTR_MESSAGE) @@ -130,7 +129,7 @@ def async_setup(hass, config): options = service.data.get(ATTR_OPTIONS) try: - url = yield from tts.async_get_url( + url = await tts.async_get_url( p_type, message, cache=cache, language=language, options=options ) @@ -146,7 +145,7 @@ def async_setup(hass, config): if entity_ids: data[ATTR_ENTITY_ID] = entity_ids - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True) hass.services.async_register( @@ -157,12 +156,11 @@ def async_setup(hass, config): in config_per_platform(config, DOMAIN)] if setup_tasks: - yield from asyncio.wait(setup_tasks, loop=hass.loop) + await asyncio.wait(setup_tasks, loop=hass.loop) - @asyncio.coroutine - def async_clear_cache_handle(service): + async def async_clear_cache_handle(service): """Handle clear cache service call.""" - yield from tts.async_clear_cache() + await tts.async_clear_cache() hass.services.async_register( DOMAIN, SERVICE_CLEAR_CACHE, async_clear_cache_handle, @@ -185,8 +183,7 @@ class SpeechManager(object): self.file_cache = {} self.mem_cache = {} - @asyncio.coroutine - def async_init_cache(self, use_cache, cache_dir, time_memory): + async def async_init_cache(self, use_cache, cache_dir, time_memory): """Init config folder and load file cache.""" self.use_cache = use_cache self.time_memory = time_memory @@ -201,7 +198,7 @@ class SpeechManager(object): return cache_dir try: - self.cache_dir = yield from self.hass.async_add_job( + self.cache_dir = await self.hass.async_add_job( init_tts_cache_dir, cache_dir) except OSError as err: raise HomeAssistantError("Can't init cache dir {}".format(err)) @@ -222,15 +219,14 @@ class SpeechManager(object): return cache try: - cache_files = yield from self.hass.async_add_job(get_cache_files) + cache_files = await self.hass.async_add_job(get_cache_files) except OSError as err: raise HomeAssistantError("Can't read cache dir {}".format(err)) if cache_files: self.file_cache.update(cache_files) - @asyncio.coroutine - def async_clear_cache(self): + async def async_clear_cache(self): """Read file cache and delete files.""" self.mem_cache = {} @@ -243,7 +239,7 @@ class SpeechManager(object): _LOGGER.warning( "Can't remove cache file '%s': %s", filename, err) - yield from self.hass.async_add_job(remove_files) + await self.hass.async_add_job(remove_files) self.file_cache = {} @callback @@ -254,9 +250,8 @@ class SpeechManager(object): provider.name = engine self.providers[engine] = provider - @asyncio.coroutine - def async_get_url(self, engine, message, cache=None, language=None, - options=None): + async def async_get_url(self, engine, message, cache=None, language=None, + options=None): """Get URL for play message. This method is a coroutine. @@ -301,21 +296,20 @@ class SpeechManager(object): self.hass.async_add_job(self.async_file_to_mem(key)) # Load speech from provider into memory else: - filename = yield from self.async_get_tts_audio( + filename = await self.async_get_tts_audio( engine, key, message, use_cache, language, options) return "{}/api/tts_proxy/{}".format( self.hass.config.api.base_url, filename) - @asyncio.coroutine - def async_get_tts_audio(self, engine, key, message, cache, language, - options): + async def async_get_tts_audio(self, engine, key, message, cache, language, + options): """Receive TTS and store for view in cache. This method is a coroutine. """ provider = self.providers[engine] - extension, data = yield from provider.async_get_tts_audio( + extension, data = await provider.async_get_tts_audio( message, language, options) if data is None or extension is None: @@ -337,8 +331,7 @@ class SpeechManager(object): return filename - @asyncio.coroutine - def async_save_tts_audio(self, key, filename, data): + async def async_save_tts_audio(self, key, filename, data): """Store voice data to file and file_cache. This method is a coroutine. @@ -351,13 +344,12 @@ class SpeechManager(object): speech.write(data) try: - yield from self.hass.async_add_job(save_speech) + await self.hass.async_add_job(save_speech) self.file_cache[key] = filename except OSError: _LOGGER.error("Can't write %s", filename) - @asyncio.coroutine - def async_file_to_mem(self, key): + async def async_file_to_mem(self, key): """Load voice from file cache into memory. This method is a coroutine. @@ -374,7 +366,7 @@ class SpeechManager(object): return speech.read() try: - data = yield from self.hass.async_add_job(load_speech) + data = await self.hass.async_add_job(load_speech) except OSError: del self.file_cache[key] raise HomeAssistantError("Can't read {}".format(voice_file)) @@ -396,8 +388,7 @@ class SpeechManager(object): self.hass.loop.call_later(self.time_memory, async_remove_from_mem) - @asyncio.coroutine - def async_read_tts(self, filename): + async def async_read_tts(self, filename): """Read a voice file and return binary. This method is a coroutine. @@ -412,7 +403,7 @@ class SpeechManager(object): if key not in self.mem_cache: if key not in self.file_cache: raise HomeAssistantError("{} not in cache!".format(key)) - yield from self.async_file_to_mem(key) + await self.async_file_to_mem(key) content, _ = mimetypes.guess_type(filename) return (content, self.mem_cache[key][MEM_CACHE_VOICE]) @@ -490,6 +481,45 @@ class Provider(object): ft.partial(self.get_tts_audio, message, language, options=options)) +class TextToSpeechUrlView(HomeAssistantView): + """TTS view to get a url to a generated speech file.""" + + requires_auth = True + url = '/api/tts_get_url' + name = 'api:tts:geturl' + + def __init__(self, tts): + """Initialize a tts view.""" + self.tts = tts + + async def post(self, request): + """Generate speech and provide url.""" + try: + data = await request.json() + except ValueError: + return self.json_message('Invalid JSON specified', 400) + if not data.get(ATTR_PLATFORM) and data.get(ATTR_MESSAGE): + return self.json_message('Must specify platform and message', 400) + + p_type = data[ATTR_PLATFORM] + message = data[ATTR_MESSAGE] + cache = data.get(ATTR_CACHE) + language = data.get(ATTR_LANGUAGE) + options = data.get(ATTR_OPTIONS) + + try: + url = await self.tts.async_get_url( + p_type, message, cache=cache, language=language, + options=options + ) + resp = self.json({'url': url}, 200) + except HomeAssistantError as err: + _LOGGER.error("Error on init tts: %s", err) + resp = self.json({'error': err}, 400) + + return resp + + class TextToSpeechView(HomeAssistantView): """TTS view to serve a speech audio.""" @@ -501,11 +531,10 @@ class TextToSpeechView(HomeAssistantView): """Initialize a tts view.""" self.tts = tts - @asyncio.coroutine - def get(self, request, filename): + async def get(self, request, filename): """Start a get request.""" try: - content, data = yield from self.tts.async_read_tts(filename) + content, data = await self.tts.async_read_tts(filename) except HomeAssistantError as err: _LOGGER.error("Error on load tts: %s", err) return web.Response(status=404) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 7a15ed28f97..b6bfa430fd2 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -2,6 +2,7 @@ import ctypes import os import shutil +import json from unittest.mock import patch, PropertyMock import pytest @@ -353,7 +354,7 @@ class TestTTS(object): demo_data = tts.SpeechManager.write_tags( "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", demo_data, self.demo_provider, - "I person is on front of your door.", 'en', None) + "AI person is in front of your door.", 'en', None) assert req.status_code == 200 assert req.content == demo_data @@ -562,3 +563,46 @@ class TestTTS(object): req = requests.get(url) assert req.status_code == 200 assert req.content == demo_data + + def test_setup_component_and_web_get_url(self): + """Setup the demo platform and receive wrong file from web.""" + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.start() + + url = ("{}/api/tts_get_url").format(self.hass.config.api.base_url) + data = {'platform': 'demo', + 'message': "I person is on front of your door."} + + req = requests.post(url, data=json.dumps(data)) + assert req.status_code == 200 + response = json.loads(req.text) + assert response.get('url') == (("{}/api/tts_proxy/265944c108cbb00b2a62" + "1be5930513e03a0bb2cd_en_-_demo.mp3") + .format(self.hass.config.api.base_url)) + + def test_setup_component_and_web_get_url_bad_config(self): + """Setup the demo platform and receive wrong file from web.""" + config = { + tts.DOMAIN: { + 'platform': 'demo', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.start() + + url = ("{}/api/tts_get_url").format(self.hass.config.api.base_url) + data = {'message': "I person is on front of your door."} + + req = requests.post(url, data=data) + assert req.status_code == 400 From 783e9a5f8c538493f956a01673693d3f4617134e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Apr 2018 10:17:54 -0400 Subject: [PATCH 410/924] Update frontend to 20180417 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 80b7cdff5a8..45d16ae5fb6 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180414.0'] +REQUIREMENTS = ['home-assistant-frontend==20180417.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index eae9e132a00..c496793c11f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -380,7 +380,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180414.0 +home-assistant-frontend==20180417.0 # homeassistant.components.homekit_controller # homekit==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7ce54c64c0..4c06b6ad959 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180414.0 +home-assistant-frontend==20180417.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e472436b8467f5bfee9659badced49ac58fb4a06 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Tue, 17 Apr 2018 17:37:00 +0200 Subject: [PATCH 411/924] Add services for bmw_connected_drive (#13497) * implemented services for bmw remote services * added vin to attributes of tracker * moved component to new package * added service description * fixed static analysis warnings * implemented first set of code reviews * removed locking related services * fixed static analysis warnings * removed excess blank lines * refactoring of setup() to resolve warning "Cell variable bimmer defined in loop (cell-var-from-loop)" * added missing docstring * added service to update all vehicles from the server * implemented changes requested in code review * added check if invalid vin is entered --- .../__init__.py} | 85 ++++++++++++++----- .../bmw_connected_drive/services.yaml | 42 +++++++++ .../device_tracker/bmw_connected_drive.py | 7 +- 3 files changed, 113 insertions(+), 21 deletions(-) rename homeassistant/components/{bmw_connected_drive.py => bmw_connected_drive/__init__.py} (55%) create mode 100644 homeassistant/components/bmw_connected_drive/services.yaml diff --git a/homeassistant/components/bmw_connected_drive.py b/homeassistant/components/bmw_connected_drive/__init__.py similarity index 55% rename from homeassistant/components/bmw_connected_drive.py rename to homeassistant/components/bmw_connected_drive/__init__.py index 48452b6d79b..347bab6f529 100644 --- a/homeassistant/components/bmw_connected_drive.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'bmw_connected_drive' CONF_REGION = 'region' - +ATTR_VIN = 'vin' ACCOUNT_SCHEMA = vol.Schema({ vol.Required(CONF_USERNAME): cv.string, @@ -35,35 +35,40 @@ CONFIG_SCHEMA = vol.Schema({ }, }, extra=vol.ALLOW_EXTRA) +SERVICE_SCHEMA = vol.Schema({ + vol.Required(ATTR_VIN): cv.string, +}) + BMW_COMPONENTS = ['binary_sensor', 'device_tracker', 'lock', 'sensor'] UPDATE_INTERVAL = 5 # in minutes +SERVICE_UPDATE_STATE = 'update_state' -def setup(hass, config): +_SERVICE_MAP = { + 'light_flash': 'trigger_remote_light_flash', + 'sound_horn': 'trigger_remote_horn', + 'activate_air_conditioning': 'trigger_remote_air_conditioning', +} + + +def setup(hass, config: dict): """Set up the BMW connected drive components.""" accounts = [] for name, account_config in config[DOMAIN].items(): - username = account_config[CONF_USERNAME] - password = account_config[CONF_PASSWORD] - region = account_config[CONF_REGION] - _LOGGER.debug('Adding new account %s', name) - bimmer = BMWConnectedDriveAccount(username, password, region, name) - accounts.append(bimmer) - - # update every UPDATE_INTERVAL minutes, starting now - # this should even out the load on the servers - - now = datetime.datetime.now() - track_utc_time_change( - hass, bimmer.update, - minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL), - second=now.second) + accounts.append(setup_account(account_config, hass, name)) hass.data[DOMAIN] = accounts - for account in accounts: - account.update() + def _update_all(call) -> None: + """Update all BMW accounts.""" + for cd_account in hass.data[DOMAIN]: + cd_account.update() + + # Service to manually trigger updates for all accounts. + hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all) + + _update_all(None) for component in BMW_COMPONENTS: discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -71,6 +76,48 @@ def setup(hass, config): return True +def setup_account(account_config: dict, hass, name: str) \ + -> 'BMWConnectedDriveAccount': + """Set up a new BMWConnectedDriveAccount based on the config.""" + username = account_config[CONF_USERNAME] + password = account_config[CONF_PASSWORD] + region = account_config[CONF_REGION] + _LOGGER.debug('Adding new account %s', name) + cd_account = BMWConnectedDriveAccount(username, password, region, name) + + def execute_service(call): + """Execute a service for a vehicle. + + This must be a member function as we need access to the cd_account + object here. + """ + vin = call.data[ATTR_VIN] + vehicle = cd_account.account.get_vehicle(vin) + if not vehicle: + _LOGGER.error('Could not find a vehicle for VIN "%s"!', vin) + return + function_name = _SERVICE_MAP[call.service] + function_call = getattr(vehicle.remote_services, function_name) + function_call() + + # register the remote services + for service in _SERVICE_MAP: + hass.services.register( + DOMAIN, service, + execute_service, + schema=SERVICE_SCHEMA) + + # update every UPDATE_INTERVAL minutes, starting now + # this should even out the load on the servers + now = datetime.datetime.now() + track_utc_time_change( + hass, cd_account.update, + minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL), + second=now.second) + + return cd_account + + class BMWConnectedDriveAccount(object): """Representation of a BMW vehicle.""" diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml new file mode 100644 index 00000000000..3c180271919 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -0,0 +1,42 @@ +# Describes the format for available services for bmw_connected_drive +# +# The services related to locking/unlocking are implemented in the lock +# component to avoid redundancy. + +light_flash: + description: > + Flash the lights of the vehicle. The vehicle is identified via the vin + (see below). + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + +sound_horn: + description: > + Sound the horn of the vehicle. The vehicle is identified via the vin + (see below). + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + +activate_air_conditioning: + description: > + Start the air conditioning of the vehicle. What exactly is started here + depends on the type of vehicle. It might range from just ventilation over + auxilary heating to real air conditioning. The vehicle is identified via + the vin (see below). + fields: + vin: + description: > + The vehicle identification number (VIN) of the vehicle, 17 characters + example: WBANXXXXXX1234567 + +update_state: + description: > + Fetch the last state of the vehicles of all your accounts from the BMW + server. This does *not* trigger an update from the vehicle, it just gets + the data from the BMW servers. This service does not require any attributes. \ No newline at end of file diff --git a/homeassistant/components/device_tracker/bmw_connected_drive.py b/homeassistant/components/device_tracker/bmw_connected_drive.py index 2267bb51944..f36afc622ee 100644 --- a/homeassistant/components/device_tracker/bmw_connected_drive.py +++ b/homeassistant/components/device_tracker/bmw_connected_drive.py @@ -48,8 +48,11 @@ class BMWDeviceTracker(object): return _LOGGER.debug('Updating %s', dev_id) - + attrs = { + 'vin': self.vehicle.vin, + } self._see( dev_id=dev_id, host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, icon='mdi:car' + gps=self.vehicle.state.gps_position, attributes=attrs, + icon='mdi:car' ) From 08f545d67b2b8add12bc6ac9961f8f510164668b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 17 Apr 2018 17:40:52 +0200 Subject: [PATCH 412/924] Fix call to parent broadlink switch (#13906) * Broadlink switch, fixes issue #13799 * slugify --- homeassistant/components/switch/broadlink.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 3e620a6a25b..50c334b1f09 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, slugify from homeassistant.util.dt import utcnow REQUIREMENTS = ['broadlink==0.8.0'] @@ -187,7 +187,7 @@ class BroadlinkRMSwitch(SwitchDevice): def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" - self.entity_id = ENTITY_ID_FORMAT.format(name) + self.entity_id = ENTITY_ID_FORMAT.format(slugify(name)) self._name = friendly_name self._state = False self._command_on = b64decode(command_on) if command_on else None @@ -257,7 +257,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): def __init__(self, friendly_name, device): """Initialize the switch.""" - super().__init__(friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 @@ -313,7 +313,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): def __init__(self, friendly_name, device, slot, parent_device): """Initialize the slot of switch.""" - super().__init__(friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._slot = slot From 1a9ea11665bbf15f37b83dd9a226825c5a1fe3e6 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Tue, 17 Apr 2018 20:00:53 +0200 Subject: [PATCH 413/924] Bump deCONZ requirement to v36 (#13960) --- homeassistant/components/deconz/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 04cd42ca620..0cf96576223 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers import discovery, aiohttp_client from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==35'] +REQUIREMENTS = ['pydeconz==36'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c496793c11f..e40e513bbc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -730,7 +730,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==35 +pydeconz==36 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c06b6ad959..56a645d4fd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -130,7 +130,7 @@ pushbullet.py==0.11.0 py-canary==0.5.0 # homeassistant.components.deconz -pydeconz==35 +pydeconz==36 # homeassistant.components.zwave pydispatcher==2.0.5 From 65b8f9764abb24982a0bf4e488bb4e8e9460d573 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Apr 2018 12:03:22 -0600 Subject: [PATCH 414/924] Bumped pypollencom to 1.1.2 (#13959) * Bumped pypollencom to 1.1.2 * Updated requirements_all.txt --- homeassistant/components/sensor/pollen.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 640e13e437a..b55c60f6e7c 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, slugify -REQUIREMENTS = ['pypollencom==1.1.1'] +REQUIREMENTS = ['pypollencom==1.1.2'] _LOGGER = logging.getLogger(__name__) ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' diff --git a/requirements_all.txt b/requirements_all.txt index e40e513bbc9..38e9ee01ab1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -889,7 +889,7 @@ pyotp==2.2.6 pyowm==2.8.0 # homeassistant.components.sensor.pollen -pypollencom==1.1.1 +pypollencom==1.1.2 # homeassistant.components.qwikswitch pyqwikswitch==0.71 From e836674a30c4f6f6a9fd928314414e9409538205 Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Sat, 7 Apr 2018 13:48:53 +1000 Subject: [PATCH 415/924] Fix Gogogate2 'available' attribute (#13728) * Fixed bug - unable to set base readaonly property * PR fixes * Added line --- homeassistant/components/cover/gogogate2.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index c2bdc9c5472..99da248b094 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -11,7 +11,7 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, STATE_UNKNOWN, + CONF_USERNAME, CONF_PASSWORD, STATE_CLOSED, CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -50,7 +50,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(MyGogogate2Device( mygogogate2, door, name) for door in devices) - return except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) @@ -60,7 +59,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): ''.format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) - return class MyGogogate2Device(CoverDevice): @@ -72,7 +70,7 @@ class MyGogogate2Device(CoverDevice): self.device_id = device['door'] self._name = name or device['name'] self._status = device['status'] - self.available = None + self._available = None @property def name(self): @@ -97,24 +95,22 @@ class MyGogogate2Device(CoverDevice): @property def available(self): """Could the device be accessed during the last update call.""" - return self.available + return self._available def close_cover(self, **kwargs): """Issue close command to cover.""" self.mygogogate2.close_device(self.device_id) - self.schedule_update_ha_state(True) def open_cover(self, **kwargs): """Issue open command to cover.""" self.mygogogate2.open_device(self.device_id) - self.schedule_update_ha_state(True) def update(self): """Update status of cover.""" try: self._status = self.mygogogate2.get_status(self.device_id) - self.available = True + self._available = True except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) - self._status = STATE_UNKNOWN - self.available = False + self._status = None + self._available = False From 0adb240fd66ef356abc1708acf7a2a45f307b545 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Sat, 7 Apr 2018 23:18:49 +0200 Subject: [PATCH 416/924] Fix so it is possible to ignore discovered config entry handlers (#13741) * Fix so it is possible to ignore discovered config entry handlers * Improve efficiency --- homeassistant/components/discovery.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index b2aa5b890a8..01ef36b778b 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -84,7 +84,8 @@ CONF_IGNORE = 'ignore' CONFIG_SCHEMA = vol.Schema({ vol.Required(DOMAIN): vol.Schema({ vol.Optional(CONF_IGNORE, default=[]): - vol.All(cv.ensure_list, [vol.In(SERVICE_HANDLERS)]) + vol.All(cv.ensure_list, [ + vol.In(list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS))]) }), }, extra=vol.ALLOW_EXTRA) From 26c76e3399dead4f014bebae2b1f38ec897011bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Apr 2018 04:31:03 -0400 Subject: [PATCH 417/924] Prevent vesync doing I/O in event loop (#13862) --- homeassistant/components/switch/vesync.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/switch/vesync.py b/homeassistant/components/switch/vesync.py index fbc73545e19..d8579a508e2 100644 --- a/homeassistant/components/switch/vesync.py +++ b/homeassistant/components/switch/vesync.py @@ -60,6 +60,8 @@ class VeSyncSwitchHA(SwitchDevice): def __init__(self, plug): """Initialize the VeSync switch device.""" self.smartplug = plug + self._current_power_w = None + self._today_energy_kwh = None @property def unique_id(self): @@ -74,12 +76,12 @@ class VeSyncSwitchHA(SwitchDevice): @property def current_power_w(self): """Return the current power usage in W.""" - return self.smartplug.get_power() + return self._current_power_w @property def today_energy_kwh(self): """Return the today total energy usage in kWh.""" - return self.smartplug.get_kwh_today() + return self._today_energy_kwh @property def available(self) -> bool: @@ -102,3 +104,5 @@ class VeSyncSwitchHA(SwitchDevice): def update(self): """Handle data changes for node values.""" self.smartplug.update() + self._current_power_w = self.smartplug.get_power() + self._today_energy_kwh = self.smartplug.get_kwh_today() From 727ab956cf6e311397fca6573020a2d6de0527c4 Mon Sep 17 00:00:00 2001 From: Kyle Niewiada Date: Sun, 15 Apr 2018 07:59:10 -0400 Subject: [PATCH 418/924] Fix #13846 Double underscore in bluetooth address (#13884) --- homeassistant/components/device_tracker/bluetooth_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/bluetooth_tracker.py b/homeassistant/components/device_tracker/bluetooth_tracker.py index 807f6c0d0a4..2ca519d225c 100644 --- a/homeassistant/components/device_tracker/bluetooth_tracker.py +++ b/homeassistant/components/device_tracker/bluetooth_tracker.py @@ -40,7 +40,7 @@ def setup_scanner(hass, config, see, discovery_info=None): attributes = {} if rssi is not None: attributes['rssi'] = rssi - see(mac="{}_{}".format(BT_PREFIX, mac), host_name=name, + see(mac="{}{}".format(BT_PREFIX, mac), host_name=name, attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH) def discover_devices(): From 663aeb11dccbf0e0c79c201ea2e85b47db72dbb0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 14 Apr 2018 17:58:45 -0400 Subject: [PATCH 419/924] Fix race condition for component loaded before listening (#13887) * Fix race condition for component loaded before listening * async/await syntax --- homeassistant/components/config/__init__.py | 49 +++++++++------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 4d0295c382a..5a8800d9583 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -18,37 +18,26 @@ SECTIONS = ('core', 'customize', 'group', 'hassbian', 'automation', 'script', ON_DEMAND = ('zwave',) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the config component.""" - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'config', 'config', 'mdi:settings') - @asyncio.coroutine - def setup_panel(panel_name): + async def setup_panel(panel_name): """Set up a panel.""" - panel = yield from async_prepare_setup_platform( + panel = await async_prepare_setup_platform( hass, config, DOMAIN, panel_name) if not panel: return - success = yield from panel.async_setup(hass) + success = await panel.async_setup(hass) if success: key = '{}.{}'.format(DOMAIN, panel_name) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) hass.config.components.add(key) - tasks = [setup_panel(panel_name) for panel_name in SECTIONS] - - for panel_name in ON_DEMAND: - if panel_name in hass.config.components: - tasks.append(setup_panel(panel_name)) - - if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) - @callback def component_loaded(event): """Respond to components being loaded.""" @@ -58,6 +47,15 @@ def async_setup(hass, config): hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + tasks = [setup_panel(panel_name) for panel_name in SECTIONS] + + for panel_name in ON_DEMAND: + if panel_name in hass.config.components: + tasks.append(setup_panel(panel_name)) + + if tasks: + await asyncio.wait(tasks, loop=hass.loop) + return True @@ -86,11 +84,10 @@ class BaseEditConfigView(HomeAssistantView): """Set value.""" raise NotImplementedError - @asyncio.coroutine - def get(self, request, config_key): + async def get(self, request, config_key): """Fetch device specific config.""" hass = request.app['hass'] - current = yield from self.read_config(hass) + current = await self.read_config(hass) value = self._get_value(hass, current, config_key) if value is None: @@ -98,11 +95,10 @@ class BaseEditConfigView(HomeAssistantView): return self.json(value) - @asyncio.coroutine - def post(self, request, config_key): + async def post(self, request, config_key): """Validate config and return results.""" try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON specified', 400) @@ -121,10 +117,10 @@ class BaseEditConfigView(HomeAssistantView): hass = request.app['hass'] path = hass.config.path(self.path) - current = yield from self.read_config(hass) + current = await self.read_config(hass) self._write_value(hass, current, config_key, data) - yield from hass.async_add_job(_write, path, current) + await hass.async_add_job(_write, path, current) if self.post_write_hook is not None: hass.async_add_job(self.post_write_hook(hass)) @@ -133,10 +129,9 @@ class BaseEditConfigView(HomeAssistantView): 'result': 'ok', }) - @asyncio.coroutine - def read_config(self, hass): + async def read_config(self, hass): """Read the config.""" - current = yield from hass.async_add_job( + current = await hass.async_add_job( _read, hass.config.path(self.path)) if not current: current = self._empty_config() From bcd8a69dfc77778b6349d624d4052a9e2e50f6e0 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 14 Apr 2018 23:53:35 +0200 Subject: [PATCH 420/924] Missing property decorator added (#13889) --- homeassistant/components/fan/xiaomi_miio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 8dc6bb54bd1..16affc08467 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -748,6 +748,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): self._available = False _LOGGER.error("Got exception while fetching the state: %s", ex) + @property def speed_list(self) -> list: """Get the list of available speeds.""" return self._speed_list From 652063537bfc853b77f982b6397075079970e400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Tue, 17 Apr 2018 17:40:52 +0200 Subject: [PATCH 421/924] Fix call to parent broadlink switch (#13906) * Broadlink switch, fixes issue #13799 * slugify --- homeassistant/components/switch/broadlink.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 3e620a6a25b..50c334b1f09 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_FRIENDLY_NAME, CONF_HOST, CONF_MAC, CONF_SWITCHES, CONF_TIMEOUT, CONF_TYPE) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, slugify from homeassistant.util.dt import utcnow REQUIREMENTS = ['broadlink==0.8.0'] @@ -187,7 +187,7 @@ class BroadlinkRMSwitch(SwitchDevice): def __init__(self, name, friendly_name, device, command_on, command_off): """Initialize the switch.""" - self.entity_id = ENTITY_ID_FORMAT.format(name) + self.entity_id = ENTITY_ID_FORMAT.format(slugify(name)) self._name = friendly_name self._state = False self._command_on = b64decode(command_on) if command_on else None @@ -257,7 +257,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): def __init__(self, friendly_name, device): """Initialize the switch.""" - super().__init__(friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 @@ -313,7 +313,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): def __init__(self, friendly_name, device, slot, parent_device): """Initialize the slot of switch.""" - super().__init__(friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None) self._command_on = 1 self._command_off = 0 self._slot = slot From fadff1855ff6b697c7ba69f663a94618d276b11d Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 15 Apr 2018 15:19:28 +0200 Subject: [PATCH 422/924] Import operation modes from air humidifier (#13908) --- homeassistant/components/fan/xiaomi_miio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fan/xiaomi_miio.py b/homeassistant/components/fan/xiaomi_miio.py index 16affc08467..2acc3895f3e 100644 --- a/homeassistant/components/fan/xiaomi_miio.py +++ b/homeassistant/components/fan/xiaomi_miio.py @@ -708,7 +708,7 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): def __init__(self, name, device, model, unique_id): """Initialize the plug switch.""" - from miio.airpurifier import OperationMode + from miio.airhumidifier import OperationMode super().__init__(name, device, model, unique_id) From 6fa60c464bb012c73f03b9b3ea69d82539d7c8f5 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sun, 15 Apr 2018 22:19:15 +0200 Subject: [PATCH 423/924] Upgrade pyqwikswitch to 0.71 (#13920) --- homeassistant/components/qwikswitch.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 708eff7cf11..4ecdad10a88 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.light import ATTR_BRIGHTNESS import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.6'] +REQUIREMENTS = ['pyqwikswitch==0.71'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8fe9c7e1c13..ec705e69e52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -885,7 +885,7 @@ pyowm==2.8.0 pypollencom==1.1.1 # homeassistant.components.qwikswitch -pyqwikswitch==0.6 +pyqwikswitch==0.71 # homeassistant.components.rainbird pyrainbird==0.1.3 From 53506821d4d0b359a4dde64d91de8833b1d28504 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Apr 2018 23:24:20 -0400 Subject: [PATCH 424/924] Upgrade somecomfort to 0.5.2 (#13940) --- homeassistant/components/climate/honeywell.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/climate/honeywell.py b/homeassistant/components/climate/honeywell.py index 20d93e3116a..11a507aded2 100644 --- a/homeassistant/components/climate/honeywell.py +++ b/homeassistant/components/climate/honeywell.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_REGION) -REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.0'] +REQUIREMENTS = ['evohomeclient==0.2.5', 'somecomfort==0.5.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ec705e69e52..89a58f4fcc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1185,7 +1185,7 @@ smappy==0.2.15 snapcast==2.0.8 # homeassistant.components.climate.honeywell -somecomfort==0.5.0 +somecomfort==0.5.2 # homeassistant.components.sensor.speedtest speedtest-cli==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c5467f7608..e44b0dc85d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -174,7 +174,7 @@ rxv==0.5.1 sleepyq==0.6 # homeassistant.components.climate.honeywell -somecomfort==0.5.0 +somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator From e9b997de3e086d6ffa860b23608792e6c70d3a4b Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Mon, 16 Apr 2018 20:52:56 -0400 Subject: [PATCH 425/924] Update pyhydroquebec to 2.2.2 (#13946) --- homeassistant/components/sensor/hydroquebec.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/hydroquebec.py b/homeassistant/components/sensor/hydroquebec.py index 9129ee17d80..2195153ab1e 100644 --- a/homeassistant/components/sensor/hydroquebec.py +++ b/homeassistant/components/sensor/hydroquebec.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyhydroquebec==2.2.1'] +REQUIREMENTS = ['pyhydroquebec==2.2.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 89a58f4fcc9..093fec0a150 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ pyhiveapi==0.2.11 pyhomematic==0.1.40 # homeassistant.components.sensor.hydroquebec -pyhydroquebec==2.2.1 +pyhydroquebec==2.2.2 # homeassistant.components.alarm_control_panel.ialarm pyialarm==0.2 From 6c456ade6a858c76766b35cc3ed7baaaa6b41791 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Mon, 16 Apr 2018 22:16:28 -0400 Subject: [PATCH 426/924] Update pyfido to 2.1.1 (#13947) --- homeassistant/components/sensor/fido.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/fido.py b/homeassistant/components/sensor/fido.py index 25a104bf259..a2ee18b3659 100644 --- a/homeassistant/components/sensor/fido.py +++ b/homeassistant/components/sensor/fido.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyfido==2.1.0'] +REQUIREMENTS = ['pyfido==2.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 093fec0a150..44d90c4582d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -756,7 +756,7 @@ pyenvisalink==2.2 pyephember==0.1.1 # homeassistant.components.sensor.fido -pyfido==2.1.0 +pyfido==2.1.1 # homeassistant.components.climate.flexit pyflexit==0.3 From 24ec8c545b317aaf3dfc51fd2beb4483a036cb12 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Apr 2018 12:03:22 -0600 Subject: [PATCH 427/924] Bumped pypollencom to 1.1.2 (#13959) * Bumped pypollencom to 1.1.2 * Updated requirements_all.txt --- homeassistant/components/sensor/pollen.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index 640e13e437a..b55c60f6e7c 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, slugify -REQUIREMENTS = ['pypollencom==1.1.1'] +REQUIREMENTS = ['pypollencom==1.1.2'] _LOGGER = logging.getLogger(__name__) ATTR_ALLERGEN_GENUS = 'primary_allergen_genus' diff --git a/requirements_all.txt b/requirements_all.txt index 44d90c4582d..01c6d395778 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -882,7 +882,7 @@ pyotp==2.2.6 pyowm==2.8.0 # homeassistant.components.sensor.pollen -pypollencom==1.1.1 +pypollencom==1.1.2 # homeassistant.components.qwikswitch pyqwikswitch==0.71 From e7aea5c5715c6cefe757860c506fd16b7bb56a10 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Apr 2018 22:37:40 -0400 Subject: [PATCH 428/924] Version bump to 0.67.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5364fe6951e..4b8d7bcd3bc 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 67 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c076dbe7e4f3d1efbd79b11a1d8972201f4d7cbc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Apr 2018 22:59:36 -0400 Subject: [PATCH 429/924] Revert "Upgrade pyqwikswitch to 0.71 (#13920)" This reverts commit 6fa60c464bb012c73f03b9b3ea69d82539d7c8f5. --- homeassistant/components/qwikswitch.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 4ecdad10a88..708eff7cf11 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.components.light import ATTR_BRIGHTNESS import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.71'] +REQUIREMENTS = ['pyqwikswitch==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 01c6d395778..36a8f30502f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -885,7 +885,7 @@ pyowm==2.8.0 pypollencom==1.1.2 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.6 # homeassistant.components.rainbird pyrainbird==0.1.3 From 4ba58d0760943b7a7b2c028636647e3e68962802 Mon Sep 17 00:00:00 2001 From: Mister Wil <1091741+MisterWil@users.noreply.github.com> Date: Wed, 18 Apr 2018 01:10:32 -0700 Subject: [PATCH 430/924] Bump skybellpy version to 0.1.2 (#13974) --- homeassistant/components/skybell.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/skybell.py b/homeassistant/components/skybell.py index 854abdda7bc..3f27c91e7c5 100644 --- a/homeassistant/components/skybell.py +++ b/homeassistant/components/skybell.py @@ -14,7 +14,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['skybellpy==0.1.1'] +REQUIREMENTS = ['skybellpy==0.1.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 38e9ee01ab1..8839fb841ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1169,7 +1169,7 @@ simplepush==1.1.4 simplisafe-python==1.0.5 # homeassistant.components.skybell -skybellpy==0.1.1 +skybellpy==0.1.2 # homeassistant.components.notify.slack slacker==0.9.65 From f11d4319d2b9b07384380c8ff4a2d132d4bfbbb2 Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Wed, 18 Apr 2018 12:43:55 +0200 Subject: [PATCH 431/924] Fix typo an coding style (#13970) --- .../components/device_tracker/nmap_tracker.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/device_tracker/nmap_tracker.py b/homeassistant/components/device_tracker/nmap_tracker.py index f62f53fe5fc..3c090e8cd3b 100644 --- a/homeassistant/components/device_tracker/nmap_tracker.py +++ b/homeassistant/components/device_tracker/nmap_tracker.py @@ -94,13 +94,11 @@ class NmapDeviceScanner(DeviceScanner): return None def get_extra_attributes(self, device): - """Return the IP pf the given device.""" - filter_ip = [result.ip for result in self.last_results - if result.mac == device] - - if filter_ip: - return {'ip': filter_ip[0]} - return None + """Return the IP of the given device.""" + filter_ip = next(( + result.ip for result in self.last_results + if result.mac == device), None) + return {'ip': filter_ip} def _update_info(self): """Scan the network for devices. From 23b97b9105fbb20396488acd6a4e6bd447195e99 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 18 Apr 2018 14:38:44 +0200 Subject: [PATCH 432/924] Params of the send command can be a list now (#13905) --- homeassistant/components/vacuum/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 095e8bfb124..d403a776ddf 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -57,7 +57,7 @@ VACUUM_SET_FAN_SPEED_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({ VACUUM_SEND_COMMAND_SERVICE_SCHEMA = VACUUM_SERVICE_SCHEMA.extend({ vol.Required(ATTR_COMMAND): cv.string, - vol.Optional(ATTR_PARAMS): cv.Dict, + vol.Optional(ATTR_PARAMS): vol.Any(cv.Dict, cv.ensure_list), }) SERVICE_TO_METHOD = { From b589dbf26c1d387105935b45f8d5bde1c3d52be1 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Wed, 18 Apr 2018 22:39:58 +1000 Subject: [PATCH 433/924] Support basic covers with open/close/stop services HomeKit (#13819) * Support basic covers with open/close/stop services * Support optional stop * Tests --- homeassistant/components/homekit/__init__.py | 2 + homeassistant/components/homekit/const.py | 4 +- .../components/homekit/type_covers.py | 67 +++++++++- .../homekit/test_get_accessories.py | 7 ++ tests/components/homekit/test_type_covers.py | 117 +++++++++++++++++- 5 files changed, 189 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 306f399092a..24c6dfa8a76 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -101,6 +101,8 @@ def get_accessory(hass, state, aid, config): a_type = 'GarageDoorOpener' elif features & SUPPORT_SET_POSITION: a_type = 'WindowCovering' + elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): + a_type = 'WindowCoveringBasic' elif state.domain == 'light': a_type = 'Light' diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 79466cd9ff0..1c498b4b3b9 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -52,7 +52,8 @@ SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' -SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition +SERV_WINDOW_COVERING = 'WindowCovering' +# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### @@ -85,6 +86,7 @@ CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean +CHAR_POSITION_STATE = 'PositionState' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 9c852bb4d86..8ec715e0e01 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -2,15 +2,17 @@ import logging from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN) + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED) + ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, + SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, + ATTR_SUPPORTED_FEATURES) from . import TYPES from .accessories import HomeAccessory, add_preload_service, setup_char from .const import ( CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, - CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, + CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) @@ -96,3 +98,62 @@ class WindowCovering(HomeAccessory): abs(current_position - self.homekit_target) < 6: self.char_target_position.set_value(current_position) self.homekit_target = None + + +@TYPES.register('WindowCoveringBasic') +class WindowCoveringBasic(HomeAccessory): + """Generate a Window accessory for a cover entity. + + The cover entity must support: open_cover, close_cover, + stop_cover (optional). + """ + + def __init__(self, *args, config): + """Initialize a WindowCovering accessory object.""" + super().__init__(*args, category=CATEGORY_WINDOW_COVERING) + features = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + self.supports_stop = features & SUPPORT_STOP + + serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) + self.char_current_position = setup_char( + CHAR_CURRENT_POSITION, serv_cover, value=0) + self.char_target_position = setup_char( + CHAR_TARGET_POSITION, serv_cover, value=0, + callback=self.move_cover) + self.char_position_state = setup_char( + CHAR_POSITION_STATE, serv_cover, value=2) + + def move_cover(self, value): + """Move cover to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set position to %d', self.entity_id, value) + + if self.supports_stop: + if value > 70: + service, position = (SERVICE_OPEN_COVER, 100) + elif value < 30: + service, position = (SERVICE_CLOSE_COVER, 0) + else: + service, position = (SERVICE_STOP_COVER, 50) + else: + if value >= 50: + service, position = (SERVICE_OPEN_COVER, 100) + else: + service, position = (SERVICE_CLOSE_COVER, 0) + + self.hass.services.call(DOMAIN, service, + {ATTR_ENTITY_ID: self.entity_id}) + + # Snap the current/target position to the expected final position. + self.char_current_position.set_value(position) + self.char_target_position.set_value(position) + self.char_position_state.set_value(2) + + def update_state(self, new_state): + """Update cover position after state changed.""" + position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0} + hk_position = position_mapping.get(new_state.state) + if hk_position is not None: + self.char_current_position.set_value(hk_position) + self.char_target_position.set_value(hk_position) + self.char_position_state.set_value(2) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 8333f1fb893..c26982e170b 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -154,6 +154,13 @@ class TestGetAccessories(unittest.TestCase): {ATTR_SUPPORTED_FEATURES: 4}) get_accessory(None, state, 2, {}) + def test_cover_open_close(self): + """Test cover with support for open and close.""" + with patch.dict(TYPES, {'WindowCoveringBasic': self.mock_type}): + state = State('cover.open_window', 'open', + {ATTR_SUPPORTED_FEATURES: 3}) + get_accessory(None, state, 2, {}) + def test_alarm_control_panel(self): """Test alarm control panel.""" config = {ATTR_CODE: '1234'} diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index f9889b1bdd8..2dcb48a4d4c 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -3,12 +3,13 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_CURRENT_POSITION) + ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) from homeassistant.components.homekit.type_covers import ( - GarageDoorOpener, WindowCovering) + GarageDoorOpener, WindowCovering, WindowCoveringBasic) from homeassistant.const import ( STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, - ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE) + ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + ATTR_SUPPORTED_FEATURES) from tests.common import get_test_home_assistant @@ -132,9 +133,117 @@ class TestHomekitSensors(unittest.TestCase): acc.char_target_position.client_update_value(75) self.hass.block_till_done() self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_cover_position') + self.events[1].data[ATTR_SERVICE], 'set_cover_position') self.assertEqual( self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75) self.assertEqual(acc.char_current_position.value, 50) self.assertEqual(acc.char_target_position.value, 75) + + def test_window_open_close(self): + """Test if accessory and HA are updated accordingly.""" + window_cover = 'cover.window' + + self.hass.states.set(window_cover, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, + config=None) + acc.run() + + self.assertEqual(acc.aid, 2) + self.assertEqual(acc.category, 14) # WindowCovering + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 2) + + self.hass.states.set(window_cover, STATE_UNKNOWN) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 2) + + self.hass.states.set(window_cover, STATE_OPEN) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_position.value, 100) + self.assertEqual(acc.char_target_position.value, 100) + self.assertEqual(acc.char_position_state.value, 2) + + self.hass.states.set(window_cover, STATE_CLOSED) + self.hass.block_till_done() + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 2) + + # Set from HomeKit + acc.char_target_position.client_update_value(25) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'close_cover') + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 2) + + # Set from HomeKit + acc.char_target_position.client_update_value(90) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'open_cover') + + self.assertEqual(acc.char_current_position.value, 100) + self.assertEqual(acc.char_target_position.value, 100) + self.assertEqual(acc.char_position_state.value, 2) + + # Set from HomeKit + acc.char_target_position.client_update_value(55) + self.hass.block_till_done() + self.assertEqual( + self.events[2].data[ATTR_SERVICE], 'open_cover') + + self.assertEqual(acc.char_current_position.value, 100) + self.assertEqual(acc.char_target_position.value, 100) + self.assertEqual(acc.char_position_state.value, 2) + + def test_window_open_close_stop(self): + """Test if accessory and HA are updated accordingly.""" + window_cover = 'cover.window' + + self.hass.states.set(window_cover, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) + acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, + config=None) + acc.run() + + # Set from HomeKit + acc.char_target_position.client_update_value(25) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'close_cover') + + self.assertEqual(acc.char_current_position.value, 0) + self.assertEqual(acc.char_target_position.value, 0) + self.assertEqual(acc.char_position_state.value, 2) + + # Set from HomeKit + acc.char_target_position.client_update_value(90) + self.hass.block_till_done() + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'open_cover') + + self.assertEqual(acc.char_current_position.value, 100) + self.assertEqual(acc.char_target_position.value, 100) + self.assertEqual(acc.char_position_state.value, 2) + + # Set from HomeKit + acc.char_target_position.client_update_value(55) + self.hass.block_till_done() + self.assertEqual( + self.events[2].data[ATTR_SERVICE], 'stop_cover') + + self.assertEqual(acc.char_current_position.value, 50) + self.assertEqual(acc.char_target_position.value, 50) + self.assertEqual(acc.char_position_state.value, 2) From 7d43ad6a37ef80c06890933113f5a9eace46a938 Mon Sep 17 00:00:00 2001 From: Ben Randall Date: Wed, 18 Apr 2018 07:18:44 -0700 Subject: [PATCH 434/924] Colorlog windows fix (#13929) * Fix colorlog on windows Modified the way logging is initialized to fix two things. 1. If the import of `colorlog` fails the logs will still be formatted using the expected HASS log format. 2. Ensure that `logging.basicConfig` is called AFTER `colorlog` is imported so that the default handler generated will be writing to the wrapped stream generated when `colorama` is initialized. This allows colored logging to work on Windows. Added support for a `--log-no-color` command line switch in the event that someone just wants to disable colored log output entirely. * Fix line lengths * Switch default value --- homeassistant/__main__.py | 9 ++- homeassistant/bootstrap.py | 76 +++++++++++++++---------- homeassistant/components/notify/xmpp.py | 2 - 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index aa966027922..deb1746c167 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -126,6 +126,10 @@ def get_arguments() -> argparse.Namespace: default=None, help='Log file to write to. If not set, CONFIG/home-assistant.log ' 'is used') + parser.add_argument( + '--log-no-color', + action='store_true', + help="Disable color logs") parser.add_argument( '--runner', action='store_true', @@ -259,13 +263,14 @@ def setup_and_run_hass(config_dir: str, hass = bootstrap.from_config_dict( config, config_dir=config_dir, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, - log_file=args.log_file) + log_file=args.log_file, log_no_color=args.log_no_color) else: config_file = ensure_config_file(config_dir) print('Config directory:', config_dir) hass = bootstrap.from_config_file( config_file, verbose=args.verbose, skip_pip=args.skip_pip, - log_rotate_days=args.log_rotate_days, log_file=args.log_file) + log_rotate_days=args.log_rotate_days, log_file=args.log_file, + log_no_color=args.log_no_color) if hass is None: return None diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 00822d93299..e0962568a66 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -42,7 +42,8 @@ def from_config_dict(config: Dict[str, Any], verbose: bool = False, skip_pip: bool = False, log_rotate_days: Any = None, - log_file: Any = None) \ + log_file: Any = None, + log_no_color: bool = False) \ -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -60,7 +61,7 @@ def from_config_dict(config: Dict[str, Any], hass = hass.loop.run_until_complete( async_from_config_dict( config, hass, config_dir, enable_log, verbose, skip_pip, - log_rotate_days, log_file) + log_rotate_days, log_file, log_no_color) ) return hass @@ -74,7 +75,8 @@ def async_from_config_dict(config: Dict[str, Any], verbose: bool = False, skip_pip: bool = False, log_rotate_days: Any = None, - log_file: Any = None) \ + log_file: Any = None, + log_no_color: bool = False) \ -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -84,7 +86,8 @@ def async_from_config_dict(config: Dict[str, Any], start = time() if enable_log: - async_enable_logging(hass, verbose, log_rotate_days, log_file) + async_enable_logging(hass, verbose, log_rotate_days, log_file, + log_no_color) core_config = config.get(core.DOMAIN, {}) @@ -164,7 +167,8 @@ def from_config_file(config_path: str, verbose: bool = False, skip_pip: bool = True, log_rotate_days: Any = None, - log_file: Any = None): + log_file: Any = None, + log_no_color: bool = False): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter if given, @@ -176,7 +180,8 @@ def from_config_file(config_path: str, # run task hass = hass.loop.run_until_complete( async_from_config_file( - config_path, hass, verbose, skip_pip, log_rotate_days, log_file) + config_path, hass, verbose, skip_pip, + log_rotate_days, log_file, log_no_color) ) return hass @@ -188,7 +193,8 @@ def async_from_config_file(config_path: str, verbose: bool = False, skip_pip: bool = True, log_rotate_days: Any = None, - log_file: Any = None): + log_file: Any = None, + log_no_color: bool = False): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter. @@ -199,7 +205,8 @@ def async_from_config_file(config_path: str, hass.config.config_dir = config_dir yield from async_mount_local_lib_path(config_dir, hass.loop) - async_enable_logging(hass, verbose, log_rotate_days, log_file) + async_enable_logging(hass, verbose, log_rotate_days, log_file, + log_no_color) try: config_dict = yield from hass.async_add_job( @@ -216,40 +223,51 @@ def async_from_config_file(config_path: str, @core.callback -def async_enable_logging(hass: core.HomeAssistant, verbose: bool = False, - log_rotate_days=None, log_file=None) -> None: +def async_enable_logging(hass: core.HomeAssistant, + verbose: bool = False, + log_rotate_days=None, + log_file=None, + log_no_color: bool = False) -> None: """Set up the logging. This method must be run in the event loop. """ - logging.basicConfig(level=logging.INFO) fmt = ("%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s") - colorfmt = "%(log_color)s{}%(reset)s".format(fmt) datefmt = '%Y-%m-%d %H:%M:%S' + if not log_no_color: + try: + from colorlog import ColoredFormatter + # basicConfig must be called after importing colorlog in order to + # ensure that the handlers it sets up wraps the correct streams. + logging.basicConfig(level=logging.INFO) + + colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + logging.getLogger().handlers[0].setFormatter(ColoredFormatter( + colorfmt, + datefmt=datefmt, + reset=True, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red', + } + )) + except ImportError: + pass + + # If the above initialization failed for any reason, setup the default + # formatting. If the above succeeds, this wil result in a no-op. + logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO) + # Suppress overly verbose logs from libraries that aren't helpful logging.getLogger('requests').setLevel(logging.WARNING) logging.getLogger('urllib3').setLevel(logging.WARNING) logging.getLogger('aiohttp.access').setLevel(logging.WARNING) - try: - from colorlog import ColoredFormatter - logging.getLogger().handlers[0].setFormatter(ColoredFormatter( - colorfmt, - datefmt=datefmt, - reset=True, - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', - } - )) - except ImportError: - pass - # Log errors to a file if we have write access to file or config dir if log_file is None: err_log_path = hass.config.path(ERROR_LOG_FILENAME) diff --git a/homeassistant/components/notify/xmpp.py b/homeassistant/components/notify/xmpp.py index 806acdb6d09..12ddf49fca8 100644 --- a/homeassistant/components/notify/xmpp.py +++ b/homeassistant/components/notify/xmpp.py @@ -76,8 +76,6 @@ def send_message(sender, password, recipient, use_tls, """Initialize the Jabber Bot.""" super(SendNotificationBot, self).__init__(sender, password) - logging.basicConfig(level=logging.ERROR) - self.use_tls = use_tls self.use_ipv6 = False self.add_event_handler('failed_auth', self.check_credentials) From c5cb28d41fcdf1b8cf4b7c8c852a5606ca8351da Mon Sep 17 00:00:00 2001 From: Kane610 Date: Wed, 18 Apr 2018 16:27:44 +0200 Subject: [PATCH 435/924] deCONZ migrate setup fully to config entry (#13679) * Initial working config entry with discovery * No need for else * Make sure that imported config doesnt exist as a config entry * Improve checks to make sure there is only instance of deconz * Fix tests and add new tests * Follow upstream changes Fix case when discovery started ongoing config entry and user completes setup from other path it was possible to complete discovered config entry as well * Add test to make sure link doesn't bypass any check for only allowing one config entry * Dont use len to determine an empty sequence * Cleanup * Allways get bridgeid to use as unique identifier for bridge --- .../components/deconz/.translations/en.json | 1 + homeassistant/components/deconz/__init__.py | 206 +++------------- .../components/deconz/config_flow.py | 139 +++++++++++ homeassistant/components/deconz/const.py | 8 + homeassistant/components/deconz/strings.json | 1 + homeassistant/components/discovery.py | 2 +- tests/components/deconz/__init__.py | 1 + tests/components/deconz/test_config_flow.py | 225 ++++++++++++++++++ tests/components/deconz/test_init.py | 69 ++++++ tests/components/test_deconz.py | 97 -------- 10 files changed, 481 insertions(+), 268 deletions(-) create mode 100644 homeassistant/components/deconz/config_flow.py create mode 100644 homeassistant/components/deconz/const.py create mode 100644 tests/components/deconz/__init__.py create mode 100644 tests/components/deconz/test_config_flow.py create mode 100644 tests/components/deconz/test_init.py delete mode 100644 tests/components/test_deconz.py diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 69165dbbbaf..7ea68af01c1 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -18,6 +18,7 @@ "no_key": "Couldn't get an API key" }, "abort": { + "already_configured": "Bridge is already configured", "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 0cf96576223..064725eda95 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,29 +4,21 @@ Support for deCONZ devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/deconz/ """ -import logging - import voluptuous as vol -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery, aiohttp_client -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util.json import load_json, save_json +from homeassistant.helpers import ( + aiohttp_client, discovery, config_validation as cv) +from homeassistant.util.json import load_json + +# Loading the config flow file will register the flow +from .config_flow import configured_hosts +from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER REQUIREMENTS = ['pydeconz==36'] -_LOGGER = logging.getLogger(__name__) - -DOMAIN = 'deconz' -DATA_DECONZ_ID = 'deconz_entities' - -CONFIG_FILE = 'deconz.conf' - CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_API_KEY): cv.string, @@ -46,46 +38,38 @@ SERVICE_SCHEMA = vol.Schema({ }) -CONFIG_INSTRUCTIONS = """ -Unlock your deCONZ gateway to register with Home Assistant. - -1. [Go to deCONZ system settings](http://{}:{}/edit_system.html) -2. Press "Unlock Gateway" button - -[deCONZ platform documentation](https://home-assistant.io/components/deconz/) -""" - - async def async_setup(hass, config): - """Set up services and configuration for deCONZ component.""" - result = False - config_file = await hass.async_add_job( - load_json, hass.config.path(CONFIG_FILE)) - - async def async_deconz_discovered(service, discovery_info): - """Call when deCONZ gateway has been found.""" - deconz_config = {} - deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) - deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - await async_request_configuration(hass, config, deconz_config) - - if config_file: - result = await async_setup_deconz(hass, config, config_file) - - if not result and DOMAIN in config and CONF_HOST in config[DOMAIN]: - deconz_config = config[DOMAIN] - if CONF_API_KEY in deconz_config: - result = await async_setup_deconz(hass, config, deconz_config) - else: - await async_request_configuration(hass, config, deconz_config) - return True - - if not result: - discovery.async_listen(hass, SERVICE_DECONZ, async_deconz_discovered) + """Load configuration for deCONZ component. + Discovery has loaded the component if DOMAIN is not present in config. + """ + if DOMAIN in config: + deconz_config = None + config_file = await hass.async_add_job( + load_json, hass.config.path(CONFIG_FILE)) + if config_file: + deconz_config = config_file + elif CONF_HOST in config[DOMAIN]: + deconz_config = config[DOMAIN] + if deconz_config and not configured_hosts(hass): + hass.async_add_job(hass.config_entries.flow.async_init( + DOMAIN, source='import', data=deconz_config + )) return True +async def async_setup_entry(hass, entry): + """Set up a deCONZ bridge for a config entry.""" + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + result = await async_setup_deconz(hass, None, entry.data) + if result: + return True + return False + + async def async_setup_deconz(hass, config, deconz_config): """Set up a deCONZ session. @@ -94,8 +78,8 @@ async def async_setup_deconz(hass, config, deconz_config): """ _LOGGER.debug("deCONZ config %s", deconz_config) from pydeconz import DeconzSession - websession = async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, websession, **deconz_config) + session = aiohttp_client.async_get_clientsession(hass) + deconz = DeconzSession(hass.loop, session, **deconz_config) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -152,121 +136,3 @@ async def async_setup_deconz(hass, config, deconz_config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown) return True - - -async def async_request_configuration(hass, config, deconz_config): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - - async def async_configuration_callback(data): - """Set up actions to do when our configuration callback is called.""" - from pydeconz.utils import async_get_api_key - websession = async_get_clientsession(hass) - api_key = await async_get_api_key(websession, **deconz_config) - if api_key: - deconz_config[CONF_API_KEY] = api_key - result = await async_setup_deconz(hass, config, deconz_config) - if result: - await hass.async_add_job( - save_json, hass.config.path(CONFIG_FILE), deconz_config) - configurator.async_request_done(request_id) - return - else: - configurator.async_notify_errors( - request_id, "Couldn't load configuration.") - else: - configurator.async_notify_errors( - request_id, "Couldn't get an API key.") - return - - instructions = CONFIG_INSTRUCTIONS.format( - deconz_config[CONF_HOST], deconz_config[CONF_PORT]) - - request_id = configurator.async_request_config( - "deCONZ", async_configuration_callback, - description=instructions, - entity_picture="/static/images/logo_deconz.jpeg", - submit_caption="I have unlocked the gateway", - ) - - -@config_entries.HANDLERS.register(DOMAIN) -class DeconzFlowHandler(data_entry_flow.FlowHandler): - """Handle a deCONZ config flow.""" - - VERSION = 1 - - def __init__(self): - """Initialize the deCONZ flow.""" - self.bridges = [] - self.deconz_config = {} - - async def async_step_init(self, user_input=None): - """Handle a flow start.""" - from pydeconz.utils import async_discovery - - if DOMAIN in self.hass.data: - return self.async_abort( - reason='one_instance_only' - ) - - if user_input is not None: - for bridge in self.bridges: - if bridge[CONF_HOST] == user_input[CONF_HOST]: - self.deconz_config = bridge - return await self.async_step_link() - - session = aiohttp_client.async_get_clientsession(self.hass) - self.bridges = await async_discovery(session) - - if len(self.bridges) == 1: - self.deconz_config = self.bridges[0] - return await self.async_step_link() - elif len(self.bridges) > 1: - hosts = [] - for bridge in self.bridges: - hosts.append(bridge[CONF_HOST]) - return self.async_show_form( - step_id='init', - data_schema=vol.Schema({ - vol.Required(CONF_HOST): vol.In(hosts) - }) - ) - - return self.async_abort( - reason='no_bridges' - ) - - async def async_step_link(self, user_input=None): - """Attempt to link with the deCONZ bridge.""" - from pydeconz.utils import async_get_api_key - errors = {} - - if user_input is not None: - session = aiohttp_client.async_get_clientsession(self.hass) - api_key = await async_get_api_key(session, **self.deconz_config) - if api_key: - self.deconz_config[CONF_API_KEY] = api_key - return self.async_create_entry( - title='deCONZ', - data=self.deconz_config - ) - else: - errors['base'] = 'no_key' - - return self.async_show_form( - step_id='link', - errors=errors, - ) - - -async def async_setup_entry(hass, entry): - """Set up a bridge for a config entry.""" - if DOMAIN in hass.data: - _LOGGER.error( - "Config entry failed since one deCONZ instance already exists") - return False - result = await async_setup_deconz(hass, None, entry.data) - if result: - return True - return False diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py new file mode 100644 index 00000000000..e900782ea65 --- /dev/null +++ b/homeassistant/components/deconz/config_flow.py @@ -0,0 +1,139 @@ +"""Config flow to configure deCONZ component.""" + +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.core import callback +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.helpers import aiohttp_client +from homeassistant.util.json import load_json + +from .const import CONFIG_FILE, DOMAIN + + +@callback +def configured_hosts(hass): + """Return a set of the configured hosts.""" + return set(entry.data['host'] for entry + in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class DeconzFlowHandler(data_entry_flow.FlowHandler): + """Handle a deCONZ config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the deCONZ config flow.""" + self.bridges = [] + self.deconz_config = {} + + async def async_step_init(self, user_input=None): + """Handle a deCONZ config flow start.""" + from pydeconz.utils import async_discovery + + if configured_hosts(self.hass): + return self.async_abort(reason='one_instance_only') + + if user_input is not None: + for bridge in self.bridges: + if bridge[CONF_HOST] == user_input[CONF_HOST]: + self.deconz_config = bridge + return await self.async_step_link() + + session = aiohttp_client.async_get_clientsession(self.hass) + self.bridges = await async_discovery(session) + + if len(self.bridges) == 1: + self.deconz_config = self.bridges[0] + return await self.async_step_link() + elif len(self.bridges) > 1: + hosts = [] + for bridge in self.bridges: + hosts.append(bridge[CONF_HOST]) + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(CONF_HOST): vol.In(hosts) + }) + ) + + return self.async_abort( + reason='no_bridges' + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the deCONZ bridge.""" + from pydeconz.utils import async_get_api_key, async_get_bridgeid + errors = {} + + if user_input is not None: + if configured_hosts(self.hass): + return self.async_abort(reason='one_instance_only') + session = aiohttp_client.async_get_clientsession(self.hass) + api_key = await async_get_api_key(session, **self.deconz_config) + if api_key: + self.deconz_config[CONF_API_KEY] = api_key + if 'bridgeid' not in self.deconz_config: + self.deconz_config['bridgeid'] = await async_get_bridgeid( + session, **self.deconz_config) + return self.async_create_entry( + title='deCONZ-' + self.deconz_config['bridgeid'], + data=self.deconz_config + ) + errors['base'] = 'no_key' + + return self.async_show_form( + step_id='link', + errors=errors, + ) + + async def async_step_discovery(self, discovery_info): + """Prepare configuration for a discovered deCONZ bridge. + + This flow is triggered by the discovery component. + """ + deconz_config = {} + deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) + deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) + deconz_config['bridgeid'] = discovery_info.get('serial') + + config_file = await self.hass.async_add_job( + load_json, self.hass.config.path(CONFIG_FILE)) + if config_file and \ + config_file[CONF_HOST] == deconz_config[CONF_HOST] and \ + CONF_API_KEY in config_file: + deconz_config[CONF_API_KEY] = config_file[CONF_API_KEY] + + return await self.async_step_import(deconz_config) + + async def async_step_import(self, import_config): + """Import a deCONZ bridge as a config entry. + + This flow is triggered by `async_setup` for configured bridges. + This flow is also triggered by `async_step_discovery`. + + This will execute for any bridge that does not have a + config entry yet (based on host). + + If an API key is provided, we will create an entry. + Otherwise we will delegate to `link` step which + will ask user to link the bridge. + """ + from pydeconz.utils import async_get_bridgeid + + if configured_hosts(self.hass): + return self.async_abort(reason='one_instance_only') + elif CONF_API_KEY not in import_config: + self.deconz_config = import_config + return await self.async_step_link() + + if 'bridgeid' not in import_config: + session = aiohttp_client.async_get_clientsession(self.hass) + import_config['bridgeid'] = await async_get_bridgeid( + session, **import_config) + return self.async_create_entry( + title='deCONZ-' + import_config['bridgeid'], + data=import_config + ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py new file mode 100644 index 00000000000..c5820c971f6 --- /dev/null +++ b/homeassistant/components/deconz/const.py @@ -0,0 +1,8 @@ +"""Constants for the deCONZ component.""" +import logging + +_LOGGER = logging.getLogger('homeassistant.components.deconz') + +DOMAIN = 'deconz' +CONFIG_FILE = 'deconz.conf' +DATA_DECONZ_ID = 'deconz_entities' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 69165dbbbaf..7ea68af01c1 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -18,6 +18,7 @@ "no_key": "Couldn't get an API key" }, "abort": { + "already_configured": "Bridge is already configured", "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" } diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 31ec3f2f60a..f0ebcba8366 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -43,6 +43,7 @@ SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' CONFIG_ENTRY_HANDLERS = { + SERVICE_DECONZ: 'deconz', SERVICE_HUE: 'hue', } @@ -57,7 +58,6 @@ SERVICE_HANDLERS = { SERVICE_WINK: ('wink', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), - SERVICE_DECONZ: ('deconz', None), SERVICE_DAIKIN: ('daikin', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), diff --git a/tests/components/deconz/__init__.py b/tests/components/deconz/__init__.py new file mode 100644 index 00000000000..59b903e8900 --- /dev/null +++ b/tests/components/deconz/__init__.py @@ -0,0 +1 @@ +"""Tests for the deCONZ component.""" diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py new file mode 100644 index 00000000000..d86475b35ef --- /dev/null +++ b/tests/components/deconz/test_config_flow.py @@ -0,0 +1,225 @@ +"""Tests for deCONZ config flow.""" +from unittest.mock import patch +import pytest + +import voluptuous as vol +from homeassistant.components.deconz import config_flow +from tests.common import MockConfigEntry + +import pydeconz + + +async def test_flow_works(hass, aioclient_mock): + """Test that config flow works.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': 80} + ]) + aioclient_mock.post('http://1.2.3.4:80/api', json=[ + {"success": {"username": "1234567890ABCDEF"}} + ]) + + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + await flow.async_step_init() + result = await flow.async_step_link(user_input={}) + + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF' + } + + +async def test_flow_already_registered_bridge(hass): + """Test config flow don't allow more than one bridge to be registered.""" + MockConfigEntry(domain='deconz', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[]) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': 80} + ]) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': 80}, + {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': 80} + ]) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_link_no_api_key(hass, aioclient_mock): + """Test config flow should abort if no API key was possible to retrieve.""" + aioclient_mock.post('http://1.2.3.4:80/api', json=[]) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', 'port': 80} + + result = await flow.async_step_link(user_input={}) + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'no_key'} + + +async def test_link_already_registered_bridge(hass): + """Test that link verifies to only allow one config entry to complete. + + This is possible with discovery which will allow the user to complete + a second config entry and then complete the discovered config entry. + """ + MockConfigEntry(domain='deconz', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', 'port': 80} + + result = await flow.async_step_link(user_input={}) + assert result['type'] == 'abort' + + +async def test_bridge_discovery(hass): + """Test a bridge being discovered with no additional config file.""" + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + with patch.object(config_flow, 'load_json', return_value={}): + result = await flow.async_step_discovery({ + 'host': '1.2.3.4', + 'port': 80, + 'serial': 'id' + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_bridge_discovery_config_file(hass): + """Test a bridge being discovered with a corresponding config file.""" + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + with patch.object(config_flow, 'load_json', + return_value={'host': '1.2.3.4', + 'port': 8080, + 'api_key': '1234567890ABCDEF'}): + result = await flow.async_step_discovery({ + 'host': '1.2.3.4', + 'port': 80, + 'serial': 'id' + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF' + } + + +async def test_bridge_discovery_other_config_file(hass): + """Test a bridge being discovered with another bridges config file.""" + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + with patch.object(config_flow, 'load_json', + return_value={'host': '5.6.7.8', 'api_key': '5678'}): + result = await flow.async_step_discovery({ + 'host': '1.2.3.4', + 'port': 80, + 'serial': 'id' + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_bridge_discovery_already_configured(hass): + """Test if a discovered bridge has already been configured.""" + MockConfigEntry(domain='deconz', data={ + 'host': '1.2.3.4' + }).add_to_hass(hass) + + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_discovery({ + 'host': '1.2.3.4', + 'serial': 'id' + }) + + assert result['type'] == 'abort' + + +async def test_import_without_api_key(hass): + """Test importing a host without an API key.""" + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_import({ + 'host': '1.2.3.4', + }) + + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_import_with_api_key(hass): + """Test importing a host with an API key.""" + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_import({ + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF' + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF' + } diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py new file mode 100644 index 00000000000..cbc8a373972 --- /dev/null +++ b/tests/components/deconz/test_init.py @@ -0,0 +1,69 @@ +"""Test deCONZ component setup process.""" +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import deconz + + +async def test_config_with_host_passed_to_config_entry(hass): + """Test that configured options for a host are loaded via config entry.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(deconz, 'configured_hosts', return_value=[]), \ + patch.object(deconz, 'load_json', return_value={}): + assert await async_setup_component(hass, deconz.DOMAIN, { + deconz.DOMAIN: { + deconz.CONF_HOST: '1.2.3.4', + deconz.CONF_PORT: 80 + } + }) is True + # Import flow started + assert len(mock_config_entries.flow.mock_calls) == 2 + + +async def test_config_file_passed_to_config_entry(hass): + """Test that configuration file for a host are loaded via config entry.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(deconz, 'configured_hosts', return_value=[]), \ + patch.object(deconz, 'load_json', + return_value={'host': '1.2.3.4'}): + assert await async_setup_component(hass, deconz.DOMAIN, { + deconz.DOMAIN: {} + }) is True + # Import flow started + assert len(mock_config_entries.flow.mock_calls) == 2 + + +async def test_config_without_host_not_passed_to_config_entry(hass): + """Test that a configuration without a host does not initiate an import.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(deconz, 'configured_hosts', return_value=[]), \ + patch.object(deconz, 'load_json', return_value={}): + assert await async_setup_component(hass, deconz.DOMAIN, { + deconz.DOMAIN: {} + }) is True + # No flow started + assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_config_already_registered_not_passed_to_config_entry(hass): + """Test that an already registered host does not initiate an import.""" + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(deconz, 'configured_hosts', + return_value=['1.2.3.4']), \ + patch.object(deconz, 'load_json', return_value={}): + assert await async_setup_component(hass, deconz.DOMAIN, { + deconz.DOMAIN: { + deconz.CONF_HOST: '1.2.3.4', + deconz.CONF_PORT: 80 + } + }) is True + # No flow started + assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_config_discovery(hass): + """Test that a discovered bridge does not initiate an import.""" + with patch.object(hass, 'config_entries') as mock_config_entries: + assert await async_setup_component(hass, deconz.DOMAIN, {}) is True + # No flow started + assert len(mock_config_entries.flow.mock_calls) == 0 diff --git a/tests/components/test_deconz.py b/tests/components/test_deconz.py deleted file mode 100644 index 2c7c656d560..00000000000 --- a/tests/components/test_deconz.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Tests for deCONZ config flow.""" -import pytest - -import voluptuous as vol - -import homeassistant.components.deconz as deconz -import pydeconz - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow.""" - aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ - {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} - ]) - aioclient_mock.post('http://1.2.3.4:80/api', json=[ - {"success": {"username": "1234567890ABCDEF"}} - ]) - - flow = deconz.DeconzFlowHandler() - flow.hass = hass - await flow.async_step_init() - result = await flow.async_step_link(user_input={}) - - assert result['type'] == 'create_entry' - assert result['title'] == 'deCONZ' - assert result['data'] == { - 'bridgeid': 'id', - 'host': '1.2.3.4', - 'port': '80', - 'api_key': '1234567890ABCDEF' - } - - -async def test_flow_already_registered_bridge(hass, aioclient_mock): - """Test config flow don't allow more than one bridge to be registered.""" - flow = deconz.DeconzFlowHandler() - flow.hass = hass - flow.hass.data[deconz.DOMAIN] = True - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_no_discovered_bridges(hass, aioclient_mock): - """Test config flow discovers no bridges.""" - aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[]) - flow = deconz.DeconzFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'abort' - - -async def test_flow_one_bridge_discovered(hass, aioclient_mock): - """Test config flow discovers one bridge.""" - aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ - {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} - ]) - flow = deconz.DeconzFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'link' - - -async def test_flow_two_bridges_discovered(hass, aioclient_mock): - """Test config flow discovers two bridges.""" - aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ - {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': '80'}, - {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': '80'} - ]) - flow = deconz.DeconzFlowHandler() - flow.hass = hass - - result = await flow.async_step_init() - assert result['type'] == 'form' - assert result['step_id'] == 'init' - - with pytest.raises(vol.Invalid): - assert result['data_schema']({'host': '0.0.0.0'}) - - result['data_schema']({'host': '1.2.3.4'}) - result['data_schema']({'host': '5.6.7.8'}) - - -async def test_flow_no_api_key(hass, aioclient_mock): - """Test config flow discovers no bridges.""" - aioclient_mock.post('http://1.2.3.4:80/api', json=[]) - flow = deconz.DeconzFlowHandler() - flow.hass = hass - flow.deconz_config = {'host': '1.2.3.4', 'port': 80} - - result = await flow.async_step_link(user_input={}) - assert result['type'] == 'form' - assert result['step_id'] == 'link' - assert result['errors'] == {'base': 'no_key'} From 0eb3e49880f87408bcaee77ba5771ce98ba3de4d Mon Sep 17 00:00:00 2001 From: Michael Wei Date: Wed, 18 Apr 2018 11:19:05 -0700 Subject: [PATCH 436/924] Alexa thermostat fails to properly parse 'value' field for climate (#13958) * Fix thermostat payload issue * fix test payload * style issue * handle both string and value object --- homeassistant/components/alexa/smart_home.py | 1 + tests/components/alexa/test_smart_home.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 707f8d02958..c5c68f1af40 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -1471,6 +1471,7 @@ async def async_api_adjust_target_temp(hass, config, request, entity): async def async_api_set_thermostat_mode(hass, config, request, entity): """Process a set thermostat mode request.""" mode = request[API_PAYLOAD]['thermostatMode'] + mode = mode if isinstance(mode, str) else mode['value'] operation_list = entity.attributes.get(climate.ATTR_OPERATION_LIST) # Work around a pylint false positive due to diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index dd404b7d57a..afa4d19b5d9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -807,15 +807,23 @@ async def test_thermostat(hass): 'Alexa.ThermostatController', 'SetThermostatMode', 'climate#test_thermostat', 'climate.set_operation_mode', hass, - payload={'thermostatMode': 'HEAT'} + payload={'thermostatMode': {'value': 'HEAT'}} ) assert call.data['operation_mode'] == 'heat' + call, _ = await assert_request_calls_service( + 'Alexa.ThermostatController', 'SetThermostatMode', + 'climate#test_thermostat', 'climate.set_operation_mode', + hass, + payload={'thermostatMode': 'HEAT'} + ) + + assert call.data['operation_mode'] == 'heat' msg = await assert_request_fails( 'Alexa.ThermostatController', 'SetThermostatMode', 'climate#test_thermostat', 'climate.set_operation_mode', hass, - payload={'thermostatMode': 'INVALID'} + payload={'thermostatMode': {'value': 'INVALID'}} ) assert msg['event']['payload']['type'] == 'UNSUPPORTED_THERMOSTAT_MODE' From 45eb611007d001abb6571ee8abf597fd91eceb13 Mon Sep 17 00:00:00 2001 From: NovapaX Date: Wed, 18 Apr 2018 21:46:44 +0200 Subject: [PATCH 437/924] renaming icons (#13982) * renaming icons * remove mdi:robot-vacuum * fix other vacuums --- homeassistant/components/hdmi_cec.py | 2 +- homeassistant/components/vacuum/__init__.py | 1 - homeassistant/components/vacuum/demo.py | 7 +------ homeassistant/components/vacuum/dyson.py | 8 -------- homeassistant/components/vacuum/mqtt.py | 7 +------ homeassistant/components/vacuum/neato.py | 7 ------- homeassistant/components/vacuum/roomba.py | 6 ------ homeassistant/components/vacuum/xiaomi_miio.py | 7 ------- tests/components/vacuum/test_dyson.py | 1 - 9 files changed, 3 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/hdmi_cec.py b/homeassistant/components/hdmi_cec.py index 8e2464d0922..b5d64f48dc7 100644 --- a/homeassistant/components/hdmi_cec.py +++ b/homeassistant/components/hdmi_cec.py @@ -35,7 +35,7 @@ CONF_TYPES = 'types' ICON_UNKNOWN = 'mdi:help' ICON_AUDIO = 'mdi:speaker' ICON_PLAYER = 'mdi:play' -ICON_TUNER = 'mdi:nest-thermostat' +ICON_TUNER = 'mdi:radio' ICON_RECORDER = 'mdi:microphone' ICON_TV = 'mdi:television' ICONS_BY_TYPE = { diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index d403a776ddf..1b7d5685231 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -76,7 +76,6 @@ SERVICE_TO_METHOD = { } DEFAULT_NAME = 'Vacuum cleaner robot' -DEFAULT_ICON = 'mdi:roomba' SUPPORT_TURN_ON = 1 SUPPORT_TURN_OFF = 2 diff --git a/homeassistant/components/vacuum/demo.py b/homeassistant/components/vacuum/demo.py index 668e3ca37e6..bd501167ffa 100644 --- a/homeassistant/components/vacuum/demo.py +++ b/homeassistant/components/vacuum/demo.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/demo/ import logging from homeassistant.components.vacuum import ( - ATTR_CLEANED_AREA, DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, + ATTR_CLEANED_AREA, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice) @@ -66,11 +66,6 @@ class DemoVacuum(VacuumDevice): """Return the name of the vacuum.""" return self._name - @property - def icon(self): - """Return the icon for the vacuum.""" - return DEFAULT_ICON - @property def should_poll(self): """No polling needed for a demo vacuum.""" diff --git a/homeassistant/components/vacuum/dyson.py b/homeassistant/components/vacuum/dyson.py index aa05d004a35..d423a8dacf5 100644 --- a/homeassistant/components/vacuum/dyson.py +++ b/homeassistant/components/vacuum/dyson.py @@ -24,8 +24,6 @@ DEPENDENCIES = ['dyson'] DYSON_360_EYE_DEVICES = "dyson_360_eye_devices" -ICON = 'mdi:roomba' - SUPPORT_DYSON = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_PAUSE | \ SUPPORT_RETURN_HOME | SUPPORT_FAN_SPEED | SUPPORT_STATUS | \ SUPPORT_BATTERY | SUPPORT_STOP @@ -56,7 +54,6 @@ class Dyson360EyeDevice(VacuumDevice): """Dyson 360 Eye robot vacuum device.""" _LOGGER.debug("Creating device %s", device.name) self._device = device - self._icon = ICON @asyncio.coroutine def async_added_to_hass(self): @@ -82,11 +79,6 @@ class Dyson360EyeDevice(VacuumDevice): """Return the name of the device.""" return self._device.name - @property - def icon(self): - """Return the icon to use for device.""" - return self._icon - @property def status(self): """Return the status of the vacuum cleaner.""" diff --git a/homeassistant/components/vacuum/mqtt.py b/homeassistant/components/vacuum/mqtt.py index f4c640f1fc7..ef3bb0f636b 100644 --- a/homeassistant/components/vacuum/mqtt.py +++ b/homeassistant/components/vacuum/mqtt.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.components.mqtt as mqtt from homeassistant.components.mqtt import MqttAvailability from homeassistant.components.vacuum import ( - DEFAULT_ICON, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, + SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, SUPPORT_STATUS, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, VacuumDevice) @@ -340,11 +340,6 @@ class MqttVacuum(MqttAvailability, VacuumDevice): """Return the name of the vacuum.""" return self._name - @property - def icon(self): - """Return the icon for the vacuum.""" - return DEFAULT_ICON - @property def should_poll(self): """No polling needed for an MQTT vacuum.""" diff --git a/homeassistant/components/vacuum/neato.py b/homeassistant/components/vacuum/neato.py index 2a4eb2d5e7f..9eba34cea32 100644 --- a/homeassistant/components/vacuum/neato.py +++ b/homeassistant/components/vacuum/neato.py @@ -24,8 +24,6 @@ SUPPORT_NEATO = SUPPORT_BATTERY | SUPPORT_PAUSE | SUPPORT_RETURN_HOME | \ SUPPORT_STOP | SUPPORT_TURN_OFF | SUPPORT_TURN_ON | \ SUPPORT_STATUS | SUPPORT_MAP -ICON = 'mdi:roomba' - ATTR_CLEAN_START = 'clean_start' ATTR_CLEAN_STOP = 'clean_stop' ATTR_CLEAN_AREA = 'clean_area' @@ -131,11 +129,6 @@ class NeatoConnectedVacuum(VacuumDevice): """Return the name of the device.""" return self._name - @property - def icon(self): - """Return the icon to use for device.""" - return ICON - @property def supported_features(self): """Flag vacuum cleaner robot features that are supported.""" diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index b983b20bd0c..44d22e03f41 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -43,7 +43,6 @@ DEFAULT_CERT = '/etc/ssl/certs/ca-certificates.crt' DEFAULT_CONTINUOUS = True DEFAULT_NAME = 'Roomba' -ICON = 'mdi:roomba' PLATFORM = 'roomba' FAN_SPEED_AUTOMATIC = 'Automatic' @@ -165,11 +164,6 @@ class RoombaVacuum(VacuumDevice): """Return the name of the device.""" return self._name - @property - def icon(self): - """Return the icon to use for device.""" - return ICON - @property def device_state_attributes(self): """Return the state attributes of the device.""" diff --git a/homeassistant/components/vacuum/xiaomi_miio.py b/homeassistant/components/vacuum/xiaomi_miio.py index b2451ed495c..620014a1bae 100644 --- a/homeassistant/components/vacuum/xiaomi_miio.py +++ b/homeassistant/components/vacuum/xiaomi_miio.py @@ -24,7 +24,6 @@ REQUIREMENTS = ['python-miio==0.3.9', 'construct==2.9.41'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Xiaomi Vacuum cleaner' -ICON = 'mdi:roomba' DATA_KEY = 'vacuum.xiaomi_miio' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -142,7 +141,6 @@ class MiroboVacuum(VacuumDevice): def __init__(self, name, vacuum): """Initialize the Xiaomi vacuum cleaner robot handler.""" self._name = name - self._icon = ICON self._vacuum = vacuum self.vacuum_state = None @@ -158,11 +156,6 @@ class MiroboVacuum(VacuumDevice): """Return the name of the device.""" return self._name - @property - def icon(self): - """Return the icon to use for device.""" - return self._icon - @property def status(self): """Return the status of the vacuum cleaner.""" diff --git a/tests/components/vacuum/test_dyson.py b/tests/components/vacuum/test_dyson.py index 186a2271a73..8a4e6d57b91 100644 --- a/tests/components/vacuum/test_dyson.py +++ b/tests/components/vacuum/test_dyson.py @@ -118,7 +118,6 @@ class DysonTest(unittest.TestCase): component3 = Dyson360EyeDevice(device3) self.assertEqual(component.name, "Device_Vacuum") self.assertTrue(component.is_on) - self.assertEqual(component.icon, "mdi:roomba") self.assertEqual(component.status, "Cleaning") self.assertEqual(component2.status, "Unknown") self.assertEqual(component.battery_level, 85) From b0a3d084fb7d44ab675e634fc36be39243d07587 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Apr 2018 15:58:14 -0400 Subject: [PATCH 438/924] Version bump to 20180418.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 45d16ae5fb6..76403c0b442 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180417.0'] +REQUIREMENTS = ['home-assistant-frontend==20180418.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 8839fb841ae..f1edc22180a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -380,7 +380,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180417.0 +home-assistant-frontend==20180418.0 # homeassistant.components.homekit_controller # homekit==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56a645d4fd9..7db4ead0856 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180417.0 +home-assistant-frontend==20180418.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From ccba858ae183deec8d01110a083f108f0fc67c09 Mon Sep 17 00:00:00 2001 From: Adam Mills Date: Wed, 18 Apr 2018 15:58:47 -0400 Subject: [PATCH 439/924] Fix for Lokalise backend misinterpretation of keys (#13986) The Lokalise server has a bug that the internal portion of key references was misinterpreted as a symfony key, and was getting auto converted by the convert placeholders feature. Since we don't use this we're turning it off to work around the bug. --- .travis.yml | 2 +- script/translations_upload | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index fce86348817..bf2d05bb185 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ script: travis_wait 30 tox --develop services: - docker before_deploy: - - docker pull lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 + - docker pull lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21 deploy: skip_cleanup: true provider: script diff --git a/script/translations_upload b/script/translations_upload index 578cc8c0ccf..5bf9fe1e121 100755 --- a/script/translations_upload +++ b/script/translations_upload @@ -35,9 +35,10 @@ script/translations_upload_merge.py docker run \ -v ${LOCAL_FILE}:/opt/src/${LOCAL_FILE} \ - lokalise/lokalise-cli@sha256:79b3108211ed1fcc9f7b09a011bfc53c240fc2f3b7fa7f0c8390f593271b4cd7 lokalise \ + lokalise/lokalise-cli@sha256:2198814ebddfda56ee041a4b427521757dd57f75415ea9693696a64c550cef21 lokalise \ --token ${LOKALISE_TOKEN} \ import ${PROJECT_ID} \ --file /opt/src/${LOCAL_FILE} \ --lang_iso ${LANG_ISO} \ + --convert_placeholders 0 \ --replace 1 From ba7fccba3472e2b4fe81ec64263e953c4df2431c Mon Sep 17 00:00:00 2001 From: thelittlefireman Date: Wed, 18 Apr 2018 21:59:48 +0200 Subject: [PATCH 440/924] Bump locationsharinglib to 1.2.1 (#13980) * Bump locationsharinglib to 1.2.1 * Bump locationsharinglib to 1.2.1 --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 9e257616361..d1e59293365 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==0.4.0'] +REQUIREMENTS = ['locationsharinglib==1.2.1'] CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' diff --git a/requirements_all.txt b/requirements_all.txt index f1edc22180a..71a32278822 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -497,7 +497,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==0.4.0 +locationsharinglib==1.2.1 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From 674682e88f5e2efef70a7760249dab88c337f212 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 19 Apr 2018 09:11:38 +0200 Subject: [PATCH 441/924] Support for multiple MAX!Cube LAN gateways added (#13517) --- .../components/binary_sensor/maxcube.py | 23 ++++----- homeassistant/components/climate/maxcube.py | 22 ++++----- homeassistant/components/maxcube.py | 49 ++++++++++++++----- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/binary_sensor/maxcube.py b/homeassistant/components/binary_sensor/maxcube.py index 1043004243a..c131de5420a 100644 --- a/homeassistant/components/binary_sensor/maxcube.py +++ b/homeassistant/components/binary_sensor/maxcube.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/maxcube/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.maxcube import MAXCUBE_HANDLE +from homeassistant.components.maxcube import DATA_KEY from homeassistant.const import STATE_UNKNOWN _LOGGER = logging.getLogger(__name__) @@ -15,16 +15,17 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): """Iterate through all MAX! Devices and add window shutters.""" - cube = hass.data[MAXCUBE_HANDLE].cube devices = [] + for handler in hass.data[DATA_KEY].values(): + cube = handler.cube + for device in cube.devices: + name = "{} {}".format( + cube.room_by_id(device.room_id).name, device.name) - for device in cube.devices: - name = "{} {}".format( - cube.room_by_id(device.room_id).name, device.name) - - # Only add Window Shutters - if cube.is_windowshutter(device): - devices.append(MaxCubeShutter(hass, name, device.rf_address)) + # Only add Window Shutters + if cube.is_windowshutter(device): + devices.append( + MaxCubeShutter(handler, name, device.rf_address)) if devices: add_devices(devices) @@ -33,12 +34,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MaxCubeShutter(BinarySensorDevice): """Representation of a MAX! Cube Binary Sensor device.""" - def __init__(self, hass, name, rf_address): + def __init__(self, handler, name, rf_address): """Initialize MAX! Cube BinarySensorDevice.""" self._name = name self._sensor_type = 'window' self._rf_address = rf_address - self._cubehandle = hass.data[MAXCUBE_HANDLE] + self._cubehandle = handler self._state = STATE_UNKNOWN @property diff --git a/homeassistant/components/climate/maxcube.py b/homeassistant/components/climate/maxcube.py index 067d11437b2..712ebb4f4ce 100644 --- a/homeassistant/components/climate/maxcube.py +++ b/homeassistant/components/climate/maxcube.py @@ -10,7 +10,7 @@ import logging from homeassistant.components.climate import ( ClimateDevice, STATE_AUTO, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) -from homeassistant.components.maxcube import MAXCUBE_HANDLE +from homeassistant.components.maxcube import DATA_KEY from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE _LOGGER = logging.getLogger(__name__) @@ -24,16 +24,16 @@ SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE def setup_platform(hass, config, add_devices, discovery_info=None): """Iterate through all MAX! Devices and add thermostats.""" - cube = hass.data[MAXCUBE_HANDLE].cube - devices = [] + for handler in hass.data[DATA_KEY].values(): + cube = handler.cube + for device in cube.devices: + name = '{} {}'.format( + cube.room_by_id(device.room_id).name, device.name) - for device in cube.devices: - name = '{} {}'.format( - cube.room_by_id(device.room_id).name, device.name) - - if cube.is_thermostat(device) or cube.is_wallthermostat(device): - devices.append(MaxCubeClimate(hass, name, device.rf_address)) + if cube.is_thermostat(device) or cube.is_wallthermostat(device): + devices.append( + MaxCubeClimate(handler, name, device.rf_address)) if devices: add_devices(devices) @@ -42,14 +42,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class MaxCubeClimate(ClimateDevice): """MAX! Cube ClimateDevice.""" - def __init__(self, hass, name, rf_address): + def __init__(self, handler, name, rf_address): """Initialize MAX! Cube ClimateDevice.""" self._name = name self._unit_of_measurement = TEMP_CELSIUS self._operation_list = [STATE_AUTO, STATE_MANUAL, STATE_BOOST, STATE_VACATION] self._rf_address = rf_address - self._cubehandle = hass.data[MAXCUBE_HANDLE] + self._cubehandle = handler @property def supported_features(self): diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index a0a8db6ba4d..13d3e0b444b 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -22,12 +22,22 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 62910 DOMAIN = 'maxcube' -MAXCUBE_HANDLE = 'maxcube' +DATA_KEY = 'maxcube' + +NOTIFICATION_ID = 'maxcube_notification' +NOTIFICATION_TITLE = 'Max!Cube gateway setup' + +CONF_GATEWAYS = 'gateways' + +CONFIG_GATEWAY = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, +}) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_GATEWAYS, default={}): + vol.All(cv.ensure_list, [CONFIG_GATEWAY]) }), }, extra=vol.ALLOW_EXTRA) @@ -36,18 +46,33 @@ def setup(hass, config): """Establish connection to MAX! Cube.""" from maxcube.connection import MaxCubeConnection from maxcube.cube import MaxCube + if DATA_KEY not in hass.data: + hass.data[DATA_KEY] = {} - host = config.get(DOMAIN).get(CONF_HOST) - port = config.get(DOMAIN).get(CONF_PORT) - - try: - cube = MaxCube(MaxCubeConnection(host, port)) - except timeout: - _LOGGER.error("Connection to Max!Cube could not be established") - cube = None + if DOMAIN not in config: return False - hass.data[MAXCUBE_HANDLE] = MaxCubeHandle(cube) + connection_failed = 0 + gateways = config[DOMAIN][CONF_GATEWAYS] + for gateway in gateways: + host = gateway[CONF_HOST] + port = gateway[CONF_PORT] + + try: + cube = MaxCube(MaxCubeConnection(host, port)) + hass.data[DATA_KEY][host] = MaxCubeHandle(cube) + except timeout as ex: + _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart Home Assistant after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + connection_failed += 1 + + if connection_failed >= len(gateways): + return False load_platform(hass, 'climate', DOMAIN) load_platform(hass, 'binary_sensor', DOMAIN) From 3dc70436f1937ab6767f8f9dc5bbbe712352990e Mon Sep 17 00:00:00 2001 From: koolsb <14332595+koolsb@users.noreply.github.com> Date: Thu, 19 Apr 2018 04:31:50 -0500 Subject: [PATCH 442/924] Add additional receiver for Onkyo zone 2 (#13551) --- .../components/media_player/onkyo.py | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 432d9ce108f..58703165385 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -22,6 +22,7 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4'] _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' +CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' @@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, + vol.Optional(CONF_ZONE2, default=False): cv.boolean, }) @@ -57,6 +59,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): eiscp.eISCP(host), config.get(CONF_SOURCES), name=config.get(CONF_NAME))) KNOWN_HOSTS.append(host) + + # Add Zone2 if configured + if config.get(CONF_ZONE2): + _LOGGER.debug("Setting up zone 2") + hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host), + config.get(CONF_SOURCES), + name=config.get(CONF_NAME) + + " Zone 2")) except OSError: _LOGGER.error("Unable to connect to receiver at %s", host) else: @@ -98,8 +108,9 @@ class OnkyoDevice(MediaPlayerDevice): return result def update(self): - """Get the latest details from the device.""" + """Get the latest state from the device.""" status = self.command('system-power query') + if not status: return if status[1] == 'on': @@ -107,9 +118,11 @@ class OnkyoDevice(MediaPlayerDevice): else: self._pwstate = STATE_OFF return + volume_raw = self.command('volume query') mute_raw = self.command('audio-muting query') current_source_raw = self.command('input-selector query') + if not (volume_raw and mute_raw and current_source_raw): return @@ -147,12 +160,12 @@ class OnkyoDevice(MediaPlayerDevice): @property def is_volume_muted(self): - """Boolean if volume is currently muted.""" + """Return boolean indicating mute status.""" return self._muted @property def supported_features(self): - """Flag media player features that are supported.""" + """Return media player features that are supported.""" return SUPPORT_ONKYO @property @@ -166,7 +179,7 @@ class OnkyoDevice(MediaPlayerDevice): return self._source_list def turn_off(self): - """Turn off media player.""" + """Turn the media player off.""" self.command('system-power standby') def set_volume_level(self, volume): @@ -189,3 +202,68 @@ class OnkyoDevice(MediaPlayerDevice): if source in self._source_list: source = self._reverse_mapping[source] self.command('input-selector {}'.format(source)) + + +class OnkyoDeviceZone2(OnkyoDevice): + """Representation of an Onkyo device's zone 2.""" + + def update(self): + """Get the latest state from the device.""" + status = self.command('zone2.power=query') + + if not status: + return + if status[1] == 'on': + self._pwstate = STATE_ON + else: + self._pwstate = STATE_OFF + return + + volume_raw = self.command('zone2.volume=query') + mute_raw = self.command('zone2.muting=query') + current_source_raw = self.command('zone2.selector=query') + + if not (volume_raw and mute_raw and current_source_raw): + return + + # eiscp can return string or tuple. Make everything tuples. + if isinstance(current_source_raw[1], str): + current_source_tuples = \ + (current_source_raw[0], (current_source_raw[1],)) + else: + current_source_tuples = current_source_raw + + for source in current_source_tuples[1]: + if source in self._source_mapping: + self._current_source = self._source_mapping[source] + break + else: + self._current_source = '_'.join( + [i for i in current_source_tuples[1]]) + self._muted = bool(mute_raw[1] == 'on') + self._volume = volume_raw[1] / 80.0 + + def turn_off(self): + """Turn the media player off.""" + self.command('zone2.power=standby') + + def set_volume_level(self, volume): + """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" + self.command('zone2.volume={}'.format(int(volume*80))) + + def mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + if mute: + self.command('zone2.muting=on') + else: + self.command('zone2.muting=off') + + def turn_on(self): + """Turn the media player on.""" + self.command('zone2.power=on') + + def select_source(self, source): + """Set the input source.""" + if source in self._source_list: + source = self._reverse_mapping[source] + self.command('zone2.selector={}'.format(source)) From 37cd63ea5a7a680ca47da03e5d5a98d2f0823bbf Mon Sep 17 00:00:00 2001 From: koolsb <14332595+koolsb@users.noreply.github.com> Date: Thu, 19 Apr 2018 04:35:38 -0500 Subject: [PATCH 443/924] Add blackbird media player component (#13549) --- .../components/media_player/blackbird.py | 213 ++++++++++++ .../components/media_player/services.yaml | 10 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + .../components/media_player/test_blackbird.py | 328 ++++++++++++++++++ 6 files changed, 558 insertions(+) create mode 100644 homeassistant/components/media_player/blackbird.py create mode 100644 tests/components/media_player/test_blackbird.py diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py new file mode 100644 index 00000000000..37b3c0ff819 --- /dev/null +++ b/homeassistant/components/media_player/blackbird.py @@ -0,0 +1,213 @@ +""" +Support for interfacing with Monoprice Blackbird 4k 8x8 HDBaseT Matrix. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/media_player.blackbird +""" +import logging + +import voluptuous as vol + +from homeassistant.components.media_player import ( + DOMAIN, MEDIA_PLAYER_SCHEMA, PLATFORM_SCHEMA, SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, SUPPORT_TURN_ON, MediaPlayerDevice) +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, CONF_HOST, CONF_PORT, STATE_OFF, STATE_ON) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['pyblackbird==0.5'] + +_LOGGER = logging.getLogger(__name__) + +SUPPORT_BLACKBIRD = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE + +ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, +}) + +SOURCE_SCHEMA = vol.Schema({ + vol.Required(CONF_NAME): cv.string, +}) + +CONF_ZONES = 'zones' +CONF_SOURCES = 'sources' +CONF_TYPE = 'type' + +DATA_BLACKBIRD = 'blackbird' + +SERVICE_SETALLZONES = 'blackbird_set_all_zones' +ATTR_SOURCE = 'source' + +BLACKBIRD_SETALLZONES_SCHEMA = MEDIA_PLAYER_SCHEMA.extend({ + vol.Required(ATTR_SOURCE): cv.string +}) + + +# Valid zone ids: 1-8 +ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) + +# Valid source ids: 1-8 +SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TYPE): vol.In(['serial', 'socket']), + vol.Optional(CONF_PORT): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform.""" + port = config.get(CONF_PORT) + host = config.get(CONF_HOST) + device_type = config.get(CONF_TYPE) + + import socket + from pyblackbird import get_blackbird + from serial import SerialException + + if device_type == 'serial': + if port is None: + _LOGGER.error("No port configured") + return + try: + blackbird = get_blackbird(port) + except SerialException: + _LOGGER.error("Error connecting to the Blackbird controller") + return + + elif device_type == 'socket': + try: + if host is None: + _LOGGER.error("No host configured") + return + blackbird = get_blackbird(host, False) + except socket.timeout: + _LOGGER.error("Error connecting to the Blackbird controller") + return + + else: + _LOGGER.error("Incorrect device type specified") + return + + sources = {source_id: extra[CONF_NAME] for source_id, extra + in config[CONF_SOURCES].items()} + + hass.data[DATA_BLACKBIRD] = [] + for zone_id, extra in config[CONF_ZONES].items(): + _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) + hass.data[DATA_BLACKBIRD].append(BlackbirdZone( + blackbird, sources, zone_id, extra[CONF_NAME])) + + add_devices(hass.data[DATA_BLACKBIRD], True) + + def service_handle(service): + """Handle for services.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + source = service.data.get(ATTR_SOURCE) + if entity_ids: + devices = [device for device in hass.data[DATA_BLACKBIRD] + if device.entity_id in entity_ids] + + else: + devices = hass.data[DATA_BLACKBIRD] + + for device in devices: + if service.service == SERVICE_SETALLZONES: + device.set_all_zones(source) + + hass.services.register(DOMAIN, SERVICE_SETALLZONES, service_handle, + schema=BLACKBIRD_SETALLZONES_SCHEMA) + + +class BlackbirdZone(MediaPlayerDevice): + """Representation of a Blackbird matrix zone.""" + + def __init__(self, blackbird, sources, zone_id, zone_name): + """Initialize new zone.""" + self._blackbird = blackbird + # dict source_id -> source name + self._source_id_name = sources + # dict source name -> source_id + self._source_name_id = {v: k for k, v in sources.items()} + # ordered list of all source names + self._source_names = sorted(self._source_name_id.keys(), + key=lambda v: self._source_name_id[v]) + self._zone_id = zone_id + self._name = zone_name + self._state = None + self._source = None + + def update(self): + """Retrieve latest state.""" + state = self._blackbird.zone_status(self._zone_id) + if not state: + return False + self._state = STATE_ON if state.power else STATE_OFF + idx = state.av + if idx in self._source_id_name: + self._source = self._source_id_name[idx] + else: + self._source = None + return True + + @property + def name(self): + """Return the name of the zone.""" + return self._name + + @property + def state(self): + """Return the state of the zone.""" + return self._state + + @property + def supported_features(self): + """Return flag of media commands that are supported.""" + return SUPPORT_BLACKBIRD + + @property + def media_title(self): + """Return the current source as media title.""" + return self._source + + @property + def source(self): + """Return the current input source of the device.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._source_names + + def set_all_zones(self, source): + """Set all zones to one source.""" + _LOGGER.debug("Setting all zones") + if source not in self._source_name_id: + return + idx = self._source_name_id[source] + _LOGGER.debug("Setting all zones source to %s", idx) + self._blackbird.set_all_zone_source(idx) + + def select_source(self, source): + """Set input source.""" + if source not in self._source_name_id: + return + idx = self._source_name_id[source] + _LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx) + self._blackbird.set_zone_source(self._zone_id, idx) + + def turn_on(self): + """Turn the media player on.""" + _LOGGER.debug("Turning zone %d on", self._zone_id) + self._blackbird.set_zone_power(self._zone_id, True) + + def turn_off(self): + """Turn the media player off.""" + _LOGGER.debug("Turning zone %d off", self._zone_id) + self._blackbird.set_zone_power(self._zone_id, False) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 95072f0270c..0a6c413a688 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -402,3 +402,13 @@ songpal_set_sound_setting: value: description: Value to set. example: 'on' + +blackbird_set_all_zones: + description: Set all Blackbird zones to a single source. + fields: + entity_id: + description: Name of any blackbird zone. + example: 'media_player.zone_1' + source: + description: Name of source to switch to. + example: 'Source 1' diff --git a/requirements_all.txt b/requirements_all.txt index 71a32278822..be44da3c9b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,6 +704,9 @@ pyatv==0.3.9 # homeassistant.components.sensor.bbox pybbox==0.0.5-alpha +# homeassistant.components.media_player.blackbird +pyblackbird==0.5 + # homeassistant.components.device_tracker.bluetooth_tracker # pybluez==0.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7db4ead0856..f02b5fcdf2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -129,6 +129,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.5.0 +# homeassistant.components.media_player.blackbird +pyblackbird==0.5 + # homeassistant.components.deconz pydeconz==36 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f15425063b4..b5b636dc874 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,7 @@ TEST_REQUIREMENTS = ( 'prometheus_client', 'pushbullet.py', 'py-canary', + 'pyblackbird', 'pydeconz', 'pydispatcher', 'PyJWT', diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py new file mode 100644 index 00000000000..86bfdfb52c4 --- /dev/null +++ b/tests/components/media_player/test_blackbird.py @@ -0,0 +1,328 @@ +"""The tests for the Monoprice Blackbird media player platform.""" +import unittest +from unittest import mock +import voluptuous as vol + +from collections import defaultdict +from homeassistant.components.media_player import ( + DOMAIN, SUPPORT_TURN_ON, SUPPORT_TURN_OFF, SUPPORT_SELECT_SOURCE) +from homeassistant.const import STATE_ON, STATE_OFF + +import tests.common +from homeassistant.components.media_player.blackbird import ( + DATA_BLACKBIRD, PLATFORM_SCHEMA, SERVICE_SETALLZONES, setup_platform) + + +class AttrDict(dict): + """Helper clas for mocking attributes.""" + + def __setattr__(self, name, value): + """Set attribute.""" + self[name] = value + + def __getattr__(self, item): + """Get attribute.""" + return self[item] + + +class MockBlackbird(object): + """Mock for pyblackbird object.""" + + def __init__(self): + """Init mock object.""" + self.zones = defaultdict(lambda: AttrDict(power=True, + av=1)) + + def zone_status(self, zone_id): + """Get zone status.""" + status = self.zones[zone_id] + status.zone = zone_id + return AttrDict(status) + + def set_zone_source(self, zone_id, source_idx): + """Set source for zone.""" + self.zones[zone_id].av = source_idx + + def set_zone_power(self, zone_id, power): + """Turn zone on/off.""" + self.zones[zone_id].power = power + + def set_all_zone_source(self, source_idx): + """Set source for all zones.""" + self.zones[3].av = source_idx + + +class TestBlackbirdSchema(unittest.TestCase): + """Test Blackbird schema.""" + + def test_valid_serial_schema(self): + """Test valid schema.""" + valid_schema = { + 'platform': 'blackbird', + 'type': 'serial', + 'port': '/dev/ttyUSB0', + 'zones': {1: {'name': 'a'}, + 2: {'name': 'a'}, + 3: {'name': 'a'}, + 4: {'name': 'a'}, + 5: {'name': 'a'}, + 6: {'name': 'a'}, + 7: {'name': 'a'}, + 8: {'name': 'a'}, + }, + 'sources': { + 1: {'name': 'a'}, + 2: {'name': 'a'}, + 3: {'name': 'a'}, + 4: {'name': 'a'}, + 5: {'name': 'a'}, + 6: {'name': 'a'}, + 7: {'name': 'a'}, + 8: {'name': 'a'}, + } + } + PLATFORM_SCHEMA(valid_schema) + + def test_valid_socket_schema(self): + """Test valid schema.""" + valid_schema = { + 'platform': 'blackbird', + 'type': 'socket', + 'port': '192.168.1.50', + 'zones': {1: {'name': 'a'}, + 2: {'name': 'a'}, + 3: {'name': 'a'}, + 4: {'name': 'a'}, + 5: {'name': 'a'}, + }, + 'sources': { + 1: {'name': 'a'}, + 2: {'name': 'a'}, + 3: {'name': 'a'}, + 4: {'name': 'a'}, + } + } + PLATFORM_SCHEMA(valid_schema) + + def test_invalid_schemas(self): + """Test invalid schemas.""" + schemas = ( + {}, # Empty + None, # None + # Missing type + { + 'platform': 'blackbird', + 'port': 'aaa', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Invalid zone number + { + 'platform': 'blackbird', + 'type': 'serial', + 'port': 'aaa', + 'name': 'Name', + 'zones': {11: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Invalid source number + { + 'platform': 'blackbird', + 'type': 'serial', + 'port': 'aaa', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {9: {'name': 'b'}}, + }, + # Zone missing name + { + 'platform': 'blackbird', + 'type': 'serial', + 'port': 'aaa', + 'name': 'Name', + 'zones': {1: {}}, + 'sources': {1: {'name': 'b'}}, + }, + # Source missing name + { + 'platform': 'blackbird', + 'type': 'serial', + 'port': 'aaa', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {1: {}}, + }, + # Invalid type + { + 'platform': 'blackbird', + 'type': 'aaa', + 'port': 'aaa', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + ) + for value in schemas: + with self.assertRaises(vol.MultipleInvalid): + PLATFORM_SCHEMA(value) + + +class TestBlackbirdMediaPlayer(unittest.TestCase): + """Test the media_player module.""" + + def setUp(self): + """Set up the test case.""" + self.blackbird = MockBlackbird() + self.hass = tests.common.get_test_home_assistant() + self.hass.start() + # Note, source dictionary is unsorted! + with mock.patch('pyblackbird.get_blackbird', + new=lambda *a: self.blackbird): + setup_platform(self.hass, { + 'platform': 'blackbird', + 'type': 'serial', + 'port': '/dev/ttyUSB0', + 'zones': {3: {'name': 'Zone name'}}, + 'sources': {1: {'name': 'one'}, + 3: {'name': 'three'}, + 2: {'name': 'two'}}, + }, lambda *args, **kwargs: None, {}) + self.hass.block_till_done() + self.media_player = self.hass.data[DATA_BLACKBIRD][0] + self.media_player.hass = self.hass + self.media_player.entity_id = 'media_player.zone_3' + + def tearDown(self): + """Tear down the test case.""" + self.hass.stop() + + def test_setup_platform(self, *args): + """Test setting up platform.""" + # One service must be registered + self.assertTrue(self.hass.services.has_service(DOMAIN, + SERVICE_SETALLZONES)) + self.assertEqual(len(self.hass.data[DATA_BLACKBIRD]), 1) + self.assertEqual(self.hass.data[DATA_BLACKBIRD][0].name, 'Zone name') + + def test_setallzones_service_call_with_entity_id(self): + """Test set all zone source service call with entity id.""" + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual('one', self.media_player.source) + + # Call set all zones service + self.hass.services.call(DOMAIN, SERVICE_SETALLZONES, + {'entity_id': 'media_player.zone_3', + 'source': 'three'}, + blocking=True) + + # Check that source was changed + self.assertEqual(3, self.blackbird.zones[3].av) + self.media_player.update() + self.assertEqual('three', self.media_player.source) + + def test_setallzones_service_call_without_entity_id(self): + """Test set all zone source service call without entity id.""" + self.media_player.update() + self.assertEqual('Zone name', self.media_player.name) + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual('one', self.media_player.source) + + # Call set all zones service + self.hass.services.call(DOMAIN, SERVICE_SETALLZONES, + {'source': 'three'}, blocking=True) + + # Check that source was changed + self.assertEqual(3, self.blackbird.zones[3].av) + self.media_player.update() + self.assertEqual('three', self.media_player.source) + + def test_update(self): + """Test updating values from blackbird.""" + self.assertIsNone(self.media_player.state) + self.assertIsNone(self.media_player.source) + + self.media_player.update() + + self.assertEqual(STATE_ON, self.media_player.state) + self.assertEqual('one', self.media_player.source) + + def test_name(self): + """Test name property.""" + self.assertEqual('Zone name', self.media_player.name) + + def test_state(self): + """Test state property.""" + self.assertIsNone(self.media_player.state) + + self.media_player.update() + self.assertEqual(STATE_ON, self.media_player.state) + + self.blackbird.zones[3].power = False + self.media_player.update() + self.assertEqual(STATE_OFF, self.media_player.state) + + def test_supported_features(self): + """Test supported features property.""" + self.assertEqual(SUPPORT_TURN_ON | SUPPORT_TURN_OFF | + SUPPORT_SELECT_SOURCE, + self.media_player.supported_features) + + def test_source(self): + """Test source property.""" + self.assertIsNone(self.media_player.source) + self.media_player.update() + self.assertEqual('one', self.media_player.source) + + def test_media_title(self): + """Test media title property.""" + self.assertIsNone(self.media_player.media_title) + self.media_player.update() + self.assertEqual('one', self.media_player.media_title) + + def test_source_list(self): + """Test source list property.""" + # Note, the list is sorted! + self.assertEqual(['one', 'two', 'three'], + self.media_player.source_list) + + def test_select_source(self): + """Test source selection methods.""" + self.media_player.update() + + self.assertEqual('one', self.media_player.source) + + self.media_player.select_source('two') + self.assertEqual(2, self.blackbird.zones[3].av) + self.media_player.update() + self.assertEqual('two', self.media_player.source) + + # Trying to set unknown source. + self.media_player.select_source('no name') + self.assertEqual(2, self.blackbird.zones[3].av) + self.media_player.update() + self.assertEqual('two', self.media_player.source) + + def test_turn_on(self): + """Testing turning on the zone.""" + self.blackbird.zones[3].power = False + self.media_player.update() + self.assertEqual(STATE_OFF, self.media_player.state) + + self.media_player.turn_on() + self.assertTrue(self.blackbird.zones[3].power) + self.media_player.update() + self.assertEqual(STATE_ON, self.media_player.state) + + def test_turn_off(self): + """Testing turning off the zone.""" + self.blackbird.zones[3].power = True + self.media_player.update() + self.assertEqual(STATE_ON, self.media_player.state) + + self.media_player.turn_off() + self.assertFalse(self.blackbird.zones[3].power) + self.media_player.update() + self.assertEqual(STATE_OFF, self.media_player.state) From 3180c8b0fbe8bee70a0487adc995c95f218cfde7 Mon Sep 17 00:00:00 2001 From: Viorel Stirbu Date: Thu, 19 Apr 2018 12:37:30 +0300 Subject: [PATCH 444/924] Add support for Sensirion SHT31 temperature/humidity sensor (#12952) --- .coveragerc | 1 + homeassistant/components/sensor/sht31.py | 152 +++++++++++++++++++++++ requirements_all.txt | 6 + 3 files changed, 159 insertions(+) create mode 100644 homeassistant/components/sensor/sht31.py diff --git a/.coveragerc b/.coveragerc index 1f86a13f6ae..eae6498cd0a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -648,6 +648,7 @@ omit = homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/serial_pm.py homeassistant/components/sensor/serial.py + homeassistant/components/sensor/sht31.py homeassistant/components/sensor/shodan.py homeassistant/components/sensor/sigfox.py homeassistant/components/sensor/simulated.py diff --git a/homeassistant/components/sensor/sht31.py b/homeassistant/components/sensor/sht31.py new file mode 100644 index 00000000000..1ba6c8f90eb --- /dev/null +++ b/homeassistant/components/sensor/sht31.py @@ -0,0 +1,152 @@ +""" +Support for Sensirion SHT31 temperature and humidity sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.sht31/ +""" + +from datetime import timedelta +import logging +import math + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.temperature import display_temp +from homeassistant.const import PRECISION_TENTHS +from homeassistant.util import Throttle + + +REQUIREMENTS = ['Adafruit-GPIO==1.0.3', + 'Adafruit-SHT31==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +CONF_I2C_ADDRESS = 'i2c_address' + +DEFAULT_NAME = 'SHT31' +DEFAULT_I2C_ADDRESS = 0x44 + +SENSOR_TEMPERATURE = 'temperature' +SENSOR_HUMIDITY = 'humidity' +SENSOR_TYPES = (SENSOR_TEMPERATURE, SENSOR_HUMIDITY) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_I2C_ADDRESS, default=DEFAULT_I2C_ADDRESS): + vol.All(vol.Coerce(int), vol.Range(min=0x44, max=0x45)), + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + from Adafruit_SHT31 import SHT31 + + i2c_address = config.get(CONF_I2C_ADDRESS) + sensor = SHT31(address=i2c_address) + + try: + if sensor.read_status() is None: + raise ValueError("CRC error while reading SHT31 status") + except (OSError, ValueError): + raise HomeAssistantError("SHT31 sensor not detected at address %s " % + hex(i2c_address)) + sensor_client = SHTClient(sensor) + + sensor_classes = { + SENSOR_TEMPERATURE: SHTSensorTemperature, + SENSOR_HUMIDITY: SHTSensorHumidity + } + + devs = [] + for sensor_type, sensor_class in sensor_classes.items(): + name = "{} {}".format(config.get(CONF_NAME), sensor_type.capitalize()) + devs.append(sensor_class(sensor_client, name)) + + add_devices(devs) + + +class SHTClient(object): + """Get the latest data from the SHT sensor.""" + + def __init__(self, adafruit_sht): + """Initialize the sensor.""" + self.adafruit_sht = adafruit_sht + self.temperature = None + self.humidity = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from the SHT sensor.""" + temperature, humidity = self.adafruit_sht.read_temperature_humidity() + if math.isnan(temperature) or math.isnan(humidity): + _LOGGER.warning("Bad sample from sensor SHT31") + return + self.temperature = temperature + self.humidity = humidity + + +class SHTSensor(Entity): + """An abstract SHTSensor, can be either temperature or humidity.""" + + def __init__(self, sensor, name): + """Initialize the sensor.""" + self._sensor = sensor + self._name = name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Fetch temperature and humidity from the sensor.""" + self._sensor.update() + + +class SHTSensorTemperature(SHTSensor): + """Representation of a temperature sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.hass.config.units.temperature_unit + + def update(self): + """Fetch temperature from the sensor.""" + super().update() + temp_celsius = self._sensor.temperature + if temp_celsius is not None: + self._state = display_temp(self.hass, temp_celsius, + TEMP_CELSIUS, PRECISION_TENTHS) + + +class SHTSensorHumidity(SHTSensor): + """Representation of a humidity sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return '%' + + def update(self): + """Fetch humidity from the sensor.""" + super().update() + humidity = self._sensor.humidity + if humidity is not None: + self._state = round(humidity) diff --git a/requirements_all.txt b/requirements_all.txt index be44da3c9b9..6b64dba2bc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -15,6 +15,12 @@ attrs==17.4.0 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 +# homeassistant.components.sensor.sht31 +Adafruit-GPIO==1.0.3 + +# homeassistant.components.sensor.sht31 +Adafruit-SHT31==1.0.2 + # homeassistant.components.bbb_gpio # Adafruit_BBIO==1.0.0 From 0999129f48b6f29108f045f640d747caf0648417 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Thu, 19 Apr 2018 11:42:40 +0200 Subject: [PATCH 445/924] Useless code removed (#13996) --- homeassistant/components/maxcube.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index 13d3e0b444b..cf5091fc308 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -49,9 +49,6 @@ def setup(hass, config): if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} - if DOMAIN not in config: - return False - connection_failed = 0 gateways = config[DOMAIN][CONF_GATEWAYS] for gateway in gateways: From 9fcbe68facf96e358163ebaf40d6643088f7dbd0 Mon Sep 17 00:00:00 2001 From: Pascal Hahn Date: Thu, 19 Apr 2018 12:48:21 +0200 Subject: [PATCH 446/924] Add Homematic HmIP-SWO-PR weather sensor support (#13904) --- homeassistant/components/homematic/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 23fe9685418..1528943a7f9 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -69,7 +69,8 @@ HM_DEVICE_TYPES = { 'WeatherStation', 'ThermostatWall2', 'TemperatureDiffSensor', 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', - 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat'], + 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', + 'IPWeatherSensor'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -78,7 +79,7 @@ HM_DEVICE_TYPES = { 'ShutterContact', 'Smoke', 'SmokeV2', 'Motion', 'MotionV2', 'MotionIP', 'RemoteMotion', 'WeatherSensor', 'TiltSensor', 'IPShutterContact', 'HMWIOSwitch', 'MaxShutterContact', 'Rain', - 'WiredSensor', 'PresenceIP'], + 'WiredSensor', 'PresenceIP', 'IPWeatherSensor'], DISCOVER_COVER: ['Blind', 'KeyBlind', 'IPKeyBlind', 'IPKeyBlindTilt'], DISCOVER_LOCKS: ['KeyMatic'] } @@ -89,7 +90,7 @@ HM_IGNORE_DISCOVERY_NODE = [ ] HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - 'ACTUAL_TEMPERATURE': ['IPAreaThermostat'], + 'ACTUAL_TEMPERATURE': ['IPAreaThermostat', 'IPWeatherSensor'], } HM_ATTRIBUTE_SUPPORT = { From 13e72f48a813d3b1fdf1420ed2109d90001856fc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Apr 2018 14:06:49 -0400 Subject: [PATCH 447/924] Disable ebox requirement (#14003) * Disable ebox requirement * Lint --- homeassistant/components/sensor/ebox.py | 3 ++- requirements_all.txt | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index eee959fceba..aca2d7bdb9a 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -19,7 +19,8 @@ from homeassistant.const import ( CONF_NAME, CONF_MONITORED_VARIABLES) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyebox==0.1.0'] +# pylint: disable=import-error +REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10 _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 6b64dba2bc6..bfff65c54f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,9 +747,6 @@ pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 -# homeassistant.components.sensor.ebox -pyebox==0.1.0 - # homeassistant.components.climate.econet pyeconet==0.0.5 From 27f3081b7458e4a7eb28cd29e2c5338427a90e20 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Apr 2018 22:16:48 -0400 Subject: [PATCH 448/924] Update frontend to 20180420.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 76403c0b442..87ca8bd2a28 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180418.0'] +REQUIREMENTS = ['home-assistant-frontend==20180420.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index bfff65c54f6..87cb0ccf7e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180418.0 +home-assistant-frontend==20180420.0 # homeassistant.components.homekit_controller # homekit==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f02b5fcdf2e..0d371996e36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180418.0 +home-assistant-frontend==20180420.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 2372419d42bca1d62039383acb8e9971c1482d55 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Fri, 20 Apr 2018 08:43:44 +0200 Subject: [PATCH 449/924] Upgraded miflora library to version 0.4.0 (#14005) --- homeassistant/components/sensor/miflora.py | 8 ++++---- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 37976151190..98cc7731d4d 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -16,7 +16,7 @@ from homeassistant.const import ( ) -REQUIREMENTS = ['miflora==0.3.0'] +REQUIREMENTS = ['miflora==0.4.0'] _LOGGER = logging.getLogger(__name__) @@ -63,10 +63,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): from miflora import miflora_poller try: import bluepy.btle # noqa: F401 # pylint: disable=unused-variable - from miflora.backends.bluepy import BluepyBackend + from btlewrap import BluepyBackend backend = BluepyBackend except ImportError: - from miflora.backends.gatttool import GatttoolBackend + from btlewrap import GatttoolBackend backend = GatttoolBackend _LOGGER.debug('Miflora is using %s backend.', backend.__name__) @@ -138,7 +138,7 @@ class MiFloraSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ - from miflora.backends import BluetoothBackendException + from btlewrap import BluetoothBackendException try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) diff --git a/requirements_all.txt b/requirements_all.txt index 87cb0ccf7e1..ccfaf0b35a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -525,7 +525,7 @@ messagebird==1.2.0 mficlient==0.3.0 # homeassistant.components.sensor.miflora -miflora==0.3.0 +miflora==0.4.0 # homeassistant.components.sensor.mopar motorparts==1.0.2 From 8ef2abfca7294d34a07ad69a6534c000cc5499ce Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 20 Apr 2018 08:45:28 +0200 Subject: [PATCH 450/924] Log an error instead of raising an exception (#14006) --- homeassistant/components/sensor/sht31.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/sht31.py b/homeassistant/components/sensor/sht31.py index 1ba6c8f90eb..e1a7f3c9e5f 100644 --- a/homeassistant/components/sensor/sht31.py +++ b/homeassistant/components/sensor/sht31.py @@ -14,7 +14,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( TEMP_CELSIUS, CONF_NAME, CONF_MONITORED_CONDITIONS) -from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.temperature import display_temp @@ -58,8 +57,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if sensor.read_status() is None: raise ValueError("CRC error while reading SHT31 status") except (OSError, ValueError): - raise HomeAssistantError("SHT31 sensor not detected at address %s " % - hex(i2c_address)) + _LOGGER.error( + "SHT31 sensor not detected at address %s", hex(i2c_address)) + return sensor_client = SHTClient(sensor) sensor_classes = { From 825f94f47fd972340245cff6988c95cd1645ae45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Fri, 20 Apr 2018 11:45:11 +0200 Subject: [PATCH 451/924] Tibber available (#13865) * Tibber available * Tibber available * Tibber * Tibber --- homeassistant/components/sensor/tibber.py | 90 ++++++++++++++--------- 1 file changed, 55 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index ca1c1922ab5..4fb378ac227 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -19,6 +19,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util +from homeassistant.util import Throttle REQUIREMENTS = ['pyTibber==0.4.1'] @@ -30,6 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ICON = 'mdi:currency-usd' SCAN_INTERVAL = timedelta(minutes=1) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) async def async_setup_platform(hass, config, async_add_devices, @@ -58,7 +60,9 @@ class TibberSensor(Entity): """Initialize the sensor.""" self._tibber_home = tibber_home self._last_updated = None + self._newest_data_timestamp = None self._state = None + self._is_available = False self._device_state_attributes = {} self._unit_of_measurement = self._tibber_home.price_unit self._name = 'Electricity price {}'.format(tibber_home.info['viewer'] @@ -68,50 +72,27 @@ class TibberSensor(Entity): """Get the latest data and updates the states.""" now = dt_util.utcnow() if self._tibber_home.current_price_total and self._last_updated and \ - dt_util.as_utc(dt_util.parse_datetime(self._last_updated)).hour\ - == now.hour: + self._last_updated.hour == now.hour and self._newest_data_timestamp: return - def _find_current_price(): - state = None - max_price = None - min_price = None - for key, price_total in self._tibber_home.price_total.items(): - price_time = dt_util.as_utc(dt_util.parse_datetime(key)) - price_total = round(price_total, 3) - time_diff = (now - price_time).total_seconds()/60 - if time_diff >= 0 and time_diff < 60: - state = price_total - self._last_updated = key - if now.date() == price_time.date(): - if max_price is None or price_total > max_price: - max_price = price_total - if min_price is None or price_total < min_price: - min_price = price_total - self._state = state - self._device_state_attributes['max_price'] = max_price - self._device_state_attributes['min_price'] = min_price - return state is not None + if (not self._newest_data_timestamp or + (self._newest_data_timestamp - now).total_seconds()/3600 < 12 + or not self._is_available): + _LOGGER.debug("Asking for new data.") + await self._fetch_data() - if _find_current_price(): - return - - _LOGGER.debug("No cached data found, so asking for new data") - await self._tibber_home.update_info() - await self._tibber_home.update_price_info() - data = self._tibber_home.info['viewer']['home'] - self._device_state_attributes['app_nickname'] = data['appNickname'] - self._device_state_attributes['grid_company'] =\ - data['meteringPointData']['gridCompany'] - self._device_state_attributes['estimated_annual_consumption'] =\ - data['meteringPointData']['estimatedAnnualConsumption'] - _find_current_price() + self._is_available = self._update_current_price() @property def device_state_attributes(self): """Return the state attributes.""" return self._device_state_attributes + @property + def available(self): + """Return True if entity is available.""" + return self._is_available + @property def name(self): """Return the name of the sensor.""" @@ -137,3 +118,42 @@ class TibberSensor(Entity): """Return a unique ID.""" home = self._tibber_home.info['viewer']['home'] return home['meteringPointData']['consumptionEan'] + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def _fetch_data(self): + try: + await self._tibber_home.update_info() + await self._tibber_home.update_price_info() + except (asyncio.TimeoutError, aiohttp.ClientError): + return + data = self._tibber_home.info['viewer']['home'] + self._device_state_attributes['app_nickname'] = data['appNickname'] + self._device_state_attributes['grid_company'] = \ + data['meteringPointData']['gridCompany'] + self._device_state_attributes['estimated_annual_consumption'] = \ + data['meteringPointData']['estimatedAnnualConsumption'] + + def _update_current_price(self): + state = None + max_price = None + min_price = None + now = dt_util.utcnow() + for key, price_total in self._tibber_home.price_total.items(): + price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_total = round(price_total, 3) + time_diff = (now - price_time).total_seconds()/60 + if (not self._newest_data_timestamp or + price_time > self._newest_data_timestamp): + self._newest_data_timestamp = price_time + if 0 <= time_diff < 60: + state = price_total + self._last_updated = price_time + if now.date() == price_time.date(): + if max_price is None or price_total > max_price: + max_price = price_total + if min_price is None or price_total < min_price: + min_price = price_total + self._state = state + self._device_state_attributes['max_price'] = max_price + self._device_state_attributes['min_price'] = min_price + return state is not None From 8459b241a22ef9c522863a2a5ed9df0c3e28dab5 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Fri, 20 Apr 2018 06:35:56 -0700 Subject: [PATCH 452/924] Upgrade pylutron-caseta to 0.5.0 to reestablish connections (#14013) * Upgrade pylutron-caseta to 0.5.0 to reestablish connections * Upgrade pylutron-caseta to 0.5.0 in requirements_all.txt --- homeassistant/components/lutron_caseta.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron_caseta.py b/homeassistant/components/lutron_caseta.py index 63f0315f35c..7b1b7417cfd 100644 --- a/homeassistant/components/lutron_caseta.py +++ b/homeassistant/components/lutron_caseta.py @@ -14,7 +14,7 @@ from homeassistant.const import CONF_HOST from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylutron-caseta==0.3.0'] +REQUIREMENTS = ['pylutron-caseta==0.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ccfaf0b35a1..aeb5b84811e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -833,7 +833,7 @@ pylitejet==0.1 pyloopenergy==0.0.18 # homeassistant.components.lutron_caseta -pylutron-caseta==0.3.0 +pylutron-caseta==0.5.0 # homeassistant.components.lutron pylutron==0.1.0 From 2a5fac3b9da4e3a93fcfea9b5175e374050c9ca0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Apr 2018 09:38:27 -0400 Subject: [PATCH 453/924] Add sensor device classes (#14010) --- homeassistant/components/sensor/__init__.py | 9 +++++++++ homeassistant/components/sensor/ecobee.py | 7 +++++++ homeassistant/components/sensor/linux_battery.py | 5 +++++ homeassistant/components/sensor/nest.py | 5 +++++ 4 files changed, 26 insertions(+) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index e0bf3c86b05..2bc35a034f4 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,6 +8,8 @@ https://home-assistant.io/components/sensor/ from datetime import timedelta import logging +import voluptuous as vol + from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -18,6 +20,13 @@ DOMAIN = 'sensor' ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=30) +DEVICE_CLASSES = [ + 'battery', # % of battery that is left + 'humidity', # % of humidity in the air + 'temperature', # temperature (C/F) +] + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index dad770d5bab..7274f421f15 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -52,6 +52,13 @@ class EcobeeSensor(Entity): """Return the name of the Ecobee sensor.""" return self._name + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.type in ('temperature', 'humidity'): + return self.type + return None + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index 3d28c44d606..1f0e3e89e5c 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -94,6 +94,11 @@ class LinuxBatterySensor(Entity): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the device class of the sensor.""" + return 'battery' + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index e2567fdf4ca..5ee4f738051 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -140,6 +140,11 @@ class NestTempSensor(NestSensor): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the device class of the sensor.""" + return 'temperature' + def update(self): """Retrieve latest state.""" if self.device.temperature_scale == 'C': From 2b537297084cbef78af8f5452871bae27b68e90d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Apr 2018 10:58:43 -0400 Subject: [PATCH 454/924] Version bump to 0.68.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 43380d00a2d..56e37e5e039 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 68 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6ccb83584e3da104f65fa915bc48a761a6d507af Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 21 Apr 2018 08:34:42 +0200 Subject: [PATCH 455/924] Qwikswitch binary sensors (#14008) --- .../components/binary_sensor/qwikswitch.py | 70 +++++++++++++++++++ homeassistant/components/qwikswitch.py | 44 ++++++++---- homeassistant/components/sensor/qwikswitch.py | 12 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../{sensor => }/test_qwikswitch.py | 70 +++++++++++++------ 6 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/binary_sensor/qwikswitch.py rename tests/components/{sensor => }/test_qwikswitch.py (55%) diff --git a/homeassistant/components/binary_sensor/qwikswitch.py b/homeassistant/components/binary_sensor/qwikswitch.py new file mode 100644 index 00000000000..067021b0c7a --- /dev/null +++ b/homeassistant/components/binary_sensor/qwikswitch.py @@ -0,0 +1,70 @@ +""" +Support for Qwikswitch Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.qwikswitch/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH +from homeassistant.core import callback + +DEPENDENCIES = [QWIKSWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, _, add_devices, discovery_info=None): + """Add binary sensor from the main Qwikswitch component.""" + if discovery_info is None: + return + + qsusb = hass.data[QWIKSWITCH] + _LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s", + qsusb, discovery_info) + devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSBinarySensor(QSEntity, BinarySensorDevice): + """Sensor based on a Qwikswitch relay/dimmer module.""" + + _val = False + + def __init__(self, sensor): + """Initialize the sensor.""" + from pyqwikswitch import SENSORS + + super().__init__(sensor['id'], sensor['name']) + self.channel = sensor['channel'] + sensor_type = sensor['type'] + + self._decode, _ = SENSORS[sensor_type] + self._invert = not sensor.get('invert', False) + self._class = sensor.get('class', 'door') + + @callback + def update_packet(self, packet): + """Receive update packet from QSUSB.""" + val = self._decode(packet, channel=self.channel) + _LOGGER.debug("Update %s (%s:%s) decoded as %s: %s", + self.entity_id, self.qsid, self.channel, val, packet) + if val is not None: + self._val = bool(val) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Check if device is on (non-zero).""" + return self._val == self._invert + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return "qs{}:{}".format(self.qsid, self.channel) + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._class diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 3dc16f513dc..f26318fa7a9 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -8,17 +8,18 @@ import logging import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL, - CONF_SENSORS, CONF_SWITCHES) + CONF_SENSORS, CONF_SWITCHES, CONF_URL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -from homeassistant.components.light import ATTR_BRIGHTNESS -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.71'] +REQUIREMENTS = ['pyqwikswitch==0.8'] _LOGGER = logging.getLogger(__name__) @@ -28,6 +29,7 @@ CONF_DIMMER_ADJUST = 'dimmer_adjust' CONF_BUTTON_EVENTS = 'button_events' CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3)) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_URL, default='http://127.0.0.1:2020'): @@ -40,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional('channel', default=1): int, vol.Required('name'): str, vol.Required('type'): str, + vol.Optional('class'): DEVICE_CLASSES_SCHEMA, + vol.Optional('invert'): bool })]), vol.Optional(CONF_SWITCHES, default=[]): vol.All( cv.ensure_list, [str]) @@ -115,7 +119,7 @@ class QSToggleEntity(QSEntity): async def async_setup(hass, config): """Qwiskswitch component setup.""" from pyqwikswitch.async_ import QSUsb - from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType + from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] @@ -143,22 +147,39 @@ async def async_setup(hass, config): hass.data[DOMAIN] = qsusb - _new = {'switch': [], 'light': [], 'sensor': sensors} + comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []} + + try: + for sens in sensors: + _, _type = SENSORS[sens['type']] + if _type is bool: + comps['binary_sensor'].append(sens) + continue + comps['sensor'].append(sens) + for _key in ('invert', 'class'): + if _key in sens: + _LOGGER.warning( + "%s should only be used for binary_sensors: %s", + _key, sens) + + except KeyError: + _LOGGER.warning("Sensor validation failed") + for qsid, dev in qsusb.devices.items(): if qsid in switches: if dev.qstype != QSType.relay: _LOGGER.warning( "You specified a switch that is not a relay %s", qsid) continue - _new['switch'].append(qsid) + comps['switch'].append(qsid) elif dev.qstype in (QSType.relay, QSType.dimmer): - _new['light'].append(qsid) + comps['light'].append(qsid) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", dev) continue # Load platforms - for comp_name, comp_conf in _new.items(): + for comp_name, comp_conf in comps.items(): if comp_conf: load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) @@ -190,9 +211,8 @@ async def async_setup(hass, config): @callback def async_stop(_): - """Stop the listener queue and clean up.""" + """Stop the listener.""" hass.data[DOMAIN].stop() - _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)") hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py index ebd5f5254d4..1497b4ad5cc 100644 --- a/homeassistant/components/sensor/qwikswitch.py +++ b/homeassistant/components/sensor/qwikswitch.py @@ -36,18 +36,18 @@ class QSSensor(QSEntity): super().__init__(sensor['id'], sensor['name']) self.channel = sensor['channel'] - self.sensor_type = sensor['type'] + sensor_type = sensor['type'] - self._decode, self.unit = SENSORS[self.sensor_type] + self._decode, self.unit = SENSORS[sensor_type] if isinstance(self.unit, type): - self.unit = "{}:{}".format(self.sensor_type, self.channel) + self.unit = "{}:{}".format(sensor_type, self.channel) @callback def update_packet(self, packet): """Receive update packet from QSUSB.""" - val = self._decode(packet.get('data'), channel=self.channel) - _LOGGER.debug("Update %s (%s) decoded as %s: %s: %s", - self.entity_id, self.qsid, val, self.channel, packet) + val = self._decode(packet, channel=self.channel) + _LOGGER.debug("Update %s (%s:%s) decoded as %s: %s", + self.entity_id, self.qsid, self.channel, val, packet) if val is not None: self._val = val self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index aeb5b84811e..bc3724d0930 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ pyowm==2.8.0 pypollencom==1.1.2 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d371996e36..cf4aa2e1b3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ pymonoprice==0.3 pynx584==0.4 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.8 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky diff --git a/tests/components/sensor/test_qwikswitch.py b/tests/components/test_qwikswitch.py similarity index 55% rename from tests/components/sensor/test_qwikswitch.py rename to tests/components/test_qwikswitch.py index d9dfe072fc0..76655f32816 100644 --- a/tests/components/sensor/test_qwikswitch.py +++ b/tests/components/test_qwikswitch.py @@ -13,17 +13,19 @@ _LOGGER = logging.getLogger(__name__) class AiohttpClientMockResponseList(list): - """List that fires an event on empty pop, for aiohttp Mocker.""" + """Return multiple values for aiohttp Mocker. + + aoihttp mocker uses decode to fetch the next value. + """ def decode(self, _): """Return next item from list.""" try: - res = list.pop(self) + res = list.pop(self, 0) _LOGGER.debug("MockResponseList popped %s: %s", res, self) return res except IndexError: - _LOGGER.debug("MockResponseList empty") - return "" + raise AssertionError("MockResponseList empty") async def wait_till_empty(self, hass): """Wait until empty.""" @@ -52,8 +54,8 @@ def aioclient_mock(): yield mock_session -async def test_sensor_device(hass, aioclient_mock): - """Test a sensor device.""" +async def test_binary_sensor_device(hass, aioclient_mock): + """Test a binary sensor device.""" config = { 'qwikswitch': { 'sensors': { @@ -67,21 +69,49 @@ async def test_sensor_device(hass, aioclient_mock): await async_setup_component(hass, QWIKSWITCH, config) await hass.async_block_till_done() - state_obj = hass.states.get('sensor.s1') - assert state_obj - assert state_obj.state == 'None' + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'off' + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - LISTEN.append( # Close - """{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""") + LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}') + LISTEN.append('') # Will cause a sleep await hass.async_block_till_done() - state_obj = hass.states.get('sensor.s1') - assert state_obj.state == 'True' + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'on' - # Causes a 30second delay: can be uncommented when upstream library - # allows cancellation of asyncio.sleep(30) on failed packet ("") - # LISTEN.append( # Open - # """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""") - # await LISTEN.wait_till_empty(hass) - # state_obj = hass.states.get('sensor.s1') - # assert state_obj.state == 'False' + LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}') + hass.data[QWIKSWITCH]._sleep_task.cancel() + await LISTEN.wait_till_empty(hass) + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'off' + + +async def test_sensor_device(hass, aioclient_mock): + """Test a sensor device.""" + config = { + 'qwikswitch': { + 'sensors': { + 'name': 'ss1', + 'id': '@a00001', + 'channel': 1, + 'type': 'qwikcord', + } + } + } + await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.ss1') + assert state_obj.state == 'None' + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + LISTEN.append( + '{"id":"@a00001","name":"ss1","type":"rel",' + '"val":"4733800001a00000"}') + LISTEN.append('') # Will cause a sleep + await LISTEN.wait_till_empty(hass) # await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.ss1') + assert state_obj.state == 'None' From cb490780c9cf9f10f1c883a7474c0ef5d3ed2ef8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 21 Apr 2018 02:16:52 -0600 Subject: [PATCH 456/924] Pollen.com: Added attributes on top 3 allergens (#14018) --- homeassistant/components/sensor/pollen.py | 26 +++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/pollen.py b/homeassistant/components/sensor/pollen.py index b55c60f6e7c..1ef5a27cf3d 100644 --- a/homeassistant/components/sensor/pollen.py +++ b/homeassistant/components/sensor/pollen.py @@ -160,7 +160,7 @@ class BaseSensor(Entity): def __init__(self, data, data_params, name, icon, unique_id): """Initialize the sensor.""" - self._attrs = {} + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = icon self._name = name self._data_params = data_params @@ -172,7 +172,6 @@ class BaseSensor(Entity): @property def device_state_attributes(self): """Return the device state attributes.""" - self._attrs.update({ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}) return self._attrs @property @@ -254,10 +253,25 @@ class AllergyIndexSensor(BaseSensor): i['label'] for i in RATING_MAPPING if i['minimum'] <= period['Index'] <= i['maximum'] ] - self._attrs[ATTR_ALLERGEN_GENUS] = period['Triggers'][0]['Genus'] - self._attrs[ATTR_ALLERGEN_NAME] = period['Triggers'][0]['Name'] - self._attrs[ATTR_ALLERGEN_TYPE] = period['Triggers'][0][ - 'PlantType'] + + for i in range(3): + index = i + 1 + try: + data = period['Triggers'][i] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_GENUS, index)] = data['Genus'] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_NAME, index)] = data['Name'] + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_TYPE, index)] = data['PlantType'] + except IndexError: + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_GENUS, index)] = None + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_NAME, index)] = None + self._attrs['{0}_{1}'.format( + ATTR_ALLERGEN_TYPE, index)] = None + self._attrs[ATTR_RATING] = rating except KeyError: From f12ff6f2970e43b19a23517ff88a6713081a079b Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sat, 21 Apr 2018 04:20:33 -0400 Subject: [PATCH 457/924] Expose the condition code on condition sensors (#14011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * expose the condition code on condition sensors * :lipstick: * like thisss duh * add test for condition_code * It’s a string --- homeassistant/components/sensor/yweather.py | 9 ++++++--- tests/components/sensor/test_yweather.py | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/yweather.py b/homeassistant/components/sensor/yweather.py index df18e086ddd..db66419e54a 100644 --- a/homeassistant/components/sensor/yweather.py +++ b/homeassistant/components/sensor/yweather.py @@ -131,9 +131,12 @@ class YahooWeatherSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - } + attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + + if self._code is not None and "weather" in self._type: + attrs['condition_code'] = self._code + + return attrs def update(self): """Get the latest data from Yahoo! and updates the states.""" diff --git a/tests/components/sensor/test_yweather.py b/tests/components/sensor/test_yweather.py index 88b94906a35..aeee47bfa80 100644 --- a/tests/components/sensor/test_yweather.py +++ b/tests/components/sensor/test_yweather.py @@ -162,6 +162,8 @@ class TestWeather(unittest.TestCase): state = self.hass.states.get('sensor.yweather_condition') assert state is not None self.assertEqual(state.state, 'Mostly Cloudy') + self.assertEqual(state.attributes.get('condition_code'), + '28') self.assertEqual(state.attributes.get('friendly_name'), 'Yweather Condition') From 4c23a61853b74e865e4972dc02a65d7aa46fccfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 21 Apr 2018 10:54:11 +0200 Subject: [PATCH 458/924] upgrade rfxtrx lib, dimming support for Lighting3 (#14026) --- homeassistant/components/rfxtrx.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index d6873a0bd91..2e96ec64d97 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -20,7 +20,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyRFXtrx==0.22.0'] +REQUIREMENTS = ['pyRFXtrx==0.22.1'] DOMAIN = 'rfxtrx' diff --git a/requirements_all.txt b/requirements_all.txt index bc3724d0930..d6f811ba68c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -677,7 +677,7 @@ pyCEC==0.4.13 pyHS100==0.3.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.22.0 +pyRFXtrx==0.22.1 # homeassistant.components.sensor.tibber pyTibber==0.4.1 From 51f55bddb7c1633c540b5e89da41cb705a6a3723 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sat, 21 Apr 2018 10:16:46 -0400 Subject: [PATCH 459/924] HomeKit Alarm Control Panel Code Exception Fix (#14025) * Catch exception for KeyError * Use get and added test --- .../components/homekit/type_security_systems.py | 2 +- .../components/homekit/test_type_security_systems.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 6b8457a3aa5..0762e0f25f9 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -30,7 +30,7 @@ class SecuritySystem(HomeAccessory): def __init__(self, *args, config): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) - self._alarm_code = config[ATTR_CODE] + self._alarm_code = config.get(ATTR_CODE) self.flag_target_state = False serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index ec538ce4b50..9c1ff0faf1a 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -109,8 +109,16 @@ class TestHomekitSecuritySystems(unittest.TestCase): acc = SecuritySystem(self.hass, 'SecuritySystem', acp, 2, config={ATTR_CODE: None}) - acc.run() - + # Set from HomeKit + acc.char_target_state.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) + + acc = SecuritySystem(self.hass, 'SecuritySystem', acp, + 2, config={}) # Set from HomeKit acc.char_target_state.client_update_value(0) self.hass.block_till_done() From c2bee496e2a83a05116c5e4c637f00aeec2109b6 Mon Sep 17 00:00:00 2001 From: Ryan Bahm Date: Sat, 21 Apr 2018 23:42:18 -0700 Subject: [PATCH 460/924] Add Accuracy to Google Location Sharing (#14039) * Update locationsharinglib to 1.2.1 and add accuracy. * Change indents to match HA style --- homeassistant/components/device_tracker/google_maps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index d1e59293365..b594b23dbeb 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -79,5 +79,6 @@ class GoogleMapsScanner(object): gps=(person.latitude, person.longitude), picture=person.picture_url, source_type=SOURCE_TYPE_GPS, + gps_accuracy=person.accuracy, attributes=attrs ) From 86374ad80927fe8a4054fc750ca96132d83c901b Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Sun, 22 Apr 2018 20:54:48 +1000 Subject: [PATCH 461/924] bump gogogate2 version (#14044) * bump gogogate2 version * Update - requirements_all --- homeassistant/components/cover/gogogate2.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index 99da248b094..688df62ca6a 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygogogate2==0.0.3'] +REQUIREMENTS = ['pygogogate2==0.0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d6f811ba68c..8efac1f8ec7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -778,7 +778,7 @@ pyfritzhome==0.3.7 pyfttt==0.3 # homeassistant.components.cover.gogogate2 -pygogogate2==0.0.3 +pygogogate2==0.0.7 # homeassistant.components.remote.harmony pyharmony==1.0.20 From 1fbc6508715c214e9b3a3077959846324a6423b1 Mon Sep 17 00:00:00 2001 From: Stijn Tintel Date: Sun, 22 Apr 2018 13:55:45 +0300 Subject: [PATCH 462/924] device_tracker.ubus: catch ConnectionError (#14045) When an OpenWrt device monitored via ubus is offline, this causes the log to be flooded with several exceptions. Avoid this by catching requests.exceptions.ConnectionError in addition to requests.exceptions.Timeout. Signed-off-by: Stijn Tintel --- homeassistant/components/device_tracker/ubus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/ubus.py b/homeassistant/components/device_tracker/ubus.py index 3d7ef5cef6e..f265014657b 100644 --- a/homeassistant/components/device_tracker/ubus.py +++ b/homeassistant/components/device_tracker/ubus.py @@ -207,7 +207,7 @@ def _req_json_rpc(url, session_id, rpcmethod, subsystem, method, **params): try: res = requests.post(url, data=data, timeout=5) - except requests.exceptions.Timeout: + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): return if res.status_code == 200: From 5d3471269aa2b40525aed7aa1d85447bf71926cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Apr 2018 15:00:24 -0400 Subject: [PATCH 463/924] Show a notification when a config entry is discovered (#14022) * Show a notification when a config entry is discovered * update comment * Inline functions * Lint --- homeassistant/config_entries.py | 24 ++++++++++++++++++++- homeassistant/data_entry_flow.py | 2 +- tests/conftest.py | 2 +- tests/test_config_entries.py | 36 ++++++++++++++++++++++++++++++++ tests/test_data_entry_flow.py | 2 +- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 46bb2f7bfe2..b159f01c72b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -141,6 +141,9 @@ ENTRY_STATE_SETUP_ERROR = 'setup_error' ENTRY_STATE_NOT_LOADED = 'not_loaded' ENTRY_STATE_FAILED_UNLOAD = 'failed_unload' +DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery' +DISCOVERY_SOURCES = (data_entry_flow.SOURCE_DISCOVERY,) + class ConfigEntry: """Hold a configuration entry.""" @@ -362,9 +365,19 @@ class ConfigEntries: await async_setup_component( self.hass, entry.domain, self._hass_config) + # Return Entry if they not from a discovery request + if result['source'] not in DISCOVERY_SOURCES: + return entry + + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + return entry - async def _async_create_flow(self, handler): + async def _async_create_flow(self, handler, *, source, data): """Create a flow for specified handler. Handler key is the domain of the component that we want to setup. @@ -379,6 +392,15 @@ class ConfigEntries: await async_process_deps_reqs( self.hass, self._hass_config, handler, component) + # Create notification. + if source in DISCOVERY_SOURCES: + self.hass.components.persistent_notification.async_create( + title='New devices discovered', + message=("We have discovered new devices on your network. " + "[Check it out](/config/integrations)"), + notification_id=DISCOVERY_NOTIFICATION_ID + ) + return handler() @callback diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index cadec3f3d69..8eb18a3a7e7 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -52,7 +52,7 @@ class FlowManager: async def async_init(self, handler, *, source=SOURCE_USER, data=None): """Start a configuration flow.""" - flow = await self._async_create_flow(handler) + flow = await self._async_create_flow(handler, source=source, data=data) flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex diff --git a/tests/conftest.py b/tests/conftest.py index 269d460ebb6..73e69605eae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ if os.environ.get('UVLOOP') == '1': import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -logging.basicConfig() +logging.basicConfig(level=logging.INFO) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 94b1dcb47da..b46909d7732 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -245,3 +245,39 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_discovery_notification(hass): + """Test that we create/dismiss a notification when source is discovery.""" + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title='Test Title', + data={ + 'token': 'abcd' + } + ) + return self.async_show_form( + step_id='discovery', + ) + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + result = await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is not None + + result = await hass.config_entries.flow.async_configure( + result['flow_id'], {}) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 2767e206c30..6d3e41436c5 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -12,7 +12,7 @@ def manager(): handlers = Registry() entries = [] - async def async_create_flow(handler_name): + async def async_create_flow(handler_name, *, source, data): handler = handlers.get(handler_name) if handler is None: From 7f634c6ed060061e7a99f9463012e63d3972839d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 22 Apr 2018 22:32:15 +0200 Subject: [PATCH 464/924] Revert cast platform polling mode (#14027) --- homeassistant/components/media_player/cast.py | 64 +++----------- tests/components/media_player/test_cast.py | 85 +------------------ 2 files changed, 13 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 30d4bd166d0..632ab4214b8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -288,8 +288,7 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None - self.media_status_position = None - self.media_status_position_received = None + self.media_status_received = None self._available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] @@ -362,26 +361,10 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None self.cast_status = None self.media_status = None - self.media_status_position = None - self.media_status_position_received = None + self.media_status_received = None self._status_listener.invalidate() self._status_listener = None - def update(self): - """Periodically update the properties. - - Even though we receive callbacks for most state changes, some 3rd party - apps don't always send them. Better poll every now and then if the - chromecast is active (i.e. an app is running). - """ - if not self._available: - # Not connected or not available. - return - - if self._chromecast.media_controller.is_active: - # We can only update status if the media namespace is active - self._chromecast.media_controller.update_status() - # ========== Callbacks ========== def new_cast_status(self, cast_status): """Handle updates of the cast status.""" @@ -390,36 +373,8 @@ class CastDevice(MediaPlayerDevice): def new_media_status(self, media_status): """Handle updates of the media status.""" - # Only use media position for playing/paused, - # and for normal playback rate - if (media_status is None or - abs(media_status.playback_rate - 1) > 0.01 or - not (media_status.player_is_playing or - media_status.player_is_paused)): - self.media_status_position = None - self.media_status_position_received = None - else: - # Avoid unnecessary state attribute updates if player_state and - # calculated position stay the same - now = dt_util.utcnow() - do_update = \ - (self.media_status is None or - self.media_status_position is None or - self.media_status.player_state != media_status.player_state) - if not do_update: - if media_status.player_is_playing: - elapsed = now - self.media_status_position_received - do_update = abs(media_status.current_time - - (self.media_status_position + - elapsed.total_seconds())) > 1 - else: - do_update = \ - self.media_status_position != media_status.current_time - if do_update: - self.media_status_position = media_status.current_time - self.media_status_position_received = now - self.media_status = media_status + self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() def new_connection_status(self, connection_status): @@ -496,8 +451,8 @@ class CastDevice(MediaPlayerDevice): # ========== Properties ========== @property def should_poll(self): - """Polling needed for cast integration, see async_update.""" - return True + """No polling needed.""" + return False @property def name(self): @@ -625,7 +580,12 @@ class CastDevice(MediaPlayerDevice): @property def media_position(self): """Position of current playing media in seconds.""" - return self.media_status_position + if self.media_status is None or \ + not (self.media_status.player_is_playing or + self.media_status.player_is_paused or + self.media_status.player_is_idle): + return None + return self.media_status.current_time @property def media_position_updated_at(self): @@ -633,7 +593,7 @@ class CastDevice(MediaPlayerDevice): Returns value from homeassistant.util.dt.utcnow(). """ - return self.media_status_position_received + return self.media_status_received @property def unique_id(self) -> Optional[str]: diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 0c0f3906dc2..ee69ec1c85d 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,7 +1,6 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access import asyncio -import datetime as dt from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID @@ -15,8 +14,7 @@ from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send -from homeassistant.components.media_player import cast, \ - ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT +from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component @@ -288,8 +286,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert entity.unique_id == full_info.uuid media_status = MagicMock(images=None) - media_status.current_time = 0 - media_status.playback_rate = 1 media_status.player_is_playing = True entity.new_media_status(media_status) await hass.async_block_till_done() @@ -324,85 +320,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == 'unknown' -async def test_entity_media_position(hass: HomeAssistantType): - """Test various entity media states.""" - info = get_fake_chromecast_info() - full_info = attr.evolve(info, model_name='google home', - friendly_name='Speaker', uuid=FakeUUID) - - with patch('pychromecast.dial.get_device_status', - return_value=full_info): - chromecast, entity = await async_setup_media_player_cast(hass, info) - - media_status = MagicMock(images=None) - media_status.current_time = 10 - media_status.playback_rate = 1 - media_status.player_is_playing = True - media_status.player_is_paused = False - media_status.player_is_idle = False - now = dt.datetime.now(dt.timezone.utc) - with patch('homeassistant.util.dt.utcnow', return_value=now): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 10 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now - - media_status.current_time = 15 - now_plus_5 = now + dt.timedelta(seconds=5) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 10 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now - - media_status.current_time = 20 - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 20 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5 - - media_status.current_time = 25 - now_plus_10 = now + dt.timedelta(seconds=10) - media_status.player_is_playing = False - media_status.player_is_paused = True - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_10): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 25 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 - - now_plus_15 = now + dt.timedelta(seconds=15) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_15): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 25 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 - - media_status.current_time = 30 - now_plus_20 = now + dt.timedelta(seconds=20) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 30 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20 - - media_status.player_is_paused = False - media_status.player_is_idle = True - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert ATTR_MEDIA_POSITION not in state.attributes - assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes - - async def test_switched_host(hass: HomeAssistantType): """Test cast device listens for changed hosts and disconnects old cast.""" info = get_fake_chromecast_info() From e4cb3af76d15567606a147da7d2a3c6c99556199 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 22 Apr 2018 13:38:01 -0700 Subject: [PATCH 465/924] Handle HomeKit configuration failure more cleanly (#14041) * Handle HomeKit configuration failure more cleanly Add support for handling cases where HomeKit configuration fails, and give the user more information about what to do. * Don't consume the exception for a homekit.UnknownError If we get an UnknownError then we should alert the user but also still generate the backtrace so there's actually something for them to file in a bug report. --- .../components/homekit_controller/__init__.py | 27 ++++++++++++++++--- requirements_all.txt | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index c33edd07918..164e7d50e4d 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -14,7 +14,7 @@ from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['homekit==0.5'] +REQUIREMENTS = ['homekit==0.6'] DOMAIN = 'homekit_controller' HOMEKIT_DIR = '.homekit' @@ -133,10 +133,31 @@ class HKDevice(): import homekit pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() - self.pairing_data = homekit.perform_pair_setup( - self.conn, code, pairing_id) + try: + self.pairing_data = homekit.perform_pair_setup(self.conn, code, + pairing_id) + except homekit.exception.UnavailableError: + error_msg = "This accessory is already paired to another device. \ + Please reset the accessory and try again." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.exception.AuthenticationError: + error_msg = "Incorrect HomeKit code for {}. Please check it and \ + try again.".format(self.model) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.exception.UnknownError: + error_msg = "Received an unknown error. Please file a bug." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + raise + if self.pairing_data is not None: homekit.save_pairing(self.pairing_file, self.pairing_data) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.request_done(_configurator) self.accessory_setup() else: error_msg = "Unable to pair, please try again" diff --git a/requirements_all.txt b/requirements_all.txt index 8efac1f8ec7..7946a0d0955 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ holidays==0.9.4 home-assistant-frontend==20180420.0 # homeassistant.components.homekit_controller -# homekit==0.5 +# homekit==0.6 # homeassistant.components.homematicip_cloud homematicip==0.8 From 5fe40530215d376640ecbb7c57bcf30fc7200a8b Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Mon, 23 Apr 2018 07:52:39 -0400 Subject: [PATCH 466/924] Update device classes for contact sensor HomeKit (#14051) --- homeassistant/components/homekit/const.py | 3 +++ homeassistant/components/homekit/type_sensors.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 1c498b4b3b9..59444c75421 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -102,6 +102,8 @@ PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Class #### DEVICE_CLASS_CO2 = 'co2' +DEVICE_CLASS_DOOR = 'door' +DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' DEVICE_CLASS_HUMIDITY = 'humidity' DEVICE_CLASS_LIGHT = 'light' @@ -112,3 +114,4 @@ DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' DEVICE_CLASS_TEMPERATURE = 'temperature' +DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 6aa8d92c0af..7d7bbc5edd6 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -20,6 +20,7 @@ from .const import ( DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, + DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -29,13 +30,16 @@ _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_SERVICE_MAP = { DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), + DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED), DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), - DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)} + DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED), + DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)} @TYPES.register('TemperatureSensor') From 8a10fcd9852411cb3e209965e0286c540498f7c4 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Mon, 23 Apr 2018 18:00:16 +0200 Subject: [PATCH 467/924] deCONZ use forward entry setup (#13990) * Use forward entry setup with light platform * Move sensor to forward entry setup * Use forward entry setup with binary sensors * Use forward entry setup with scene platform * Remove import of unused functionality * Move deconz setup in to setup entry Create initial negative tests for setup entry * Fix hound comment * Improved tests * Add test for scene platform * Add test for binary sensor platform * Add test for light platform * Add test for light platform * Add test for sensor platform * Fix hound comment * More asserts on sensor types --- .../components/binary_sensor/__init__.py | 7 +- .../components/binary_sensor/deconz.py | 8 +- homeassistant/components/deconz/__init__.py | 31 +++---- homeassistant/components/light/deconz.py | 8 +- homeassistant/components/scene/__init__.py | 7 +- homeassistant/components/scene/deconz.py | 8 +- homeassistant/components/sensor/__init__.py | 7 +- homeassistant/components/sensor/deconz.py | 8 +- tests/components/binary_sensor/test_deconz.py | 55 +++++++++++++ tests/components/deconz/test_init.py | 42 +++++++++- tests/components/light/test_deconz.py | 74 +++++++++++++++++ tests/components/scene/test_deconz.py | 57 +++++++++++++ tests/components/sensor/test_deconz.py | 82 +++++++++++++++++++ 13 files changed, 358 insertions(+), 36 deletions(-) create mode 100644 tests/components/binary_sensor/test_deconz.py create mode 100644 tests/components/light/test_deconz.py create mode 100644 tests/components/scene/test_deconz.py create mode 100644 tests/components/sensor/test_deconz.py diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ad475be76ca..ee2a0ce712d 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -50,13 +50,18 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for binary sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + # pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index ef3ec506e3a..a9a3e28f4be 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -15,10 +15,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ binary sensor.""" - if discovery_info is None: - return + """Old way of setting up deCONZ binary sensors.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ binary sensor.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR sensors = hass.data[DATA_DECONZ].sensors entities = [] diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 064725eda95..d68edac9e59 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -9,8 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback -from homeassistant.helpers import ( - aiohttp_client, discovery, config_validation as cv) +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.util.json import load_json # Loading the config flow file will register the flow @@ -58,28 +57,20 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): - """Set up a deCONZ bridge for a config entry.""" - if DOMAIN in hass.data: - _LOGGER.error( - "Config entry failed since one deCONZ instance already exists") - return False - result = await async_setup_deconz(hass, None, entry.data) - if result: - return True - return False - - -async def async_setup_deconz(hass, config, deconz_config): - """Set up a deCONZ session. +async def async_setup_entry(hass, config_entry): + """Set up a deCONZ bridge for a config entry. Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ - _LOGGER.debug("deCONZ config %s", deconz_config) from pydeconz import DeconzSession + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **deconz_config) + deconz = DeconzSession(hass.loop, session, **config_entry.data) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -89,8 +80,8 @@ async def async_setup_deconz(hass, config, deconz_config): hass.data[DATA_DECONZ_ID] = {} for component in ['binary_sensor', 'light', 'scene', 'sensor']: - hass.async_add_job(discovery.async_load_platform( - hass, component, DOMAIN, {}, config)) + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + config_entry, component)) deconz.start() async def async_configure(call): diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 020f43d9935..36ad572a263 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -19,10 +19,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ light.""" - if discovery_info is None: - return + """Old way of setting up deCONZ lights.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ lights from a config entry.""" lights = hass.data[DATA_DECONZ].lights groups = hass.data[DATA_DECONZ].groups entities = [] diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8f0b9d5c7ab..a3e3a5b38a7 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -71,7 +71,7 @@ def activate(hass, entity_id=None): async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) - component = EntityComponent(logger, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) @@ -90,6 +90,11 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + class Scene(Entity): """A scene is a group of entities and the states we want them to be.""" diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index dffc7720776..3eb73736717 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -13,10 +13,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up scenes for deCONZ component.""" - if discovery_info is None: - return + """Old way of setting up deCONZ scenes.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up scenes for deCONZ component.""" scenes = hass.data[DATA_DECONZ].scenes entities = [] diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2bc35a034f4..2887d32b987 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -31,8 +31,13 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True + + +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index e569c5578ac..dc28a181aa0 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -22,10 +22,12 @@ ATTR_EVENT_ID = 'event_id' async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ sensors.""" - if discovery_info is None: - return + """Old way of setting up deCONZ sensors.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ sensors.""" from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE sensors = hass.data[DATA_DECONZ].sensors entities = [] diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py new file mode 100644 index 00000000000..84ed059e97e --- /dev/null +++ b/tests/components/binary_sensor/test_deconz.py @@ -0,0 +1,55 @@ +"""deCONZ binary sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ binary sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup( + config_entry, 'binary_sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index cbc8a373972..ce231e3d162 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,9 +1,11 @@ """Test deCONZ component setup process.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.setup import async_setup_component from homeassistant.components import deconz +from tests.common import mock_coro + async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" @@ -67,3 +69,41 @@ async def test_config_discovery(hass): assert await async_setup_component(hass, deconz.DOMAIN, {}) is True # No flow started assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_setup_entry_already_registered_bridge(hass): + """Test setup entry doesn't allow more than one instance of deCONZ.""" + hass.data[deconz.DOMAIN] = True + assert await deconz.async_setup_entry(hass, {}) is False + + +async def test_setup_entry_no_available_bridge(hass): + """Test setup entry fails if deCONZ is not available.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(False)): + assert await deconz.async_setup_entry(hass, entry) is False + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(hass, 'async_add_job') as mock_add_job, \ + patch.object(hass, 'config_entries') as mock_config_entries, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + assert hass.data[deconz.DOMAIN] + assert hass.data[deconz.DATA_DECONZ_ID] == {} + assert len(mock_add_job.mock_calls) == 4 + assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 + assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'binary_sensor') + assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \ + (entry, 'light') + assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \ + (entry, 'scene') + assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ + (entry, 'sensor') diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py new file mode 100644 index 00000000000..d907697354e --- /dev/null +++ b/tests/components/light/test_deconz.py @@ -0,0 +1,74 @@ +"""deCONZ light platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {} + } +} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [], + "lights": [ + "1", + "2" + ] + }, + "2": { + "id": "Group 2 id", + "name": "Group 2 name", + "state": {}, + "action": {}, + "scenes": [] + }, +} + + +async def setup_bridge(hass, data): + """Load the deCONZ light platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'light') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_lights_or_groups(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_lights_and_groups(hass): + """Test the update_lights function with some lights.""" + await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) + assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 3 diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py new file mode 100644 index 00000000000..53f25808be2 --- /dev/null +++ b/tests/components/scene/test_deconz.py @@ -0,0 +1,57 @@ +"""deCONZ scenes platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [{ + "id": "1", + "name": "Scene 1" + }], + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ scene platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'scene') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_scenes(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_scenes(hass): + """Test the update_lights function with some lights.""" + data = {"groups": GROUP} + await setup_bridge(hass, data) + assert "scene.group_1_name_scene_1" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py new file mode 100644 index 00000000000..b70fb396686 --- /dev/null +++ b/tests/components/sensor/test_deconz.py @@ -0,0 +1,82 @@ +"""deCONZ sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + }, + "3": { + "id": "Sensor 3 id", + "name": "Sensor 3 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {} + }, + "4": { + "id": "Sensor 4 id", + "name": "Sensor 4 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_sensors(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name_battery_level" not in \ + hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name_battery_level" in \ + hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 2 From 5ed73fecd3e67e381573bad8b9f03c5d8259a193 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Apr 2018 13:47:06 -0400 Subject: [PATCH 468/924] Order the output of the automation editor (#14019) * Order the output of the automation editor * Lint --- homeassistant/components/config/automation.py | 34 +++++++- tests/components/config/test_automation.py | 83 +++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tests/components/config/test_automation.py diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 6ede91e9b66..1e260854687 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,8 @@ """Provide configuration end points for Automations.""" import asyncio +from collections import OrderedDict +from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView from homeassistant.components.automation import ( PLATFORM_SCHEMA, DOMAIN, async_reload) @@ -13,8 +15,38 @@ CONFIG_PATH = 'automations.yaml' @asyncio.coroutine def async_setup(hass): """Set up the Automation config API.""" - hass.http.register_view(EditIdBasedConfigView( + hass.http.register_view(EditAutomationConfigView( DOMAIN, 'config', CONFIG_PATH, cv.string, PLATFORM_SCHEMA, post_write_hook=async_reload )) return True + + +class EditAutomationConfigView(EditIdBasedConfigView): + """Edit automation config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + index = None + for index, cur_value in enumerate(data): + if cur_value[CONF_ID] == config_key: + break + else: + cur_value = OrderedDict() + cur_value[CONF_ID] = config_key + index = len(data) + data.append(cur_value) + + # Iterate through some keys that we want to have ordered in the output + updated_value = OrderedDict() + for key in ('id', 'alias', 'trigger', 'condition', 'action'): + if key in cur_value: + updated_value[key] = cur_value[key] + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(cur_value) + updated_value.update(new_value) + data[index] = updated_value diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py new file mode 100644 index 00000000000..327283e74aa --- /dev/null +++ b/tests/components/config/test_automation.py @@ -0,0 +1,83 @@ +"""Test Automation config panel.""" +import json +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components import config + + +async def test_get_device_config(hass, aiohttp_client): + """Test getting device config.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + def mock_read(path): + """Mock reading data.""" + return [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + with patch('homeassistant.components.config._read', mock_read): + resp = await client.get( + '/api/config/automation/config/moon') + + assert resp.status == 200 + result = await resp.json() + + assert result == {'id': 'moon'} + + +async def test_update_device_config(hass, aiohttp_client): + """Test updating device config.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + assert list(orig_data[1]) == ['id', 'trigger', 'condition', 'action'] + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } + assert written[0] == orig_data From 31554e8368d7ce9e62831fddbefce22d2873dd4c Mon Sep 17 00:00:00 2001 From: John Mihalic Date: Mon, 23 Apr 2018 16:43:59 -0400 Subject: [PATCH 469/924] Bump pyEight version to update API & reduce connection issues (#14058) --- homeassistant/components/eight_sleep.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eight_sleep.py b/homeassistant/components/eight_sleep.py index 7ae4ec862bb..3478d5cd08e 100644 --- a/homeassistant/components/eight_sleep.py +++ b/homeassistant/components/eight_sleep.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -REQUIREMENTS = ['pyeight==0.0.7'] +REQUIREMENTS = ['pyeight==0.0.8'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7946a0d0955..9f398a37bdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -754,7 +754,7 @@ pyeconet==0.0.5 pyedimax==0.1 # homeassistant.components.eight_sleep -pyeight==0.0.7 +pyeight==0.0.8 # homeassistant.components.media_player.emby pyemby==1.5 From ca29224846e7ed8a71d5f8d504007eaabdabaf4c Mon Sep 17 00:00:00 2001 From: thelittlefireman Date: Tue, 24 Apr 2018 18:46:17 +0200 Subject: [PATCH 470/924] Bump locationsharinglib to 1.2.2 (#14070) * Bump locationsharinglib to 1.2.2 * Bump locationsharinglib to 1.2.2 --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index b594b23dbeb..1d0058ed229 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==1.2.1'] +REQUIREMENTS = ['locationsharinglib==1.2.2'] CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' diff --git a/requirements_all.txt b/requirements_all.txt index 9f398a37bdf..86450c529d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -503,7 +503,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==1.2.1 +locationsharinglib==1.2.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From 18137733f91d9a2f8d67133eab91bbe8508b9da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 25 Apr 2018 04:45:16 +0200 Subject: [PATCH 471/924] Upgrade broadlink lib (#14074) --- homeassistant/components/sensor/broadlink.py | 2 +- homeassistant/components/switch/broadlink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 5182ba4530e..9376687cf13 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['broadlink==0.8.0'] +REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 50c334b1f09..46002112177 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, slugify from homeassistant.util.dt import utcnow -REQUIREMENTS = ['broadlink==0.8.0'] +REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 86450c529d5..c5c206f9a62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -180,7 +180,7 @@ botocore==1.7.34 # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink -broadlink==0.8.0 +broadlink==0.9.0 # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 From 4e97954bbed1a7cc8443bf6f7fa7ef68ebc0103a Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Wed, 25 Apr 2018 04:45:57 +0200 Subject: [PATCH 472/924] Remove excessive debugging in webostv module (#14056) --- .../components/media_player/webostv.py | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/media_player/webostv.py b/homeassistant/components/media_player/webostv.py index d7682a611b9..c3426e45404 100644 --- a/homeassistant/components/media_player/webostv.py +++ b/homeassistant/components/media_player/webostv.py @@ -352,33 +352,30 @@ class LgWebOSDevice(MediaPlayerDevice): if media_type == MEDIA_TYPE_CHANNEL: _LOGGER.debug("Searching channel...") partial_match_channel_id = None + perfect_match_channel_id = None for channel in self._client.get_channels(): - _LOGGER.debug( - "Checking channel number <%s>, name <%s>, id <%s>...", - channel['channelNumber'], - channel['channelName'], - channel['channelId']) if media_id == channel['channelNumber']: - _LOGGER.debug( - "Perfect match on channel number: switching!") - self._client.set_channel(channel['channelId']) - return + perfect_match_channel_id = channel['channelId'] + continue elif media_id.lower() == channel['channelName'].lower(): - _LOGGER.debug( - "Perfect match on channel name: switching!") - self._client.set_channel(channel['channelId']) - return + perfect_match_channel_id = channel['channelId'] + continue elif media_id.lower() in channel['channelName'].lower(): - _LOGGER.debug( - "Partial match on channel name: saving it...") partial_match_channel_id = channel['channelId'] - if partial_match_channel_id is not None: - _LOGGER.debug( - "Using partial match on channel name: switching!") + if perfect_match_channel_id is not None: + _LOGGER.info( + "Switching to channel <%s> with perfect match", + perfect_match_channel_id) + self._client.set_channel(perfect_match_channel_id) + elif partial_match_channel_id is not None: + _LOGGER.info( + "Switching to channel <%s> with partial match", + partial_match_channel_id) self._client.set_channel(partial_match_channel_id) - return + + return def media_play(self): """Send play command.""" From 75fffb6a860e01fe0828723f761e96ebd0ae4536 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Apr 2018 23:18:28 -0400 Subject: [PATCH 473/924] Bump frontend to 20180425.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 87ca8bd2a28..ba487a935a2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180420.0'] +REQUIREMENTS = ['home-assistant-frontend==20180425.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index c5c206f9a62..29783c7ab16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180420.0 +home-assistant-frontend==20180425.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf4aa2e1b3c..779b304f490 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180420.0 +home-assistant-frontend==20180425.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 8cb1e17ad85f26e861e5e63f2c7067d34f551fc4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Apr 2018 23:18:28 -0400 Subject: [PATCH 474/924] Bump frontend to 20180425.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 87ca8bd2a28..ba487a935a2 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180420.0'] +REQUIREMENTS = ['home-assistant-frontend==20180425.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index aeb5b84811e..57197443779 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180420.0 +home-assistant-frontend==20180425.0 # homeassistant.components.homekit_controller # homekit==0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d371996e36..23ffab2fc78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180420.0 +home-assistant-frontend==20180425.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 44be80145b55deda65b2e655fe0bd2cedc2eb8ad Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Sat, 21 Apr 2018 08:34:42 +0200 Subject: [PATCH 475/924] Qwikswitch binary sensors (#14008) --- .../components/binary_sensor/qwikswitch.py | 70 +++++++++++++++++++ homeassistant/components/qwikswitch.py | 44 ++++++++---- homeassistant/components/sensor/qwikswitch.py | 12 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../{sensor => }/test_qwikswitch.py | 70 +++++++++++++------ 6 files changed, 160 insertions(+), 40 deletions(-) create mode 100644 homeassistant/components/binary_sensor/qwikswitch.py rename tests/components/{sensor => }/test_qwikswitch.py (55%) diff --git a/homeassistant/components/binary_sensor/qwikswitch.py b/homeassistant/components/binary_sensor/qwikswitch.py new file mode 100644 index 00000000000..067021b0c7a --- /dev/null +++ b/homeassistant/components/binary_sensor/qwikswitch.py @@ -0,0 +1,70 @@ +""" +Support for Qwikswitch Binary Sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.qwikswitch/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.qwikswitch import QSEntity, DOMAIN as QWIKSWITCH +from homeassistant.core import callback + +DEPENDENCIES = [QWIKSWITCH] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, _, add_devices, discovery_info=None): + """Add binary sensor from the main Qwikswitch component.""" + if discovery_info is None: + return + + qsusb = hass.data[QWIKSWITCH] + _LOGGER.debug("Setup qwikswitch.binary_sensor %s, %s", + qsusb, discovery_info) + devs = [QSBinarySensor(sensor) for sensor in discovery_info[QWIKSWITCH]] + add_devices(devs) + + +class QSBinarySensor(QSEntity, BinarySensorDevice): + """Sensor based on a Qwikswitch relay/dimmer module.""" + + _val = False + + def __init__(self, sensor): + """Initialize the sensor.""" + from pyqwikswitch import SENSORS + + super().__init__(sensor['id'], sensor['name']) + self.channel = sensor['channel'] + sensor_type = sensor['type'] + + self._decode, _ = SENSORS[sensor_type] + self._invert = not sensor.get('invert', False) + self._class = sensor.get('class', 'door') + + @callback + def update_packet(self, packet): + """Receive update packet from QSUSB.""" + val = self._decode(packet, channel=self.channel) + _LOGGER.debug("Update %s (%s:%s) decoded as %s: %s", + self.entity_id, self.qsid, self.channel, val, packet) + if val is not None: + self._val = bool(val) + self.async_schedule_update_ha_state() + + @property + def is_on(self): + """Check if device is on (non-zero).""" + return self._val == self._invert + + @property + def unique_id(self): + """Return a unique identifier for this sensor.""" + return "qs{}:{}".format(self.qsid, self.channel) + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._class diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index 3dc16f513dc..f26318fa7a9 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -8,17 +8,18 @@ import logging import voluptuous as vol +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, CONF_URL, - CONF_SENSORS, CONF_SWITCHES) + CONF_SENSORS, CONF_SWITCHES, CONF_URL, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -from homeassistant.components.light import ATTR_BRIGHTNESS -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyqwikswitch==0.71'] +REQUIREMENTS = ['pyqwikswitch==0.8'] _LOGGER = logging.getLogger(__name__) @@ -28,6 +29,7 @@ CONF_DIMMER_ADJUST = 'dimmer_adjust' CONF_BUTTON_EVENTS = 'button_events' CV_DIM_VALUE = vol.All(vol.Coerce(float), vol.Range(min=1, max=3)) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Required(CONF_URL, default='http://127.0.0.1:2020'): @@ -40,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional('channel', default=1): int, vol.Required('name'): str, vol.Required('type'): str, + vol.Optional('class'): DEVICE_CLASSES_SCHEMA, + vol.Optional('invert'): bool })]), vol.Optional(CONF_SWITCHES, default=[]): vol.All( cv.ensure_list, [str]) @@ -115,7 +119,7 @@ class QSToggleEntity(QSEntity): async def async_setup(hass, config): """Qwiskswitch component setup.""" from pyqwikswitch.async_ import QSUsb - from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType + from pyqwikswitch import CMD_BUTTONS, QS_CMD, QS_ID, QSType, SENSORS # Add cmd's to in /&listen packets will fire events # By default only buttons of type [TOGGLE,SCENE EXE,LEVEL] @@ -143,22 +147,39 @@ async def async_setup(hass, config): hass.data[DOMAIN] = qsusb - _new = {'switch': [], 'light': [], 'sensor': sensors} + comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []} + + try: + for sens in sensors: + _, _type = SENSORS[sens['type']] + if _type is bool: + comps['binary_sensor'].append(sens) + continue + comps['sensor'].append(sens) + for _key in ('invert', 'class'): + if _key in sens: + _LOGGER.warning( + "%s should only be used for binary_sensors: %s", + _key, sens) + + except KeyError: + _LOGGER.warning("Sensor validation failed") + for qsid, dev in qsusb.devices.items(): if qsid in switches: if dev.qstype != QSType.relay: _LOGGER.warning( "You specified a switch that is not a relay %s", qsid) continue - _new['switch'].append(qsid) + comps['switch'].append(qsid) elif dev.qstype in (QSType.relay, QSType.dimmer): - _new['light'].append(qsid) + comps['light'].append(qsid) else: _LOGGER.warning("Ignored unknown QSUSB device: %s", dev) continue # Load platforms - for comp_name, comp_conf in _new.items(): + for comp_name, comp_conf in comps.items(): if comp_conf: load_platform(hass, comp_name, DOMAIN, {DOMAIN: comp_conf}, config) @@ -190,9 +211,8 @@ async def async_setup(hass, config): @callback def async_stop(_): - """Stop the listener queue and clean up.""" + """Stop the listener.""" hass.data[DOMAIN].stop() - _LOGGER.info("Waiting for long poll to QSUSB to time out (max 30sec)") hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, async_stop) diff --git a/homeassistant/components/sensor/qwikswitch.py b/homeassistant/components/sensor/qwikswitch.py index ebd5f5254d4..1497b4ad5cc 100644 --- a/homeassistant/components/sensor/qwikswitch.py +++ b/homeassistant/components/sensor/qwikswitch.py @@ -36,18 +36,18 @@ class QSSensor(QSEntity): super().__init__(sensor['id'], sensor['name']) self.channel = sensor['channel'] - self.sensor_type = sensor['type'] + sensor_type = sensor['type'] - self._decode, self.unit = SENSORS[self.sensor_type] + self._decode, self.unit = SENSORS[sensor_type] if isinstance(self.unit, type): - self.unit = "{}:{}".format(self.sensor_type, self.channel) + self.unit = "{}:{}".format(sensor_type, self.channel) @callback def update_packet(self, packet): """Receive update packet from QSUSB.""" - val = self._decode(packet.get('data'), channel=self.channel) - _LOGGER.debug("Update %s (%s) decoded as %s: %s: %s", - self.entity_id, self.qsid, val, self.channel, packet) + val = self._decode(packet, channel=self.channel) + _LOGGER.debug("Update %s (%s:%s) decoded as %s: %s", + self.entity_id, self.qsid, self.channel, val, packet) if val is not None: self._val = val self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 57197443779..579d8914d66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -898,7 +898,7 @@ pyowm==2.8.0 pypollencom==1.1.2 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23ffab2fc78..779b304f490 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -149,7 +149,7 @@ pymonoprice==0.3 pynx584==0.4 # homeassistant.components.qwikswitch -pyqwikswitch==0.71 +pyqwikswitch==0.8 # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky diff --git a/tests/components/sensor/test_qwikswitch.py b/tests/components/test_qwikswitch.py similarity index 55% rename from tests/components/sensor/test_qwikswitch.py rename to tests/components/test_qwikswitch.py index d9dfe072fc0..76655f32816 100644 --- a/tests/components/sensor/test_qwikswitch.py +++ b/tests/components/test_qwikswitch.py @@ -13,17 +13,19 @@ _LOGGER = logging.getLogger(__name__) class AiohttpClientMockResponseList(list): - """List that fires an event on empty pop, for aiohttp Mocker.""" + """Return multiple values for aiohttp Mocker. + + aoihttp mocker uses decode to fetch the next value. + """ def decode(self, _): """Return next item from list.""" try: - res = list.pop(self) + res = list.pop(self, 0) _LOGGER.debug("MockResponseList popped %s: %s", res, self) return res except IndexError: - _LOGGER.debug("MockResponseList empty") - return "" + raise AssertionError("MockResponseList empty") async def wait_till_empty(self, hass): """Wait until empty.""" @@ -52,8 +54,8 @@ def aioclient_mock(): yield mock_session -async def test_sensor_device(hass, aioclient_mock): - """Test a sensor device.""" +async def test_binary_sensor_device(hass, aioclient_mock): + """Test a binary sensor device.""" config = { 'qwikswitch': { 'sensors': { @@ -67,21 +69,49 @@ async def test_sensor_device(hass, aioclient_mock): await async_setup_component(hass, QWIKSWITCH, config) await hass.async_block_till_done() - state_obj = hass.states.get('sensor.s1') - assert state_obj - assert state_obj.state == 'None' + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'off' + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - LISTEN.append( # Close - """{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}""") + LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1601","rssi":"61%"}') + LISTEN.append('') # Will cause a sleep await hass.async_block_till_done() - state_obj = hass.states.get('sensor.s1') - assert state_obj.state == 'True' + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'on' - # Causes a 30second delay: can be uncommented when upstream library - # allows cancellation of asyncio.sleep(30) on failed packet ("") - # LISTEN.append( # Open - # """{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}""") - # await LISTEN.wait_till_empty(hass) - # state_obj = hass.states.get('sensor.s1') - # assert state_obj.state == 'False' + LISTEN.append('{"id":"@a00001","cmd":"","data":"4e0e1701","rssi":"61%"}') + hass.data[QWIKSWITCH]._sleep_task.cancel() + await LISTEN.wait_till_empty(hass) + state_obj = hass.states.get('binary_sensor.s1') + assert state_obj.state == 'off' + + +async def test_sensor_device(hass, aioclient_mock): + """Test a sensor device.""" + config = { + 'qwikswitch': { + 'sensors': { + 'name': 'ss1', + 'id': '@a00001', + 'channel': 1, + 'type': 'qwikcord', + } + } + } + await async_setup_component(hass, QWIKSWITCH, config) + await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.ss1') + assert state_obj.state == 'None' + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + LISTEN.append( + '{"id":"@a00001","name":"ss1","type":"rel",' + '"val":"4733800001a00000"}') + LISTEN.append('') # Will cause a sleep + await LISTEN.wait_till_empty(hass) # await hass.async_block_till_done() + + state_obj = hass.states.get('sensor.ss1') + assert state_obj.state == 'None' From 2bc87bfcf0ec2dc1ea45c05325431153afae0517 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Apr 2018 13:47:06 -0400 Subject: [PATCH 476/924] Order the output of the automation editor (#14019) * Order the output of the automation editor * Lint --- homeassistant/components/config/automation.py | 34 +++++++- tests/components/config/test_automation.py | 83 +++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 tests/components/config/test_automation.py diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 6ede91e9b66..1e260854687 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,8 @@ """Provide configuration end points for Automations.""" import asyncio +from collections import OrderedDict +from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView from homeassistant.components.automation import ( PLATFORM_SCHEMA, DOMAIN, async_reload) @@ -13,8 +15,38 @@ CONFIG_PATH = 'automations.yaml' @asyncio.coroutine def async_setup(hass): """Set up the Automation config API.""" - hass.http.register_view(EditIdBasedConfigView( + hass.http.register_view(EditAutomationConfigView( DOMAIN, 'config', CONFIG_PATH, cv.string, PLATFORM_SCHEMA, post_write_hook=async_reload )) return True + + +class EditAutomationConfigView(EditIdBasedConfigView): + """Edit automation config.""" + + def _write_value(self, hass, data, config_key, new_value): + """Set value.""" + index = None + for index, cur_value in enumerate(data): + if cur_value[CONF_ID] == config_key: + break + else: + cur_value = OrderedDict() + cur_value[CONF_ID] = config_key + index = len(data) + data.append(cur_value) + + # Iterate through some keys that we want to have ordered in the output + updated_value = OrderedDict() + for key in ('id', 'alias', 'trigger', 'condition', 'action'): + if key in cur_value: + updated_value[key] = cur_value[key] + if key in new_value: + updated_value[key] = new_value[key] + + # We cover all current fields above, but just in case we start + # supporting more fields in the future. + updated_value.update(cur_value) + updated_value.update(new_value) + data[index] = updated_value diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py new file mode 100644 index 00000000000..327283e74aa --- /dev/null +++ b/tests/components/config/test_automation.py @@ -0,0 +1,83 @@ +"""Test Automation config panel.""" +import json +from unittest.mock import patch + +from homeassistant.bootstrap import async_setup_component +from homeassistant.components import config + + +async def test_get_device_config(hass, aiohttp_client): + """Test getting device config.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + def mock_read(path): + """Mock reading data.""" + return [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + with patch('homeassistant.components.config._read', mock_read): + resp = await client.get( + '/api/config/automation/config/moon') + + assert resp.status == 200 + result = await resp.json() + + assert result == {'id': 'moon'} + + +async def test_update_device_config(hass, aiohttp_client): + """Test updating device config.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + assert list(orig_data[1]) == ['id', 'trigger', 'condition', 'action'] + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } + assert written[0] == orig_data From cb839eff0fc8a36d8bd869c1830d424f567a4aeb Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sat, 21 Apr 2018 10:16:46 -0400 Subject: [PATCH 477/924] HomeKit Alarm Control Panel Code Exception Fix (#14025) * Catch exception for KeyError * Use get and added test --- .../components/homekit/type_security_systems.py | 2 +- .../components/homekit/test_type_security_systems.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 6b8457a3aa5..0762e0f25f9 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -30,7 +30,7 @@ class SecuritySystem(HomeAccessory): def __init__(self, *args, config): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) - self._alarm_code = config[ATTR_CODE] + self._alarm_code = config.get(ATTR_CODE) self.flag_target_state = False serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index ec538ce4b50..9c1ff0faf1a 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -109,8 +109,16 @@ class TestHomekitSecuritySystems(unittest.TestCase): acc = SecuritySystem(self.hass, 'SecuritySystem', acp, 2, config={ATTR_CODE: None}) - acc.run() - + # Set from HomeKit + acc.char_target_state.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') + self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) + self.assertEqual(acc.char_target_state.value, 0) + + acc = SecuritySystem(self.hass, 'SecuritySystem', acp, + 2, config={}) # Set from HomeKit acc.char_target_state.client_update_value(0) self.hass.block_till_done() From fc1f6ee0f0a0fd70c2f35dc0282b9316fe8c25d6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 22 Apr 2018 22:32:15 +0200 Subject: [PATCH 478/924] Revert cast platform polling mode (#14027) --- homeassistant/components/media_player/cast.py | 64 +++----------- tests/components/media_player/test_cast.py | 85 +------------------ 2 files changed, 13 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 30d4bd166d0..632ab4214b8 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -288,8 +288,7 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None # type: Optional[pychromecast.Chromecast] self.cast_status = None self.media_status = None - self.media_status_position = None - self.media_status_position_received = None + self.media_status_received = None self._available = False # type: bool self._status_listener = None # type: Optional[CastStatusListener] @@ -362,26 +361,10 @@ class CastDevice(MediaPlayerDevice): self._chromecast = None self.cast_status = None self.media_status = None - self.media_status_position = None - self.media_status_position_received = None + self.media_status_received = None self._status_listener.invalidate() self._status_listener = None - def update(self): - """Periodically update the properties. - - Even though we receive callbacks for most state changes, some 3rd party - apps don't always send them. Better poll every now and then if the - chromecast is active (i.e. an app is running). - """ - if not self._available: - # Not connected or not available. - return - - if self._chromecast.media_controller.is_active: - # We can only update status if the media namespace is active - self._chromecast.media_controller.update_status() - # ========== Callbacks ========== def new_cast_status(self, cast_status): """Handle updates of the cast status.""" @@ -390,36 +373,8 @@ class CastDevice(MediaPlayerDevice): def new_media_status(self, media_status): """Handle updates of the media status.""" - # Only use media position for playing/paused, - # and for normal playback rate - if (media_status is None or - abs(media_status.playback_rate - 1) > 0.01 or - not (media_status.player_is_playing or - media_status.player_is_paused)): - self.media_status_position = None - self.media_status_position_received = None - else: - # Avoid unnecessary state attribute updates if player_state and - # calculated position stay the same - now = dt_util.utcnow() - do_update = \ - (self.media_status is None or - self.media_status_position is None or - self.media_status.player_state != media_status.player_state) - if not do_update: - if media_status.player_is_playing: - elapsed = now - self.media_status_position_received - do_update = abs(media_status.current_time - - (self.media_status_position + - elapsed.total_seconds())) > 1 - else: - do_update = \ - self.media_status_position != media_status.current_time - if do_update: - self.media_status_position = media_status.current_time - self.media_status_position_received = now - self.media_status = media_status + self.media_status_received = dt_util.utcnow() self.schedule_update_ha_state() def new_connection_status(self, connection_status): @@ -496,8 +451,8 @@ class CastDevice(MediaPlayerDevice): # ========== Properties ========== @property def should_poll(self): - """Polling needed for cast integration, see async_update.""" - return True + """No polling needed.""" + return False @property def name(self): @@ -625,7 +580,12 @@ class CastDevice(MediaPlayerDevice): @property def media_position(self): """Position of current playing media in seconds.""" - return self.media_status_position + if self.media_status is None or \ + not (self.media_status.player_is_playing or + self.media_status.player_is_paused or + self.media_status.player_is_idle): + return None + return self.media_status.current_time @property def media_position_updated_at(self): @@ -633,7 +593,7 @@ class CastDevice(MediaPlayerDevice): Returns value from homeassistant.util.dt.utcnow(). """ - return self.media_status_position_received + return self.media_status_received @property def unique_id(self) -> Optional[str]: diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index 0c0f3906dc2..ee69ec1c85d 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -1,7 +1,6 @@ """The tests for the Cast Media player platform.""" # pylint: disable=protected-access import asyncio -import datetime as dt from typing import Optional from unittest.mock import patch, MagicMock, Mock from uuid import UUID @@ -15,8 +14,7 @@ from homeassistant.components.media_player.cast import ChromecastInfo from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.dispatcher import async_dispatcher_connect, \ async_dispatcher_send -from homeassistant.components.media_player import cast, \ - ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT +from homeassistant.components.media_player import cast from homeassistant.setup import async_setup_component @@ -288,8 +286,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert entity.unique_id == full_info.uuid media_status = MagicMock(images=None) - media_status.current_time = 0 - media_status.playback_rate = 1 media_status.player_is_playing = True entity.new_media_status(media_status) await hass.async_block_till_done() @@ -324,85 +320,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == 'unknown' -async def test_entity_media_position(hass: HomeAssistantType): - """Test various entity media states.""" - info = get_fake_chromecast_info() - full_info = attr.evolve(info, model_name='google home', - friendly_name='Speaker', uuid=FakeUUID) - - with patch('pychromecast.dial.get_device_status', - return_value=full_info): - chromecast, entity = await async_setup_media_player_cast(hass, info) - - media_status = MagicMock(images=None) - media_status.current_time = 10 - media_status.playback_rate = 1 - media_status.player_is_playing = True - media_status.player_is_paused = False - media_status.player_is_idle = False - now = dt.datetime.now(dt.timezone.utc) - with patch('homeassistant.util.dt.utcnow', return_value=now): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 10 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now - - media_status.current_time = 15 - now_plus_5 = now + dt.timedelta(seconds=5) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 10 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now - - media_status.current_time = 20 - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_5): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 20 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_5 - - media_status.current_time = 25 - now_plus_10 = now + dt.timedelta(seconds=10) - media_status.player_is_playing = False - media_status.player_is_paused = True - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_10): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 25 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 - - now_plus_15 = now + dt.timedelta(seconds=15) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_15): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 25 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_10 - - media_status.current_time = 30 - now_plus_20 = now + dt.timedelta(seconds=20) - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert state.attributes[ATTR_MEDIA_POSITION] == 30 - assert state.attributes[ATTR_MEDIA_POSITION_UPDATED_AT] == now_plus_20 - - media_status.player_is_paused = False - media_status.player_is_idle = True - with patch('homeassistant.util.dt.utcnow', return_value=now_plus_20): - entity.new_media_status(media_status) - await hass.async_block_till_done() - state = hass.states.get('media_player.speaker') - assert ATTR_MEDIA_POSITION not in state.attributes - assert ATTR_MEDIA_POSITION_UPDATED_AT not in state.attributes - - async def test_switched_host(hass: HomeAssistantType): """Test cast device listens for changed hosts and disconnects old cast.""" info = get_fake_chromecast_info() From 7566bb5aed499449446ecb0a6de45dd70d700b76 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 22 Apr 2018 13:38:01 -0700 Subject: [PATCH 479/924] Handle HomeKit configuration failure more cleanly (#14041) * Handle HomeKit configuration failure more cleanly Add support for handling cases where HomeKit configuration fails, and give the user more information about what to do. * Don't consume the exception for a homekit.UnknownError If we get an UnknownError then we should alert the user but also still generate the backtrace so there's actually something for them to file in a bug report. --- .../components/homekit_controller/__init__.py | 27 ++++++++++++++++--- requirements_all.txt | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index c33edd07918..164e7d50e4d 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -14,7 +14,7 @@ from homeassistant.components.discovery import SERVICE_HOMEKIT from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['homekit==0.5'] +REQUIREMENTS = ['homekit==0.6'] DOMAIN = 'homekit_controller' HOMEKIT_DIR = '.homekit' @@ -133,10 +133,31 @@ class HKDevice(): import homekit pairing_id = str(uuid.uuid4()) code = callback_data.get('code').strip() - self.pairing_data = homekit.perform_pair_setup( - self.conn, code, pairing_id) + try: + self.pairing_data = homekit.perform_pair_setup(self.conn, code, + pairing_id) + except homekit.exception.UnavailableError: + error_msg = "This accessory is already paired to another device. \ + Please reset the accessory and try again." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.exception.AuthenticationError: + error_msg = "Incorrect HomeKit code for {}. Please check it and \ + try again.".format(self.model) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + return + except homekit.exception.UnknownError: + error_msg = "Received an unknown error. Please file a bug." + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.notify_errors(_configurator, error_msg) + raise + if self.pairing_data is not None: homekit.save_pairing(self.pairing_file, self.pairing_data) + _configurator = self.hass.data[DOMAIN+self.hkid] + self.configurator.request_done(_configurator) self.accessory_setup() else: error_msg = "Unable to pair, please try again" diff --git a/requirements_all.txt b/requirements_all.txt index 579d8914d66..5070f904b43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ holidays==0.9.4 home-assistant-frontend==20180425.0 # homeassistant.components.homekit_controller -# homekit==0.5 +# homekit==0.6 # homeassistant.components.homematicip_cloud homematicip==0.8 From 2e3a27e418e9f149de99ed55cefe0cead7d4267f Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Mon, 23 Apr 2018 07:52:39 -0400 Subject: [PATCH 480/924] Update device classes for contact sensor HomeKit (#14051) --- homeassistant/components/homekit/const.py | 3 +++ homeassistant/components/homekit/type_sensors.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 1c498b4b3b9..59444c75421 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -102,6 +102,8 @@ PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Class #### DEVICE_CLASS_CO2 = 'co2' +DEVICE_CLASS_DOOR = 'door' +DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' DEVICE_CLASS_HUMIDITY = 'humidity' DEVICE_CLASS_LIGHT = 'light' @@ -112,3 +114,4 @@ DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' DEVICE_CLASS_TEMPERATURE = 'temperature' +DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 6aa8d92c0af..7d7bbc5edd6 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -20,6 +20,7 @@ from .const import ( DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, + DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -29,13 +30,16 @@ _LOGGER = logging.getLogger(__name__) BINARY_SENSOR_SERVICE_MAP = { DEVICE_CLASS_CO2: (SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED), + DEVICE_CLASS_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), + DEVICE_CLASS_GARAGE_DOOR: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), DEVICE_CLASS_GAS: (SERV_CARBON_MONOXIDE_SENSOR, CHAR_CARBON_MONOXIDE_DETECTED), DEVICE_CLASS_MOISTURE: (SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED), DEVICE_CLASS_MOTION: (SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED), DEVICE_CLASS_OCCUPANCY: (SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED), DEVICE_CLASS_OPENING: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE), - DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED)} + DEVICE_CLASS_SMOKE: (SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED), + DEVICE_CLASS_WINDOW: (SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE)} @TYPES.register('TemperatureSensor') From c49751542fff78711727dd0a0beb524a4e4670b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 Apr 2018 23:19:33 -0400 Subject: [PATCH 481/924] Version bump to 0.68.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 56e37e5e039..eed6664bd0a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 68 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 0a0d34d394e0f460cc772d79834a5aaf8ac907de Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Wed, 25 Apr 2018 13:05:00 +0800 Subject: [PATCH 482/924] Support new Xiaomi Aqara device model names and LAN protocol 2.0 (#13540) --- .../components/binary_sensor/xiaomi_aqara.py | 29 ++++++++++++------- .../components/light/xiaomi_aqara.py | 2 +- .../components/sensor/xiaomi_aqara.py | 4 +-- .../components/switch/xiaomi_aqara.py | 8 +++-- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 2ed0de66b18..49f716b9eb7 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -25,30 +25,35 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices['binary_sensor']: model = device['model'] - if model in ['motion', 'sensor_motion.aq2']: + if model in ['motion', 'sensor_motion', 'sensor_motion.aq2']: devices.append(XiaomiMotionSensor(device, hass, gateway)) - elif model in ['magnet', 'sensor_magnet.aq2']: + elif model in ['magnet', 'sensor_magnet', 'sensor_magnet.aq2']: devices.append(XiaomiDoorSensor(device, gateway)) elif model == 'sensor_wleak.aq1': devices.append(XiaomiWaterLeakSensor(device, gateway)) - elif model == 'smoke': + elif model in ['smoke', 'sensor_smoke']: devices.append(XiaomiSmokeSensor(device, gateway)) - elif model == 'natgas': + elif model in ['natgas', 'sensor_natgas']: devices.append(XiaomiNatgasSensor(device, gateway)) - elif model in ['switch', 'sensor_switch.aq2', 'sensor_switch.aq3']: - devices.append(XiaomiButton(device, 'Switch', 'status', + elif model in ['switch', 'sensor_switch', + 'sensor_switch.aq2', 'sensor_switch.aq3']: + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' + devices.append(XiaomiButton(device, 'Switch', data_key, hass, gateway)) - elif model == '86sw1': + elif model in ['86sw1', 'sensor_86sw1.aq1']: devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', hass, gateway)) - elif model == '86sw2': + elif model in ['86sw2', 'sensor_86sw2.aq1']: devices.append(XiaomiButton(device, 'Wall Switch (Left)', 'channel_0', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Right)', 'channel_1', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Both)', 'dual_channel', hass, gateway)) - elif model == 'cube': + elif model in ['cube', 'sensor_cube']: devices.append(XiaomiCube(device, hass, gateway)) add_devices(devices) @@ -129,8 +134,12 @@ class XiaomiMotionSensor(XiaomiBinarySensor): """Initialize the XiaomiMotionSensor.""" self._hass = hass self._no_motion_since = 0 + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'motion_status' XiaomiBinarySensor.__init__(self, device, 'Motion Sensor', xiaomi_hub, - 'status', 'motion') + data_key, 'motion') @property def device_state_attributes(self): diff --git a/homeassistant/components/light/xiaomi_aqara.py b/homeassistant/components/light/xiaomi_aqara.py index 125e791829f..37ae60e3494 100644 --- a/homeassistant/components/light/xiaomi_aqara.py +++ b/homeassistant/components/light/xiaomi_aqara.py @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices['light']: model = device['model'] - if model == 'gateway': + if model in ['gateway', 'gateway.v3']: devices.append(XiaomiGatewayLight(device, 'Gateway Light', gateway)) add_devices(devices) diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 33bbdc32308..497a3915154 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -26,7 +26,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'temperature', gateway)) devices.append(XiaomiSensor(device, 'Humidity', 'humidity', gateway)) - elif device['model'] == 'weather.v1': + elif device['model'] in ['weather', 'weather.v1']: devices.append(XiaomiSensor(device, 'Temperature', 'temperature', gateway)) devices.append(XiaomiSensor(device, 'Humidity', @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): elif device['model'] == 'sensor_motion.aq2': devices.append(XiaomiSensor(device, 'Illumination', 'lux', gateway)) - elif device['model'] == 'gateway': + elif device['model'] in ['gateway', 'gateway.v3', 'acpartner.v3']: devices.append(XiaomiSensor(device, 'Illumination', 'illumination', gateway)) add_devices(devices) diff --git a/homeassistant/components/switch/xiaomi_aqara.py b/homeassistant/components/switch/xiaomi_aqara.py index 939fc70660a..4c44d6b2592 100644 --- a/homeassistant/components/switch/xiaomi_aqara.py +++ b/homeassistant/components/switch/xiaomi_aqara.py @@ -26,7 +26,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device in gateway.devices['switch']: model = device['model'] if model == 'plug': - devices.append(XiaomiGenericSwitch(device, "Plug", 'status', + if 'proto' not in device or int(device['proto'][0:1]) == 1: + data_key = 'status' + else: + data_key = 'channel_0' + devices.append(XiaomiGenericSwitch(device, "Plug", data_key, True, gateway)) elif model in ['ctrl_neutral1', 'ctrl_neutral1.aq1']: devices.append(XiaomiGenericSwitch(device, 'Wall Switch', @@ -52,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'Wall Switch LN Right', 'channel_1', False, gateway)) - elif model in ['86plug', 'ctrl_86plug.aq1']: + elif model in ['86plug', 'ctrl_86plug', 'ctrl_86plug.aq1']: devices.append(XiaomiGenericSwitch(device, 'Wall Plug', 'status', True, gateway)) add_devices(devices) From 558b659f7caad4027e5d696dfa4d581cf5240a41 Mon Sep 17 00:00:00 2001 From: Thijs de Jong Date: Wed, 25 Apr 2018 07:09:45 +0200 Subject: [PATCH 483/924] Add devices to Tahoma (#14075) --- homeassistant/components/cover/tahoma.py | 2 ++ homeassistant/components/sensor/tahoma.py | 5 +++++ homeassistant/components/tahoma.py | 2 ++ 3 files changed, 9 insertions(+) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index c99076de851..20625143daf 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -79,5 +79,7 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.tahoma_device.type == \ 'io:RollerShutterWithLowSpeedManagementIOComponent': self.apply_action('setPosition', 'secured') + elif self.tahoma_device.type == 'rts:BlindRTSComponent': + self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index 39d1cbc75a3..cafa942f65b 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -46,6 +46,8 @@ class TahomaSensor(TahomaDevice, Entity): """Return the unit of measurement of this entity, if any.""" if self.tahoma_device.type == 'Temperature Sensor': return None + elif self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + return None elif self.tahoma_device.type == 'io:LightIOSystemSensor': return 'lux' elif self.tahoma_device.type == 'Humidity Sensor': @@ -57,3 +59,6 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == 'io:LightIOSystemSensor': self.current_value = self.tahoma_device.active_states[ 'core:LuminanceState'] + if self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': + self.current_value = self.tahoma_device.active_states[ + 'core:ContactState'] diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 055e3f410ea..9848d20094c 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -38,6 +38,8 @@ TAHOMA_COMPONENTS = [ TAHOMA_TYPES = { 'rts:RollerShutterRTSComponent': 'cover', 'rts:CurtainRTSComponent': 'cover', + 'rts:BlindRTSComponent': 'cover', + 'rts:VenetianBlindRTSComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', 'io:RollerShutterVeluxIOComponent': 'cover', 'io:RollerShutterGenericIOComponent': 'cover', From f23f9465d3072a46e4031e9de250674caf99381d Mon Sep 17 00:00:00 2001 From: Mitko Masarliev Date: Wed, 25 Apr 2018 13:33:47 +0300 Subject: [PATCH 484/924] New sensor domain expiry (#14067) * domain expiry * domain expiry * domain expiry * scan interval * change host to domain --- .coveragerc | 1 + .../components/sensor/domain_expiry.py | 76 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 80 insertions(+) create mode 100644 homeassistant/components/sensor/domain_expiry.py diff --git a/.coveragerc b/.coveragerc index eae6498cd0a..452dbec7559 100644 --- a/.coveragerc +++ b/.coveragerc @@ -574,6 +574,7 @@ omit = homeassistant/components/sensor/discogs.py homeassistant/components/sensor/dnsip.py homeassistant/components/sensor/dovado.py + homeassistant/components/sensor/domain_expiry.py homeassistant/components/sensor/dte_energy_bridge.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/dwd_weather_warnings.py diff --git a/homeassistant/components/sensor/domain_expiry.py b/homeassistant/components/sensor/domain_expiry.py new file mode 100644 index 00000000000..9364ce041f2 --- /dev/null +++ b/homeassistant/components/sensor/domain_expiry.py @@ -0,0 +1,76 @@ +""" +Counter for the days till domain will expire. + +For more details about this sensor please refer to the documentation at +https://home-assistant.io/components/sensor.domain_expiry/ +""" +import logging +from datetime import datetime, timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_NAME, CONF_DOMAIN) +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['python-whois==0.6.9'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Domain Expiry' + +SCAN_INTERVAL = timedelta(hours=24) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DOMAIN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up domain expiry sensor.""" + server_name = config.get(CONF_DOMAIN) + sensor_name = config.get(CONF_NAME) + + add_devices([DomainExpiry(sensor_name, server_name)], True) + + +class DomainExpiry(Entity): + """Implementation of the domain expiry sensor.""" + + def __init__(self, sensor_name, server_name): + """Initialize the sensor.""" + self.server_name = server_name + self._name = sensor_name + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'days' + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return 'mdi:earth' + + def update(self): + """Fetch the domain information.""" + import whois + domain = whois.whois(self.server_name) + if isinstance(domain.expiration_date, datetime): + expiry = domain.expiration_date - datetime.today() + self._state = expiry.days + else: + _LOGGER.error("Cannot get expiry date for %s", self.server_name) diff --git a/requirements_all.txt b/requirements_all.txt index 29783c7ab16..f4810116a40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1035,6 +1035,9 @@ python-velbus==2.0.11 # homeassistant.components.media_player.vlc python-vlc==1.1.2 +# homeassistant.components.sensor.domain_expiry +python-whois==0.6.9 + # homeassistant.components.wink python-wink==1.7.3 From a94864c86f2230ce0aa0c270f6aa7760f5033800 Mon Sep 17 00:00:00 2001 From: c727 Date: Wed, 25 Apr 2018 12:37:57 +0200 Subject: [PATCH 485/924] Modify weather components for "new" frontend card (#14076) * Enable weather condition for all forecasts (OWM) * Remove entity_picture from BR * Remove summary texts from Dark Sky * Update test_darksky.py --- .../components/weather/buienradar.py | 9 -------- homeassistant/components/weather/darksky.py | 22 ------------------- .../components/weather/openweathermap.py | 8 +++---- tests/components/weather/test_darksky.py | 3 --- 4 files changed, 3 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index a49a1664eec..bf1864a9c0f 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -121,15 +121,6 @@ class BrWeather(WeatherEntity): if conditions: return conditions.get(ccode) - @property - def entity_picture(self): - """Return the entity picture to use in the frontend, if any.""" - from buienradar.buienradar import (IMAGE) - - if self._data and self._data.condition: - return self._data.condition.get(IMAGE, None) - return None - @property def temperature(self): """Return the current temperature.""" diff --git a/homeassistant/components/weather/darksky.py b/homeassistant/components/weather/darksky.py index 52aa8c46046..f0712542ea5 100644 --- a/homeassistant/components/weather/darksky.py +++ b/homeassistant/components/weather/darksky.py @@ -25,9 +25,6 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by Dark Sky" -ATTR_DAILY_FORECAST_SUMMARY = 'daily_forecast_summary' -ATTR_HOURLY_FORECAST_SUMMARY = 'hourly_forecast_summary' - CONF_UNITS = 'units' DEFAULT_NAME = 'Dark Sky' @@ -122,25 +119,6 @@ class DarkSkyWeather(WeatherEntity): ATTR_FORECAST_TEMP: entry.d.get('temperature')} for entry in self._ds_hourly.data] - @property - def hourly_forecast_summary(self): - """Return a summary of the hourly forecast.""" - return self._ds_hourly.summary - - @property - def daily_forecast_summary(self): - """Return a summary of the daily forecast.""" - return self._ds_daily.summary - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = { - ATTR_DAILY_FORECAST_SUMMARY: self.daily_forecast_summary, - ATTR_HOURLY_FORECAST_SUMMARY: self.hourly_forecast_summary - } - return attrs - def update(self): """Get the latest data from Dark Sky.""" self._dark_sky.update() diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index c8a1bdf8f68..a8e26d39cb3 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -28,7 +28,6 @@ DEFAULT_NAME = 'OpenWeatherMap' MIN_TIME_BETWEEN_FORECAST_UPDATES = timedelta(minutes=30) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS = 3 CONDITION_CLASSES = { 'cloudy': [804], @@ -144,12 +143,11 @@ class OpenWeatherMapWeather(WeatherEntity): data.append({ ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, ATTR_FORECAST_TEMP: - entry.get_temperature('celsius').get('temp') - }) - if (len(data) - 1) % MIN_OFFSET_BETWEEN_FORECAST_CONDITIONS == 0: - data[len(data) - 1][ATTR_FORECAST_CONDITION] = \ + entry.get_temperature('celsius').get('temp'), + ATTR_FORECAST_CONDITION: [k for k, v in CONDITION_CLASSES.items() if entry.get_weather_code() in v][0] + }) return data def update(self): diff --git a/tests/components/weather/test_darksky.py b/tests/components/weather/test_darksky.py index 787aca2ca17..7faa033e0a8 100644 --- a/tests/components/weather/test_darksky.py +++ b/tests/components/weather/test_darksky.py @@ -49,6 +49,3 @@ class TestDarkSky(unittest.TestCase): state = self.hass.states.get('weather.test') self.assertEqual(state.state, 'Clear') - self.assertEqual(state.attributes['daily_forecast_summary'], - 'No precipitation throughout the week, with ' - 'temperatures falling to 66°F on Thursday.') From 241a0793bb804dc00bb9a92ceec02a783dde7059 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 25 Apr 2018 20:31:42 +0200 Subject: [PATCH 486/924] Add Sonos device attribute with grouping information (#13553) --- homeassistant/components/media_player/sonos.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b10c761d532..cc10355abe8 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -67,7 +67,7 @@ ATTR_WITH_GROUP = 'with_group' ATTR_NIGHT_SOUND = 'night_sound' ATTR_SPEECH_ENHANCE = 'speech_enhance' -ATTR_IS_COORDINATOR = 'is_coordinator' +ATTR_SONOS_GROUP = 'sonos_group' UPNP_ERRORS_TO_IGNORE = ['701', '711'] @@ -340,6 +340,7 @@ class SonosDevice(MediaPlayerDevice): self._play_mode = None self._name = None self._coordinator = None + self._sonos_group = None self._status = None self._media_duration = None self._media_position = None @@ -688,7 +689,14 @@ class SonosDevice(MediaPlayerDevice): if p.uid != coordinator_uid] if self.unique_id == coordinator_uid: + sonos_group = [] + for uid in (coordinator_uid, *slave_uids): + entity = _get_entity_from_soco_uid(self.hass, uid) + if entity: + sonos_group.append(entity.entity_id) + self._coordinator = None + self._sonos_group = sonos_group self.schedule_update_ha_state() for slave_uid in slave_uids: @@ -696,6 +704,7 @@ class SonosDevice(MediaPlayerDevice): if slave: # pylint: disable=protected-access slave._coordinator = self + slave._sonos_group = sonos_group slave.schedule_update_ha_state() @property @@ -1038,7 +1047,7 @@ class SonosDevice(MediaPlayerDevice): @property def device_state_attributes(self): """Return device specific state attributes.""" - attributes = {ATTR_IS_COORDINATOR: self.is_coordinator} + attributes = {ATTR_SONOS_GROUP: self._sonos_group} if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound From 8c2dedab52bba516e92347dc1e0809538f626d52 Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Wed, 25 Apr 2018 21:57:44 +0200 Subject: [PATCH 487/924] Re-implement HomematicIP cloud to async (#13468) * Recode to async version of homematicip-rest-api * Remove blank line * Cleanup of access point status class * Fix to loong line * Fix import errors * Bugfix missing wait the _retry_task for sleep command * Update comment * Updates after review * Small updates of logging and property name * Fix DOMAIN and revert back to lowercase snakecase strings * Fix intention and tripple double quotes * Fix travis build * Remove unnecessary state attributes * Fix optional name in configuration * Further reduction of state attributes --- homeassistant/components/homematicip_cloud.py | 222 ++++++++++-------- .../components/sensor/homematicip_cloud.py | 141 +++-------- requirements_all.txt | 2 +- 3 files changed, 159 insertions(+), 206 deletions(-) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 180d6943d8a..0ed9fe22e27 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -5,143 +5,181 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/homematicip_cloud/ """ +import asyncio import logging -from socket import timeout - import voluptuous as vol -from homeassistant.core import callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import (dispatcher_send, - async_dispatcher_connect) -from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['homematicip==0.8'] +REQUIREMENTS = ['homematicip==0.9.2.4'] _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' +COMPONENTS = [ + 'sensor' +] + CONF_NAME = 'name' CONF_ACCESSPOINT = 'accesspoint' CONF_AUTHTOKEN = 'authtoken' CONFIG_SCHEMA = vol.Schema({ - vol.Optional(DOMAIN): [vol.Schema({ - vol.Optional(CONF_NAME, default=''): cv.string, + vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ + vol.Optional(CONF_NAME): vol.Any(cv.string), vol.Required(CONF_ACCESSPOINT): cv.string, vol.Required(CONF_AUTHTOKEN): cv.string, - })], + })]), }, extra=vol.ALLOW_EXTRA) -EVENT_HOME_CHANGED = 'homematicip_home_changed' -EVENT_DEVICE_CHANGED = 'homematicip_device_changed' -EVENT_GROUP_CHANGED = 'homematicip_group_changed' -EVENT_SECURITY_CHANGED = 'homematicip_security_changed' -EVENT_JOURNAL_CHANGED = 'homematicip_journal_changed' +HMIP_ACCESS_POINT = 'Access Point' +HMIP_HUB = 'HmIP-HUB' ATTR_HOME_ID = 'home_id' -ATTR_HOME_LABEL = 'home_label' +ATTR_HOME_NAME = 'home_name' ATTR_DEVICE_ID = 'device_id' ATTR_DEVICE_LABEL = 'device_label' ATTR_STATUS_UPDATE = 'status_update' ATTR_FIRMWARE_STATE = 'firmware_state' +ATTR_UNREACHABLE = 'unreachable' ATTR_LOW_BATTERY = 'low_battery' +ATTR_MODEL_TYPE = 'model_type' +ATTR_GROUP_TYPE = 'group_type' +ATTR_DEVICE_RSSI = 'device_rssi' +ATTR_DUTY_CYCLE = 'duty_cycle' +ATTR_CONNECTED = 'connected' ATTR_SABOTAGE = 'sabotage' -ATTR_RSSI = 'rssi' -ATTR_TYPE = 'type' +ATTR_OPERATION_LOCK = 'operation_lock' -def setup(hass, config): +async def async_setup(hass, config): """Set up the HomematicIP component.""" - # pylint: disable=import-error, no-name-in-module - from homematicip.home import Home + from homematicip.base.base_connection import HmipConnectionError hass.data.setdefault(DOMAIN, {}) - homes = hass.data[DOMAIN] accesspoints = config.get(DOMAIN, []) - - def _update_event(events): - """Handle incoming HomeMaticIP events.""" - for event in events: - etype = event['eventType'] - edata = event['data'] - if etype == 'DEVICE_CHANGED': - dispatcher_send(hass, EVENT_DEVICE_CHANGED, edata.id) - elif etype == 'GROUP_CHANGED': - dispatcher_send(hass, EVENT_GROUP_CHANGED, edata.id) - elif etype == 'HOME_CHANGED': - dispatcher_send(hass, EVENT_HOME_CHANGED, edata.id) - elif etype == 'JOURNAL_CHANGED': - dispatcher_send(hass, EVENT_SECURITY_CHANGED, edata.id) - return True - - for device in accesspoints: - name = device.get(CONF_NAME) - accesspoint = device.get(CONF_ACCESSPOINT) - authtoken = device.get(CONF_AUTHTOKEN) - - home = Home() - if name.lower() == 'none': - name = '' - home.label = name + for conf in accesspoints: + _websession = async_get_clientsession(hass) + _hmip = HomematicipConnector(hass, conf, _websession) try: - home.set_auth_token(authtoken) - home.init(accesspoint) - if home.get_current_state(): - _LOGGER.info("Connection to HMIP established") - else: - _LOGGER.warning("Connection to HMIP could not be established") - return False - except timeout: - _LOGGER.warning("Connection to HMIP could not be established") + await _hmip.init() + except HmipConnectionError: + _LOGGER.error('Failed to connect to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) return False - homes[home.id] = home - home.onEvent += _update_event - home.enable_events() - _LOGGER.info('HUB name: %s, id: %s', home.label, home.id) - for component in ['sensor']: - load_platform(hass, component, DOMAIN, {'homeid': home.id}, config) + home = _hmip.home + home.name = conf.get(CONF_NAME) + home.label = HMIP_ACCESS_POINT + home.modelType = HMIP_HUB + hass.data[DOMAIN][home.id] = home + _LOGGER.info('Connected to the HomematicIP server, %s.', + conf.get(CONF_ACCESSPOINT)) + homeid = {ATTR_HOME_ID: home.id} + for component in COMPONENTS: + hass.async_add_job(async_load_platform(hass, component, DOMAIN, + homeid, config)) + + hass.loop.create_task(_hmip.connect()) return True +class HomematicipConnector: + """Manages HomematicIP http and websocket connection.""" + + def __init__(self, hass, config, websession): + """Initialize HomematicIP cloud connection.""" + from homematicip.async.home import AsyncHome + self._hass = hass + self._ws_close_requested = False + self._retry_task = None + self._tries = 0 + self._accesspoint = config.get(CONF_ACCESSPOINT) + _authtoken = config.get(CONF_AUTHTOKEN) + + self.home = AsyncHome(hass.loop, websession) + self.home.set_auth_token(_authtoken) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) + + async def init(self): + """Initialize connection.""" + await self.home.init(self._accesspoint) + await self.home.get_current_state() + + async def _handle_connection(self): + """Handle websocket connection.""" + from homematicip.base.base_connection import HmipConnectionError + + await self.home.get_current_state() + hmip_events = await self.home.enable_events() + try: + await hmip_events + except HmipConnectionError: + return + + async def connect(self): + """Start websocket connection.""" + self._tries = 0 + while True: + await self._handle_connection() + if self._ws_close_requested: + break + self._ws_close_requested = False + self._tries += 1 + try: + self._retry_task = self._hass.async_add_job(asyncio.sleep( + 2 ** min(9, self._tries), loop=self._hass.loop)) + await self._retry_task + except asyncio.CancelledError: + break + _LOGGER.info('Reconnect (%s) to the HomematicIP cloud server.', + self._tries) + + async def close(self): + """Close the websocket connection.""" + self._ws_close_requested = True + if self._retry_task is not None: + self._retry_task.cancel() + await self.home.disable_events() + _LOGGER.info("Closed connection to HomematicIP cloud server.") + + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home, device): + def __init__(self, home, device, post=None): """Initialize the generic device.""" self._home = home self._device = device + self.post = post + _LOGGER.info('Setting up %s (%s)', self.name, + self._device.modelType) async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_DEVICE_CHANGED, self._device_changed) + self._device.on_update(self._device_changed) - @callback - def _device_changed(self, deviceid): + def _device_changed(self, json, **kwargs): """Handle device state changes.""" - if deviceid is None or deviceid == self._device.id: - _LOGGER.debug('Event device %s', self._device.label) - self.async_schedule_update_ha_state() - - def _name(self, addon=''): - """Return the name of the device.""" - name = '' - if self._home.label != '': - name += self._home.label + ' ' - name += self._device.label - if addon != '': - name += ' ' + addon - return name + _LOGGER.debug('Event %s (%s)', self.name, self._device.modelType) + self.async_schedule_update_ha_state() @property def name(self): """Return the name of the generic device.""" - return self._name() + name = self._device.label + if self._home.name is not None: + name = "{} {}".format(self._home.name, name) + if self.post is not None: + name = "{} {}".format(name, self.post) + return name @property def should_poll(self): @@ -153,24 +191,10 @@ class HomematicipGenericDevice(Entity): """Device available.""" return not self._device.unreach - def _generic_state_attributes(self): - """Return the state attributes of the generic device.""" - laststatus = '' - if self._device.lastStatusUpdate is not None: - laststatus = self._device.lastStatusUpdate.isoformat() - return { - ATTR_HOME_LABEL: self._home.label, - ATTR_DEVICE_LABEL: self._device.label, - ATTR_HOME_ID: self._device.homeId, - ATTR_DEVICE_ID: self._device.id.lower(), - ATTR_STATUS_UPDATE: laststatus, - ATTR_FIRMWARE_STATE: self._device.updateState.lower(), - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - ATTR_TYPE: self._device.modelType - } - @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - return self._generic_state_attributes() + return { + ATTR_LOW_BATTERY: self._device.lowBat, + ATTR_MODEL_TYPE: self._device.modelType + } diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index 1a37aa1ad4e..aa350f7be5d 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -7,13 +7,10 @@ https://home-assistant.io/components/sensor.homematicip_cloud/ import logging -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.components.homematicip_cloud import ( - HomematicipGenericDevice, DOMAIN, EVENT_HOME_CHANGED, - ATTR_HOME_LABEL, ATTR_HOME_ID, ATTR_LOW_BATTERY, ATTR_RSSI) -from homeassistant.const import TEMP_CELSIUS, STATE_OK + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) +from homeassistant.const import TEMP_CELSIUS _LOGGER = logging.getLogger(__name__) @@ -21,68 +18,49 @@ DEPENDENCIES = ['homematicip_cloud'] ATTR_VALVE_STATE = 'valve_state' ATTR_VALVE_POSITION = 'valve_position' +ATTR_TEMPERATURE = 'temperature' ATTR_TEMPERATURE_OFFSET = 'temperature_offset' +ATTR_HUMIDITY = 'humidity' HMIP_UPTODATE = 'up_to_date' HMIP_VALVE_DONE = 'adaption_done' HMIP_SABOTAGE = 'sabotage' +STATE_OK = 'ok' STATE_LOW_BATTERY = 'low_battery' STATE_SABOTAGE = 'sabotage' -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the HomematicIP sensors devices.""" - # pylint: disable=import-error, no-name-in-module from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, TemperatureHumiditySensorDisplay) - homeid = discovery_info['homeid'] - home = hass.data[DOMAIN][homeid] - devices = [HomematicipAccesspoint(home)] + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [HomematicipAccesspointStatus(home)] for device in home.devices: - devices.append(HomematicipDeviceStatus(home, device)) if isinstance(device, HeatingThermostat): devices.append(HomematicipHeatingThermostat(home, device)) - if isinstance(device, TemperatureHumiditySensorWithoutDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) - if isinstance(device, TemperatureHumiditySensorDisplay): - devices.append(HomematicipSensorThermometer(home, device)) - devices.append(HomematicipSensorHumidity(home, device)) + if isinstance(device, (TemperatureHumiditySensorDisplay, + TemperatureHumiditySensorWithoutDisplay)): + devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHumiditySensor(home, device)) - if home.devices: - add_devices(devices) + if devices: + async_add_devices(devices) -class HomematicipAccesspoint(Entity): +class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP access point.""" def __init__(self, home): - """Initialize the access point sensor.""" - self._home = home - _LOGGER.debug('Setting up access point %s', home.label) - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect( - self.hass, EVENT_HOME_CHANGED, self._home_changed) - - @callback - def _home_changed(self, deviceid): - """Handle device state changes.""" - if deviceid is None or deviceid == self._home.id: - _LOGGER.debug('Event home %s', self._home.label) - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the name of the access point device.""" - if self._home.label == '': - return 'Access Point Status' - return '{} Access Point Status'.format(self._home.label) + """Initialize access point device.""" + super().__init__(home, home) @property def icon(self): @@ -102,24 +80,15 @@ class HomematicipAccesspoint(Entity): @property def device_state_attributes(self): """Return the state attributes of the access point.""" - return { - ATTR_HOME_LABEL: self._home.label, - ATTR_HOME_ID: self._home.id, - } + return {} class HomematicipDeviceStatus(HomematicipGenericDevice): """Representation of an HomematicIP device status.""" def __init__(self, home, device): - """Initialize the device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up sensor device status: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Status') + """Initialize generic status device.""" + super().__init__(home, device, 'Status') @property def icon(self): @@ -150,9 +119,8 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """MomematicIP heating thermostat representation.""" def __init__(self, home, device): - """"Initialize heating thermostat.""" - super().__init__(home, device) - _LOGGER.debug('Setting up heating thermostat device: %s', device.label) + """Initialize heating thermostat device.""" + super().__init__(home, device, 'Heating') @property def icon(self): @@ -173,34 +141,18 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_VALVE_STATE: self._device.valveState.lower(), - ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue - } - -class HomematicipSensorHumidity(HomematicipGenericDevice): - """MomematicIP thermometer device.""" +class HomematicipHumiditySensor(HomematicipGenericDevice): + """MomematicIP humidity device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up humidity device: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Humidity') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Humidity') @property def icon(self): """Return the icon.""" - return 'mdi:water' + return 'mdi:water-percent' @property def state(self): @@ -212,27 +164,13 @@ class HomematicipSensorHumidity(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return '%' - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - } - -class HomematicipSensorThermometer(HomematicipGenericDevice): - """MomematicIP thermometer device.""" +class HomematicipTemperatureSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" def __init__(self, home, device): - """"Initialize the thermometer device.""" - super().__init__(home, device) - _LOGGER.debug('Setting up thermometer device: %s', device.label) - - @property - def name(self): - """Return the name of the device.""" - return self._name('Temperature') + """Initialize the thermometer device.""" + super().__init__(home, device, 'Temperature') @property def icon(self): @@ -248,12 +186,3 @@ class HomematicipSensorThermometer(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return TEMP_CELSIUS - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_TEMPERATURE_OFFSET: self._device.temperatureOffset, - ATTR_LOW_BATTERY: self._device.lowBat, - ATTR_RSSI: self._device.rssiDeviceValue, - } diff --git a/requirements_all.txt b/requirements_all.txt index f4810116a40..12e178502c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ home-assistant-frontend==20180425.0 # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.8 +homematicip==0.9.2.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a From d7f77354906944adc07cb5373a3a784d861b8486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 26 Apr 2018 09:49:35 +0200 Subject: [PATCH 488/924] Fix timezone issue when calculating min/max values in tibber #14009 (#14080) * fix timezone issue in tibber #14009 * remove debug print --- homeassistant/components/sensor/tibber.py | 30 +++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 4fb378ac227..42568a6b9ad 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -60,7 +60,7 @@ class TibberSensor(Entity): """Initialize the sensor.""" self._tibber_home = tibber_home self._last_updated = None - self._newest_data_timestamp = None + self._last_data_timestamp = None self._state = None self._is_available = False self._device_state_attributes = {} @@ -70,13 +70,13 @@ class TibberSensor(Entity): async def async_update(self): """Get the latest data and updates the states.""" - now = dt_util.utcnow() + now = dt_util.now() if self._tibber_home.current_price_total and self._last_updated and \ - self._last_updated.hour == now.hour and self._newest_data_timestamp: + self._last_updated.hour == now.hour and self._last_data_timestamp: return - if (not self._newest_data_timestamp or - (self._newest_data_timestamp - now).total_seconds()/3600 < 12 + if (not self._last_data_timestamp or + (self._last_data_timestamp - now).total_seconds()/3600 < 12 or not self._is_available): _LOGGER.debug("Asking for new data.") await self._fetch_data() @@ -135,24 +135,22 @@ class TibberSensor(Entity): def _update_current_price(self): state = None - max_price = None - min_price = None - now = dt_util.utcnow() + max_price = 0 + min_price = 10000 + now = dt_util.now() for key, price_total in self._tibber_home.price_total.items(): - price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_time = dt_util.as_local(dt_util.parse_datetime(key)) price_total = round(price_total, 3) time_diff = (now - price_time).total_seconds()/60 - if (not self._newest_data_timestamp or - price_time > self._newest_data_timestamp): - self._newest_data_timestamp = price_time + if (not self._last_data_timestamp or + price_time > self._last_data_timestamp): + self._last_data_timestamp = price_time if 0 <= time_diff < 60: state = price_total self._last_updated = price_time if now.date() == price_time.date(): - if max_price is None or price_total > max_price: - max_price = price_total - if min_price is None or price_total < min_price: - min_price = price_total + max_price = max(max_price, price_total) + min_price = min(min_price, price_total) self._state = state self._device_state_attributes['max_price'] = max_price self._device_state_attributes['min_price'] = min_price From 47e143d5a16832d5854a26a430544c580dc8703e Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Thu, 26 Apr 2018 19:30:28 +0200 Subject: [PATCH 489/924] Update pyhomematic to 0.1.42 (#14095) * Updated pyhomematic to 0.1.42 * Updated pyhomematic to 0.1.42 --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 1528943a7f9..0291cc28fed 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.41'] +REQUIREMENTS = ['pyhomematic==0.1.42'] DOMAIN = 'homematic' _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 12e178502c4..8650ec153db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -790,7 +790,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.41 +pyhomematic==0.1.42 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 From ff7b51259e0b3adf80f8be71be594f2c3d0028ac Mon Sep 17 00:00:00 2001 From: GotoCode Date: Thu, 26 Apr 2018 20:35:29 +0300 Subject: [PATCH 490/924] Updated list of AWS regions for Amazon Polly (#14097) Fixes #14052 --- homeassistant/components/tts/amazon_polly.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index d7cf0f1f2d1..46c1a24caa0 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -20,7 +20,11 @@ CONF_PROFILE_NAME = 'profile_name' ATTR_CREDENTIALS = 'credentials' DEFAULT_REGION = 'us-east-1' -SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-2', 'eu-west-1'] +SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', + 'ca-central-1', 'eu-west-1', 'eu-central-1', 'eu-west-2', + 'eu-west-3', 'ap-southeast-1', 'ap-southeast-2', + 'ap-northeast-2', 'ap-northeast-1', 'ap-south-1', + 'sa-east-1'] CONF_VOICE = 'voice' CONF_OUTPUT_FORMAT = 'output_format' From 3e18078700e97d7380828a31cfd40ff1d201c432 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 26 Apr 2018 20:01:58 +0100 Subject: [PATCH 491/924] Adds update file_path service to local_file camera (#13976) * WIP: Add update_file service to local_file camera * Add event on update * Update local_file.py * Update services.yaml * Fix indent * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update local_file.py * Update test_local_file.py * Update local_file.py * Adds file_path to device_state_attributes * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update test_local_file.py * Update local_file.py * Update test_local_file.py * fixed test_update_file_path * Update local_file.py * Update test_local_file.py * Update test_local_file.py * Update services.yaml * Update local_file.py * Update local_file.py * Update test_local_file.py * Update local_file.py --- homeassistant/components/camera/local_file.py | 47 ++++++++++++++++--- homeassistant/components/camera/services.yaml | 11 ++++- tests/components/camera/test_local_file.py | 37 +++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/camera/local_file.py b/homeassistant/components/camera/local_file.py index 95d24c7d42e..95eade48568 100644 --- a/homeassistant/components/camera/local_file.py +++ b/homeassistant/components/camera/local_file.py @@ -11,31 +11,44 @@ import os import voluptuous as vol from homeassistant.const import CONF_NAME -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.components.camera import ( + Camera, CAMERA_SERVICE_SCHEMA, DOMAIN, PLATFORM_SCHEMA) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FILE_PATH = 'file_path' - DEFAULT_NAME = 'Local File' +SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FILE_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string }) +CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(CONF_FILE_PATH): cv.string +}) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Camera that works with local files.""" file_path = config[CONF_FILE_PATH] + camera = LocalFile(config[CONF_NAME], file_path) - # check filepath given is readable - if not os.access(file_path, os.R_OK): - _LOGGER.warning("Could not read camera %s image from file: %s", - config[CONF_NAME], file_path) + def update_file_path_service(call): + """Update the file path.""" + file_path = call.data.get(CONF_FILE_PATH) + camera.update_file_path(file_path) + return True - add_devices([LocalFile(config[CONF_NAME], file_path)]) + hass.services.register( + DOMAIN, + SERVICE_UPDATE_FILE_PATH, + update_file_path_service, + schema=CAMERA_SERVICE_UPDATE_FILE_PATH) + + add_devices([camera]) class LocalFile(Camera): @@ -46,6 +59,7 @@ class LocalFile(Camera): super().__init__() self._name = name + self.check_file_path_access(file_path) self._file_path = file_path # Set content type of local file content, _ = mimetypes.guess_type(file_path) @@ -61,7 +75,26 @@ class LocalFile(Camera): _LOGGER.warning("Could not read camera %s image from file: %s", self._name, self._file_path) + def check_file_path_access(self, file_path): + """Check that filepath given is readable.""" + if not os.access(file_path, os.R_OK): + _LOGGER.warning("Could not read camera %s image from file: %s", + self._name, file_path) + + def update_file_path(self, file_path): + """Update the file_path.""" + self.check_file_path_access(file_path) + self._file_path = file_path + self.schedule_update_ha_state() + @property def name(self): """Return the name of this camera.""" return self._name + + @property + def device_state_attributes(self): + """Return the camera state attributes.""" + return { + 'file_path': self._file_path, + } diff --git a/homeassistant/components/camera/services.yaml b/homeassistant/components/camera/services.yaml index b548f3d1ada..544fd0e6b8a 100644 --- a/homeassistant/components/camera/services.yaml +++ b/homeassistant/components/camera/services.yaml @@ -24,6 +24,16 @@ snapshot: description: Template of a Filename. Variable is entity_id. example: '/tmp/snapshot_{{ entity_id }}' +local_file_update_file_path: + description: Update the file_path for a local_file camera. + fields: + entity_id: + description: Name(s) of entities to update. + example: 'camera.local_file' + file_path: + description: Path to the new image file. + example: '/images/newimage.jpg' + onvif_ptz: description: Pan/Tilt/Zoom service for ONVIF camera. fields: @@ -39,4 +49,3 @@ onvif_ptz: zoom: description: "Zoom. Allowed values: ZOOM_IN, ZOOM_OUT" example: "ZOOM_IN" - diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 1098c8c9233..40517ea1298 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -6,6 +6,9 @@ from unittest import mock # https://bugs.python.org/issue23004 from mock_open import MockOpen +from homeassistant.components.camera import DOMAIN +from homeassistant.components.camera.local_file import ( + SERVICE_UPDATE_FILE_PATH) from homeassistant.setup import async_setup_component from tests.common import mock_registry @@ -115,3 +118,37 @@ def test_camera_content_type(hass, aiohttp_client): assert resp_4.content_type == 'image/jpeg' body = yield from resp_4.text() assert body == image + + +async def test_update_file_path(hass): + """Test update_file_path service.""" + # Setup platform + + mock_registry(hass) + + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ + mock.patch('os.access', mock.Mock(return_value=True)): + await async_setup_component(hass, 'camera', { + 'camera': { + 'platform': 'local_file', + 'file_path': 'mock/path.jpg' + } + }) + + # Fetch state and check motion detection attribute + state = hass.states.get('camera.local_file') + assert state.attributes.get('friendly_name') == 'Local File' + assert state.attributes.get('file_path') == 'mock/path.jpg' + + service_data = { + "entity_id": 'camera.local_file', + "file_path": 'new/path.jpg' + } + + await hass.services.async_call(DOMAIN, + SERVICE_UPDATE_FILE_PATH, + service_data) + await hass.async_block_till_done() + + state = hass.states.get('camera.local_file') + assert state.attributes.get('file_path') == 'new/path.jpg' From f5de2b9e5b7d5dc2c75acfa598b580e9d32f1c9b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Apr 2018 16:39:14 -0400 Subject: [PATCH 492/924] Bump frontend to 20180426 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ba487a935a2..4a181c00c02 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180425.0'] +REQUIREMENTS = ['home-assistant-frontend==20180426.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 8650ec153db..91b6c71eaa1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180425.0 +home-assistant-frontend==20180426.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 779b304f490..876aba4574d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180425.0 +home-assistant-frontend==20180426.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 1b71ce32e440075aa527d9b777a44befc86c01d2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Apr 2018 16:39:14 -0400 Subject: [PATCH 493/924] Bump frontend to 20180426 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index ba487a935a2..4a181c00c02 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180425.0'] +REQUIREMENTS = ['home-assistant-frontend==20180426.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 5070f904b43..e48ee1ec292 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180425.0 +home-assistant-frontend==20180426.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 779b304f490..876aba4574d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.4 # homeassistant.components.frontend -home-assistant-frontend==20180425.0 +home-assistant-frontend==20180426.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 833508fbbb4cdf3ce1382095293b09acfa7b3bee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Apr 2018 16:39:42 -0400 Subject: [PATCH 494/924] Version bump to 0.68.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eed6664bd0a..bc32c20f142 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 68 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 4b06392442a177e5c0aa5e8cb4f8cfe3383affae Mon Sep 17 00:00:00 2001 From: Kane610 Date: Thu, 26 Apr 2018 23:59:22 +0200 Subject: [PATCH 495/924] Zone component config entry support (#14059) * Initial commit * Add error handling to config flow Change unique identifyer to name Clean up hound comments * Ensure hass home zone is created with correct entity id Fix failing tests * Fix rest of tests * Move zone tests to zone folder Create config flow tests * Add possibility to unload entry * Use hass.data instead of globas * Don't calculate configures zones every loop iteration * No need to know about home zone during setup of entry * Only use name as title * Don't cache hass home zone * Add new tests for setup and setup entry * Break out functionality from init to zone.py * Make hass home zone be created directly * Make sure that config flow doesn't override hass home zone * A newline was missing in const * Configured zones shall not be imported Removed config flow import functionality Improved tests --- .../components/device_tracker/__init__.py | 3 +- .../components/device_tracker/icloud.py | 2 +- .../components/zone/.translations/en.json | 21 ++++ homeassistant/components/zone/__init__.py | 93 ++++++++++++++++ homeassistant/components/zone/config_flow.py | 56 ++++++++++ homeassistant/components/zone/const.py | 5 + homeassistant/components/zone/strings.json | 21 ++++ homeassistant/components/{ => zone}/zone.py | 70 +----------- homeassistant/config_entries.py | 1 + homeassistant/helpers/condition.py | 4 +- tests/components/zone/__init__.py | 1 + tests/components/zone/test_config_flow.py | 55 ++++++++++ .../{test_zone.py => zone/test_init.py} | 102 +++++++++++++++--- 13 files changed, 351 insertions(+), 83 deletions(-) create mode 100644 homeassistant/components/zone/.translations/en.json create mode 100644 homeassistant/components/zone/__init__.py create mode 100644 homeassistant/components/zone/config_flow.py create mode 100644 homeassistant/components/zone/const.py create mode 100644 homeassistant/components/zone/strings.json rename homeassistant/components/{ => zone}/zone.py (57%) create mode 100644 tests/components/zone/__init__.py create mode 100644 tests/components/zone/test_config_flow.py rename tests/components/{test_zone.py => zone/test_init.py} (55%) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index b24f7784faf..2f068481953 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -15,6 +15,7 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.components import group, zone +from homeassistant.components.zone.zone import async_active_zone from homeassistant.config import load_yaml_config_file, async_log_exception from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -541,7 +542,7 @@ class Device(Entity): elif self.location_name: self._state = self.location_name elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: - zone_state = zone.async_active_zone( + zone_state = async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy) if zone_state is None: self._state = STATE_NOT_HOME diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 781e3674550..5d40f5d533a 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DOMAIN, ATTR_ATTRIBUTES, ENTITY_ID_FORMAT, DeviceScanner) -from homeassistant.components.zone import active_zone +from homeassistant.components.zone.zone import active_zone from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json new file mode 100644 index 00000000000..ff2c7c07c14 --- /dev/null +++ b/homeassistant/components/zone/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Zone", + "step": { + "init": { + "title": "Define zone parameters", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Radius", + "passive": "Passive", + "icon": "Icon" + } + } + }, + "error": { + "name_exists": "Name already exists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py new file mode 100644 index 00000000000..d3628fd57f3 --- /dev/null +++ b/homeassistant/components/zone/__init__.py @@ -0,0 +1,93 @@ +""" +Support for the definition of zones. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/zone/ +""" + +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util import slugify + +from .config_flow import configured_zones +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from .zone import Zone + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Unnamed zone' +DEFAULT_PASSIVE = False +DEFAULT_RADIUS = 100 + +ENTITY_ID_FORMAT = 'zone.{}' +ENTITY_ID_HOME = ENTITY_ID_FORMAT.format(HOME_ZONE) + +ICON_HOME = 'mdi:home' +ICON_IMPORT = 'mdi:import' + +# The config that zone accepts is the same as if it has platforms. +PLATFORM_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Setup configured zones as well as home assistant zone if necessary.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + zone_entries = configured_zones(hass) + for _, entry in config_per_platform(config, DOMAIN): + name = slugify(entry[CONF_NAME]) + if name not in zone_entries: + zone = Zone(hass, entry[CONF_NAME], entry[CONF_LATITUDE], + entry[CONF_LONGITUDE], entry.get(CONF_RADIUS), + entry.get(CONF_ICON), entry.get(CONF_PASSIVE)) + zone.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, entry[CONF_NAME], None, hass) + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][name] = zone + + if HOME_ZONE not in hass.data[DOMAIN] and HOME_ZONE not in zone_entries: + name = hass.config.location_name + zone = Zone(hass, name, hass.config.latitude, hass.config.longitude, + DEFAULT_RADIUS, ICON_HOME, False) + zone.entity_id = ENTITY_ID_HOME + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][slugify(name)] = zone + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up zone as config entry.""" + entry = config_entry.data + name = entry[CONF_NAME] + zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], + entry.get(CONF_RADIUS), entry.get(CONF_ICON), + entry.get(CONF_PASSIVE)) + zone.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, name, None, hass) + hass.async_add_job(zone.async_update_ha_state()) + hass.data[DOMAIN][slugify(name)] = zone + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + zones = hass.data[DOMAIN] + name = slugify(config_entry.data[CONF_NAME]) + zone = zones.pop(name) + await zone.async_remove() + return True diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py new file mode 100644 index 00000000000..5ec955a48d9 --- /dev/null +++ b/homeassistant/components/zone/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow to configure zone component.""" + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.core import callback +from homeassistant.util import slugify + +from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE + + +@callback +def configured_zones(hass): + """Return a set of the configured hosts.""" + return set((slugify(entry.data[CONF_NAME])) for + entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class ZoneFlowHandler(data_entry_flow.FlowHandler): + """Zone config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize zone configuration flow.""" + pass + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + errors = {} + + if user_input is not None: + name = slugify(user_input[CONF_NAME]) + if name not in configured_zones(self.hass) and name != HOME_ZONE: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + errors['base'] = 'name_exists' + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema({ + vol.Required(CONF_NAME): str, + vol.Required(CONF_LATITUDE): cv.latitude, + vol.Required(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_RADIUS): vol.Coerce(float), + vol.Optional(CONF_ICON): str, + vol.Optional(CONF_PASSIVE): bool, + }), + errors=errors, + ) diff --git a/homeassistant/components/zone/const.py b/homeassistant/components/zone/const.py new file mode 100644 index 00000000000..b69ba67302a --- /dev/null +++ b/homeassistant/components/zone/const.py @@ -0,0 +1,5 @@ +"""Constants for the zone component.""" + +CONF_PASSIVE = 'passive' +DOMAIN = 'zone' +HOME_ZONE = 'home' diff --git a/homeassistant/components/zone/strings.json b/homeassistant/components/zone/strings.json new file mode 100644 index 00000000000..ff2c7c07c14 --- /dev/null +++ b/homeassistant/components/zone/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Zone", + "step": { + "init": { + "title": "Define zone parameters", + "data": { + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Radius", + "passive": "Passive", + "icon": "Icon" + } + } + }, + "error": { + "name_exists": "Name already exists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone.py b/homeassistant/components/zone/zone.py similarity index 57% rename from homeassistant/components/zone.py rename to homeassistant/components/zone/zone.py index b1a94f3809c..b7c2e9ee858 100644 --- a/homeassistant/components/zone.py +++ b/homeassistant/components/zone/zone.py @@ -1,54 +1,18 @@ -""" -Support for the definition of zones. +"""Component entity and functionality.""" -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zone/ -""" -import asyncio -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_LATITUDE, - CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) +from homeassistant.const import ATTR_HIDDEN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -from homeassistant.helpers import config_per_platform -from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.location import distance -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN ATTR_PASSIVE = 'passive' ATTR_RADIUS = 'radius' -CONF_PASSIVE = 'passive' - -DEFAULT_NAME = 'Unnamed zone' -DEFAULT_PASSIVE = False -DEFAULT_RADIUS = 100 -DOMAIN = 'zone' - -ENTITY_ID_FORMAT = 'zone.{}' -ENTITY_ID_HOME = ENTITY_ID_FORMAT.format('home') - -ICON_HOME = 'mdi:home' -ICON_IMPORT = 'mdi:import' - STATE = 'zoning' -# The config that zone accepts is the same as if it has platforms. -PLATFORM_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_LATITUDE): cv.latitude, - vol.Required(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), - vol.Optional(CONF_PASSIVE, default=DEFAULT_PASSIVE): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, -}, extra=vol.ALLOW_EXTRA) - @bind_hass def active_zone(hass, latitude, longitude, radius=0): @@ -104,32 +68,6 @@ def in_zone(zone, latitude, longitude, radius=0): return zone_dist - radius < zone.attributes[ATTR_RADIUS] -@asyncio.coroutine -def async_setup(hass, config): - """Set up the zone.""" - entities = set() - tasks = [] - for _, entry in config_per_platform(config, DOMAIN): - name = entry.get(CONF_NAME) - zone = Zone(hass, name, entry[CONF_LATITUDE], entry[CONF_LONGITUDE], - entry.get(CONF_RADIUS), entry.get(CONF_ICON), - entry.get(CONF_PASSIVE)) - zone.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, name, entities) - tasks.append(zone.async_update_ha_state()) - entities.add(zone.entity_id) - - if ENTITY_ID_HOME not in entities: - zone = Zone(hass, hass.config.location_name, - hass.config.latitude, hass.config.longitude, - DEFAULT_RADIUS, ICON_HOME, False) - zone.entity_id = ENTITY_ID_HOME - tasks.append(zone.async_update_ha_state()) - - yield from asyncio.wait(tasks, loop=hass.loop) - return True - - class Zone(Entity): """Representation of a Zone.""" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b159f01c72b..c23d53f2735 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -129,6 +129,7 @@ HANDLERS = Registry() FLOWS = [ 'deconz', 'hue', + 'zone', ] diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index f8f841cc449..cb577e8a9c7 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -393,8 +393,8 @@ def zone(hass, zone_ent, entity): if latitude is None or longitude is None: return False - return zone_cmp.in_zone(zone_ent, latitude, longitude, - entity.attributes.get(ATTR_GPS_ACCURACY, 0)) + return zone_cmp.zone.in_zone(zone_ent, latitude, longitude, + entity.attributes.get(ATTR_GPS_ACCURACY, 0)) def zone_from_config(config, config_validation=True): diff --git a/tests/components/zone/__init__.py b/tests/components/zone/__init__.py new file mode 100644 index 00000000000..2ba325fce81 --- /dev/null +++ b/tests/components/zone/__init__.py @@ -0,0 +1 @@ +"""Tests for the zone component.""" diff --git a/tests/components/zone/test_config_flow.py b/tests/components/zone/test_config_flow.py new file mode 100644 index 00000000000..d8ee6f7c5c0 --- /dev/null +++ b/tests/components/zone/test_config_flow.py @@ -0,0 +1,55 @@ +"""Tests for zone config flow.""" + +from homeassistant.components.zone import config_flow +from homeassistant.components.zone.const import CONF_PASSIVE, DOMAIN, HOME_ZONE +from homeassistant.const import ( + CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS) + +from tests.common import MockConfigEntry + + +async def test_flow_works(hass): + """Test that config flow works.""" + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={ + CONF_NAME: 'Name', + CONF_LATITUDE: '1.1', + CONF_LONGITUDE: '2.2', + CONF_RADIUS: '100', + CONF_ICON: 'mdi:home', + CONF_PASSIVE: True + }) + + assert result['type'] == 'create_entry' + assert result['title'] == 'Name' + assert result['data'] == { + CONF_NAME: 'Name', + CONF_LATITUDE: '1.1', + CONF_LONGITUDE: '2.2', + CONF_RADIUS: '100', + CONF_ICON: 'mdi:home', + CONF_PASSIVE: True + } + + +async def test_flow_requires_unique_name(hass): + """Test that config flow verifies that each zones name is unique.""" + MockConfigEntry(domain=DOMAIN, data={ + CONF_NAME: 'Name' + }).add_to_hass(hass) + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={CONF_NAME: 'Name'}) + assert result['errors'] == {'base': 'name_exists'} + + +async def test_flow_requires_name_different_from_home(hass): + """Test that config flow verifies that each zones name is unique.""" + flow = config_flow.ZoneFlowHandler() + flow.hass = hass + + result = await flow.async_step_init(user_input={CONF_NAME: HOME_ZONE}) + assert result['errors'] == {'base': 'name_exists'} diff --git a/tests/components/test_zone.py b/tests/components/zone/test_init.py similarity index 55% rename from tests/components/test_zone.py rename to tests/components/zone/test_init.py index 0ea84324362..1c698438f2c 100644 --- a/tests/components/test_zone.py +++ b/tests/components/zone/test_init.py @@ -1,10 +1,42 @@ """Test zone component.""" + import unittest +from unittest.mock import Mock from homeassistant import setup from homeassistant.components import zone from tests.common import get_test_home_assistant +from tests.common import MockConfigEntry + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = { + zone.CONF_NAME: 'Test Zone', + zone.CONF_LATITUDE: 1.1, + zone.CONF_LONGITUDE: -2.2, + zone.CONF_RADIUS: 250, + zone.CONF_RADIUS: True + } + hass.data[zone.DOMAIN] = {} + assert await zone.async_setup_entry(hass, entry) is True + assert 'test_zone' in hass.data[zone.DOMAIN] + + +async def test_unload_entry_successful(hass): + """Test unload entry is successful.""" + entry = Mock() + entry.data = { + zone.CONF_NAME: 'Test Zone', + zone.CONF_LATITUDE: 1.1, + zone.CONF_LONGITUDE: -2.2 + } + hass.data[zone.DOMAIN] = {} + assert await zone.async_setup_entry(hass, entry) is True + assert await zone.async_unload_entry(hass, entry) is True + assert not hass.data[zone.DOMAIN] class TestComponentZone(unittest.TestCase): @@ -20,18 +52,17 @@ class TestComponentZone(unittest.TestCase): def test_setup_no_zones_still_adds_home_zone(self): """Test if no config is passed in we still get the home zone.""" - assert setup.setup_component(self.hass, zone.DOMAIN, - {'zone': None}) - + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) assert len(self.hass.states.entity_ids('zone')) == 1 state = self.hass.states.get('zone.home') assert self.hass.config.location_name == state.name assert self.hass.config.latitude == state.attributes['latitude'] assert self.hass.config.longitude == state.attributes['longitude'] assert not state.attributes.get('passive', False) + assert 'test_home' in self.hass.data[zone.DOMAIN] def test_setup(self): - """Test setup.""" + """Test a successful setup.""" info = { 'name': 'Test Zone', 'latitude': 32.880837, @@ -39,16 +70,61 @@ class TestComponentZone(unittest.TestCase): 'radius': 250, 'passive': True } - assert setup.setup_component(self.hass, zone.DOMAIN, { - 'zone': info - }) + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + assert len(self.hass.states.entity_ids('zone')) == 2 state = self.hass.states.get('zone.test_zone') assert info['name'] == state.name assert info['latitude'] == state.attributes['latitude'] assert info['longitude'] == state.attributes['longitude'] assert info['radius'] == state.attributes['radius'] assert info['passive'] == state.attributes['passive'] + assert 'test_zone' in self.hass.data[zone.DOMAIN] + assert 'test_home' in self.hass.data[zone.DOMAIN] + + def test_setup_zone_skips_home_zone(self): + """Test that zone named Home should override hass home zone.""" + info = { + 'name': 'Home', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + + assert len(self.hass.states.entity_ids('zone')) == 1 + state = self.hass.states.get('zone.home') + assert info['name'] == state.name + assert 'home' in self.hass.data[zone.DOMAIN] + assert 'test_home' not in self.hass.data[zone.DOMAIN] + + def test_setup_registered_zone_skips_home_zone(self): + """Test that config entry named home should override hass home zone.""" + entry = MockConfigEntry(domain=zone.DOMAIN, data={ + zone.CONF_NAME: 'home' + }) + entry.add_to_hass(self.hass) + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': None}) + assert len(self.hass.states.entity_ids('zone')) == 0 + assert not self.hass.data[zone.DOMAIN] + + def test_setup_registered_zone_skips_configured_zone(self): + """Test if config entry will override configured zone.""" + entry = MockConfigEntry(domain=zone.DOMAIN, data={ + zone.CONF_NAME: 'Test Zone' + }) + entry.add_to_hass(self.hass) + info = { + 'name': 'Test Zone', + 'latitude': 1.1, + 'longitude': -2.2, + } + assert setup.setup_component(self.hass, zone.DOMAIN, {'zone': info}) + + assert len(self.hass.states.entity_ids('zone')) == 1 + state = self.hass.states.get('zone.test_zone') + assert not state + assert 'test_zone' not in self.hass.data[zone.DOMAIN] + assert 'test_home' in self.hass.data[zone.DOMAIN] def test_active_zone_skips_passive_zones(self): """Test active and passive zones.""" @@ -64,7 +140,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.active_zone(self.hass, 32.880600, -117.237561) + active = zone.zone.active_zone(self.hass, 32.880600, -117.237561) assert active is None def test_active_zone_skips_passive_zones_2(self): @@ -80,7 +156,7 @@ class TestComponentZone(unittest.TestCase): ] }) self.hass.block_till_done() - active = zone.active_zone(self.hass, 32.880700, -117.237561) + active = zone.zone.active_zone(self.hass, 32.880700, -117.237561) assert 'zone.active_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance(self): @@ -104,7 +180,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.active_zone(self.hass, latitude, longitude) + active = zone.zone.active_zone(self.hass, latitude, longitude) assert 'zone.small_zone' == active.entity_id def test_active_zone_prefers_smaller_zone_if_same_distance_2(self): @@ -122,7 +198,7 @@ class TestComponentZone(unittest.TestCase): ] }) - active = zone.active_zone(self.hass, latitude, longitude) + active = zone.zone.active_zone(self.hass, latitude, longitude) assert 'zone.smallest_zone' == active.entity_id def test_in_zone_works_for_passive_zones(self): @@ -141,5 +217,5 @@ class TestComponentZone(unittest.TestCase): ] }) - assert zone.in_zone(self.hass.states.get('zone.passive_zone'), - latitude, longitude) + assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'), + latitude, longitude) From 9d1f9fe20490672eca410dbd6b2d64534805fbd5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 27 Apr 2018 13:15:45 +0200 Subject: [PATCH 496/924] Improve MQTT topic validation (#14099) * Improve MQTT topic validation * Fix test * Improve length check --- homeassistant/components/mqtt/__init__.py | 56 ++++++++++++++++++----- tests/components/mqtt/test_init.py | 52 +++++++++++++++++++-- 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index b81a4fc16a7..55d99a0817e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -90,22 +90,52 @@ ATTR_RETAIN = CONF_RETAIN MAX_RECONNECT_WAIT = 300 # seconds -def valid_subscribe_topic(value: Any, invalid_chars='\0') -> str: - """Validate that we can subscribe using this MQTT topic.""" +def valid_topic(value: Any) -> str: + """Validate that this is a valid topic name/filter.""" value = cv.string(value) - if all(c not in value for c in invalid_chars): - return vol.Length(min=1, max=65535)(value) - raise vol.Invalid('Invalid MQTT topic name') + try: + raw_value = value.encode('utf-8') + except UnicodeError: + raise vol.Invalid("MQTT topic name/filter must be valid UTF-8 string.") + if not raw_value: + raise vol.Invalid("MQTT topic name/filter must not be empty.") + if len(raw_value) > 65535: + raise vol.Invalid("MQTT topic name/filter must not be longer than " + "65535 encoded bytes.") + if '\0' in value: + raise vol.Invalid("MQTT topic name/filter must not contain null " + "character.") + return value + + +def valid_subscribe_topic(value: Any) -> str: + """Validate that we can subscribe using this MQTT topic.""" + value = valid_topic(value) + for i in (i for i, c in enumerate(value) if c == '+'): + if (i > 0 and value[i - 1] != '/') or \ + (i < len(value) - 1 and value[i + 1] != '/'): + raise vol.Invalid("Single-level wildcard must occupy an entire " + "level of the filter") + + index = value.find('#') + if index != -1: + if index != len(value) - 1: + # If there are multiple wildcards, this will also trigger + raise vol.Invalid("Multi-level wildcard must be the last " + "character in the topic filter.") + if len(value) > 1 and value[index - 1] != '/': + raise vol.Invalid("Multi-level wildcard must be after a topic " + "level separator.") + + return value def valid_publish_topic(value: Any) -> str: """Validate that we can publish using this MQTT topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0') - - -def valid_discovery_topic(value: Any) -> str: - """Validate a discovery topic.""" - return valid_subscribe_topic(value, invalid_chars='#+\0/') + value = valid_topic(value) + if '+' in value or '#' in value: + raise vol.Invalid("Wildcards can not be used in topic names") + return value _VALID_QOS_SCHEMA = vol.All(vol.Coerce(int), vol.In([0, 1, 2])) @@ -143,8 +173,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. vol.Optional(CONF_DISCOVERY_PREFIX, - default=DEFAULT_DISCOVERY_PREFIX): valid_discovery_topic, + default=DEFAULT_DISCOVERY_PREFIX): valid_publish_topic, }), }, extra=vol.ALLOW_EXTRA) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b25479bb75a..05c5de71b8c 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -131,10 +131,56 @@ class TestMQTTComponent(unittest.TestCase): self.hass.data['mqtt'].async_publish.call_args[0][2], 2) self.assertFalse(self.hass.data['mqtt'].async_publish.call_args[0][3]) - def test_invalid_mqtt_topics(self): - """Test invalid topics.""" + def test_validate_topic(self): + """Test topic name/filter validation.""" + # Invalid UTF-8, must not contain U+D800 to U+DFFF. + self.assertRaises(vol.Invalid, mqtt.valid_topic, '\ud800') + self.assertRaises(vol.Invalid, mqtt.valid_topic, '\udfff') + # Topic MUST NOT be empty + self.assertRaises(vol.Invalid, mqtt.valid_topic, '') + # Topic MUST NOT be longer than 65535 encoded bytes. + self.assertRaises(vol.Invalid, mqtt.valid_topic, 'ü' * 32768) + # UTF-8 MUST NOT include null character + self.assertRaises(vol.Invalid, mqtt.valid_topic, 'bad\0one') + + # Topics "SHOULD NOT" include these special characters + # (not MUST NOT, RFC2119). The receiver MAY close the connection. + mqtt.valid_topic('\u0001') + mqtt.valid_topic('\u001F') + mqtt.valid_topic('\u009F') + mqtt.valid_topic('\u009F') + mqtt.valid_topic('\uffff') + + def test_validate_subscribe_topic(self): + """Test invalid subscribe topics.""" + mqtt.valid_subscribe_topic('#') + mqtt.valid_subscribe_topic('sport/#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/#/') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'foo/bar#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'foo/#/bar') + + mqtt.valid_subscribe_topic('+') + mqtt.valid_subscribe_topic('+/tennis/#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport+') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport+/') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/+1') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'sport/+#') + self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad+topic') + mqtt.valid_subscribe_topic('sport/+/player1') + mqtt.valid_subscribe_topic('/finance') + mqtt.valid_subscribe_topic('+/+') + mqtt.valid_subscribe_topic('$SYS/#') + + def test_validate_publish_topic(self): + """Test invalid publish topics.""" + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'pub+') + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'pub/+') + self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, '1#') self.assertRaises(vol.Invalid, mqtt.valid_publish_topic, 'bad+topic') - self.assertRaises(vol.Invalid, mqtt.valid_subscribe_topic, 'bad\0one') + mqtt.valid_publish_topic('//') + + # Topic names beginning with $ SHOULD NOT be used, but can + mqtt.valid_publish_topic('$SYS/') # pylint: disable=invalid-name From 0b350993b5a9cb1fd2a8229bc47e7149e18e9dba Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 27 Apr 2018 13:18:58 +0200 Subject: [PATCH 497/924] Improve precision of Hue color state (#14113) --- homeassistant/components/light/hue.py | 20 +++++--------------- tests/components/light/test_hue.py | 17 ++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6eb8de99c99..6b4908b02d4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -242,26 +242,16 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # pylint: disable=redefined-outer-name mode = self._color_mode - - if mode not in ('hs', 'xy'): - return - source = self.light.action if self.is_group else self.light.state - hue = source.get('hue') - sat = source.get('sat') + if mode == 'xy' and 'xy' in source: + return color.color_xy_to_hs(*source['xy']) - # Sometimes the state will not include valid hue/sat values. - # Reported as issue 13434 - if hue is not None and sat is not None: - return hue / 65535 * 360, sat / 255 * 100 + if mode == 'hs' and 'hue' in source and 'sat' in source: + return source['hue'] / 65535 * 360, source['sat'] / 255 * 100 - if 'xy' not in source: - return None - - return color.color_xy_to_hs(*source['xy']) + return None @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 712cd17a7c7..d36548e1e91 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -237,7 +237,7 @@ async def test_lights(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) lamp_2 = hass.states.get('light.hue_lamp_2') assert lamp_2 is not None @@ -253,7 +253,7 @@ async def test_lights_color_mode(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) assert 'color_temp' not in lamp_1.attributes new_light1_on = LIGHT_1_ON.copy() @@ -668,19 +668,6 @@ def test_hs_color(): 'colormode': 'xy', 'hue': 1234, 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'xy', - 'hue': None, - 'sat': 123, 'xy': [0.4, 0.5] }), request_bridge_update=None, From 403a546bdc4566b8672532a70837afb0ab08c2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 25 Apr 2018 04:45:16 +0200 Subject: [PATCH 498/924] Upgrade broadlink lib (#14074) --- homeassistant/components/sensor/broadlink.py | 2 +- homeassistant/components/switch/broadlink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/broadlink.py b/homeassistant/components/sensor/broadlink.py index 5182ba4530e..9376687cf13 100644 --- a/homeassistant/components/sensor/broadlink.py +++ b/homeassistant/components/sensor/broadlink.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['broadlink==0.8.0'] +REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/broadlink.py b/homeassistant/components/switch/broadlink.py index 50c334b1f09..46002112177 100644 --- a/homeassistant/components/switch/broadlink.py +++ b/homeassistant/components/switch/broadlink.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, slugify from homeassistant.util.dt import utcnow -REQUIREMENTS = ['broadlink==0.8.0'] +REQUIREMENTS = ['broadlink==0.9.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index e48ee1ec292..7cc644129b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -180,7 +180,7 @@ botocore==1.7.34 # homeassistant.components.sensor.broadlink # homeassistant.components.switch.broadlink -broadlink==0.8.0 +broadlink==0.9.0 # homeassistant.components.device_tracker.bluetooth_tracker bt_proximity==0.1.2 From 9d0251cfeb8c9131e6a703e7a337bb658f576b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 26 Apr 2018 09:49:35 +0200 Subject: [PATCH 499/924] Fix timezone issue when calculating min/max values in tibber #14009 (#14080) * fix timezone issue in tibber #14009 * remove debug print --- homeassistant/components/sensor/tibber.py | 30 +++++++++++------------ 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/tibber.py b/homeassistant/components/sensor/tibber.py index 4fb378ac227..42568a6b9ad 100644 --- a/homeassistant/components/sensor/tibber.py +++ b/homeassistant/components/sensor/tibber.py @@ -60,7 +60,7 @@ class TibberSensor(Entity): """Initialize the sensor.""" self._tibber_home = tibber_home self._last_updated = None - self._newest_data_timestamp = None + self._last_data_timestamp = None self._state = None self._is_available = False self._device_state_attributes = {} @@ -70,13 +70,13 @@ class TibberSensor(Entity): async def async_update(self): """Get the latest data and updates the states.""" - now = dt_util.utcnow() + now = dt_util.now() if self._tibber_home.current_price_total and self._last_updated and \ - self._last_updated.hour == now.hour and self._newest_data_timestamp: + self._last_updated.hour == now.hour and self._last_data_timestamp: return - if (not self._newest_data_timestamp or - (self._newest_data_timestamp - now).total_seconds()/3600 < 12 + if (not self._last_data_timestamp or + (self._last_data_timestamp - now).total_seconds()/3600 < 12 or not self._is_available): _LOGGER.debug("Asking for new data.") await self._fetch_data() @@ -135,24 +135,22 @@ class TibberSensor(Entity): def _update_current_price(self): state = None - max_price = None - min_price = None - now = dt_util.utcnow() + max_price = 0 + min_price = 10000 + now = dt_util.now() for key, price_total in self._tibber_home.price_total.items(): - price_time = dt_util.as_utc(dt_util.parse_datetime(key)) + price_time = dt_util.as_local(dt_util.parse_datetime(key)) price_total = round(price_total, 3) time_diff = (now - price_time).total_seconds()/60 - if (not self._newest_data_timestamp or - price_time > self._newest_data_timestamp): - self._newest_data_timestamp = price_time + if (not self._last_data_timestamp or + price_time > self._last_data_timestamp): + self._last_data_timestamp = price_time if 0 <= time_diff < 60: state = price_total self._last_updated = price_time if now.date() == price_time.date(): - if max_price is None or price_total > max_price: - max_price = price_total - if min_price is None or price_total < min_price: - min_price = price_total + max_price = max(max_price, price_total) + min_price = min(min_price, price_total) self._state = state self._device_state_attributes['max_price'] = max_price self._device_state_attributes['min_price'] = min_price From c42c668815383168565c85db575b087aa755e8e9 Mon Sep 17 00:00:00 2001 From: GotoCode Date: Thu, 26 Apr 2018 20:35:29 +0300 Subject: [PATCH 500/924] Updated list of AWS regions for Amazon Polly (#14097) Fixes #14052 --- homeassistant/components/tts/amazon_polly.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tts/amazon_polly.py b/homeassistant/components/tts/amazon_polly.py index d7cf0f1f2d1..46c1a24caa0 100644 --- a/homeassistant/components/tts/amazon_polly.py +++ b/homeassistant/components/tts/amazon_polly.py @@ -20,7 +20,11 @@ CONF_PROFILE_NAME = 'profile_name' ATTR_CREDENTIALS = 'credentials' DEFAULT_REGION = 'us-east-1' -SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-2', 'eu-west-1'] +SUPPORTED_REGIONS = ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', + 'ca-central-1', 'eu-west-1', 'eu-central-1', 'eu-west-2', + 'eu-west-3', 'ap-southeast-1', 'ap-southeast-2', + 'ap-northeast-2', 'ap-northeast-1', 'ap-south-1', + 'sa-east-1'] CONF_VOICE = 'voice' CONF_OUTPUT_FORMAT = 'output_format' From 9fb2bf72f9c969defbafcfb92b3207ff807a2a91 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 Apr 2018 15:35:20 -0400 Subject: [PATCH 501/924] Version bump to 0.68.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bc32c20f142..0a69f166b43 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 68 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 7e39a5c4d50cf5754f5f32a84870ca57a5778b02 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Fri, 27 Apr 2018 13:39:07 -0700 Subject: [PATCH 502/924] Change Eufy brightness handling (#14111) Eufy device state isn't reported if the bulb is off, so avoid stamping on the previous values if the bulb isn't going to give us useful information. In addition, improve handling of bulb turn on if we aren't provided with a brightness - this should avoid the bulb tending to end up with a brightness of 1 after power cycling. --- homeassistant/components/light/eufy.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py index a66e219c1a8..6f0a8816eea 100644 --- a/homeassistant/components/light/eufy.py +++ b/homeassistant/components/light/eufy.py @@ -61,13 +61,14 @@ class EufyLight(Light): def update(self): """Synchronise state from the bulb.""" self._bulb.update() - self._brightness = self._bulb.brightness - self._temp = self._bulb.temperature - if self._bulb.colors: - self._colormode = True - self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) - else: - self._colormode = False + if self._bulb.power: + self._brightness = self._bulb.brightness + self._temp = self._bulb.temperature + if self._bulb.colors: + self._colormode = True + self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) + else: + self._colormode = False self._state = self._bulb.power @property @@ -130,7 +131,9 @@ class EufyLight(Light): if brightness is not None: brightness = int(brightness * 100 / 255) else: - brightness = max(1, self._brightness) + if self._brightness is None: + self._brightness = 100 + brightness = self._brightness if colortemp is not None: self._colormode = False From 7da1d757073c0dd49e7b6e17102b725388e5ac85 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Fri, 27 Apr 2018 13:39:07 -0700 Subject: [PATCH 503/924] Change Eufy brightness handling (#14111) Eufy device state isn't reported if the bulb is off, so avoid stamping on the previous values if the bulb isn't going to give us useful information. In addition, improve handling of bulb turn on if we aren't provided with a brightness - this should avoid the bulb tending to end up with a brightness of 1 after power cycling. --- homeassistant/components/light/eufy.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/eufy.py b/homeassistant/components/light/eufy.py index a66e219c1a8..6f0a8816eea 100644 --- a/homeassistant/components/light/eufy.py +++ b/homeassistant/components/light/eufy.py @@ -61,13 +61,14 @@ class EufyLight(Light): def update(self): """Synchronise state from the bulb.""" self._bulb.update() - self._brightness = self._bulb.brightness - self._temp = self._bulb.temperature - if self._bulb.colors: - self._colormode = True - self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) - else: - self._colormode = False + if self._bulb.power: + self._brightness = self._bulb.brightness + self._temp = self._bulb.temperature + if self._bulb.colors: + self._colormode = True + self._hs = color_util.color_RGB_to_hs(*self._bulb.colors) + else: + self._colormode = False self._state = self._bulb.power @property @@ -130,7 +131,9 @@ class EufyLight(Light): if brightness is not None: brightness = int(brightness * 100 / 255) else: - brightness = max(1, self._brightness) + if self._brightness is None: + self._brightness = 100 + brightness = self._brightness if colortemp is not None: self._colormode = False From 58ae8d91f931151d3d2dbc2338d4831c52b1ec84 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 28 Apr 2018 12:35:19 +0200 Subject: [PATCH 504/924] Fix the optional friendly name of the Yeelight (Closes: #14088) (#14110) --- homeassistant/components/light/yeelight.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index d6d860cbd9e..202c6ac594d 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -32,16 +32,17 @@ LEGACY_DEVICE_TYPE_MAP = { 'ceiling1': 'ceiling', } -CONF_TRANSITION = 'transition' +DEFAULT_NAME = 'Yeelight' DEFAULT_TRANSITION = 350 +CONF_TRANSITION = 'transition' CONF_SAVE_ON_CHANGE = 'save_on_change' CONF_MODE_MUSIC = 'use_music_mode' DATA_KEY = 'light.yeelight' DEVICE_SCHEMA = vol.Schema({ - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, vol.Optional(CONF_SAVE_ON_CHANGE, default=True): cv.boolean, @@ -136,20 +137,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Not using hostname, as it seems to vary. name = "yeelight_%s_%s" % (device_type, discovery_info['properties']['mac']) - device = {'name': name, 'ipaddr': discovery_info['host']} + host = discovery_info['host'] + device = {'name': name, 'ipaddr': host} light = YeelightLight(device, DEVICE_SCHEMA({})) lights.append(light) - hass.data[DATA_KEY][name] = light + hass.data[DATA_KEY][host] = light else: - for ipaddr, device_config in config[CONF_DEVICES].items(): - name = device_config[CONF_NAME] - _LOGGER.debug("Adding configured %s", name) - - device = {'name': name, 'ipaddr': ipaddr} + for host, device_config in config[CONF_DEVICES].items(): + device = {'name': device_config[CONF_NAME], 'ipaddr': host} light = YeelightLight(device, device_config) lights.append(light) - hass.data[DATA_KEY][name] = light + hass.data[DATA_KEY][host] = light add_devices(lights, True) From 2749ca4ef4cec399d8d1d1b5158d21995dbb3d77 Mon Sep 17 00:00:00 2001 From: Colin O'Dell Date: Sat, 28 Apr 2018 05:39:45 -0500 Subject: [PATCH 505/924] Update QNAP lib to 0.2.6; handle null temps gracefully (#14117) There's one particular QNAP model which sometimes return empty/null temperatures for certain disks. This commit ensures that this model can be integrated with HASS without causing KeyErrors or other exceptions - if this edge case is hit, the sensor will simply show `0` instead. --- homeassistant/components/sensor/qnap.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index 629a5f6a0ee..b3ca054f88f 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['qnapstats==0.2.5'] +REQUIREMENTS = ['qnapstats==0.2.6'] _LOGGER = logging.getLogger(__name__) @@ -352,7 +352,7 @@ class QNAPDriveSensor(QNAPSensor): return data['health'] if self.var_id == 'drive_temp': - return int(data['temp_c']) + return int(data['temp_c']) if data['temp_c'] is not None else 0 @property def name(self): diff --git a/requirements_all.txt b/requirements_all.txt index 91b6c71eaa1..b277e638bd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1099,7 +1099,7 @@ pyxeoma==1.4.0 pyzabbix==0.7.4 # homeassistant.components.sensor.qnap -qnapstats==0.2.5 +qnapstats==0.2.6 # homeassistant.components.switch.rachio rachiopy==0.1.2 From 00706ad90c901b7ac503d6c0db1d3781ae664379 Mon Sep 17 00:00:00 2001 From: ratcash Date: Sat, 28 Apr 2018 13:35:51 +0200 Subject: [PATCH 506/924] Support Xiaomi Mijia Bluetooth Wireless Temperature and Humidity Sensor (#13955) --- homeassistant/components/sensor/mitemp_bt.py | 172 +++++++++++++++++++ requirements_all.txt | 3 + 2 files changed, 175 insertions(+) create mode 100644 homeassistant/components/sensor/mitemp_bt.py diff --git a/homeassistant/components/sensor/mitemp_bt.py b/homeassistant/components/sensor/mitemp_bt.py new file mode 100644 index 00000000000..3628765293b --- /dev/null +++ b/homeassistant/components/sensor/mitemp_bt.py @@ -0,0 +1,172 @@ +""" +Support for Xiaomi Mi Temp BLE environmental sensor. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.mitemp_bt/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_FORCE_UPDATE, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_MAC +) + + +REQUIREMENTS = ['mitemp_bt==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +CONF_ADAPTER = 'adapter' +CONF_CACHE = 'cache_value' +CONF_MEDIAN = 'median' +CONF_RETRIES = 'retries' +CONF_TIMEOUT = 'timeout' + +DEFAULT_ADAPTER = 'hci0' +DEFAULT_UPDATE_INTERVAL = 300 +DEFAULT_FORCE_UPDATE = False +DEFAULT_MEDIAN = 3 +DEFAULT_NAME = 'MiTemp BT' +DEFAULT_RETRIES = 2 +DEFAULT_TIMEOUT = 10 + + +# Sensor types are defined like: Name, units +SENSOR_TYPES = { + 'temperature': ['Temperature', '°C'], + 'humidity': ['Humidity', '%'], + 'battery': ['Battery', '%'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, + vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, + vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, + vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the MiTempBt sensor.""" + from mitemp_bt import mitemp_bt_poller + try: + import bluepy.btle # noqa: F401 # pylint: disable=unused-variable + from btlewrap import BluepyBackend + backend = BluepyBackend + except ImportError: + from btlewrap import GatttoolBackend + backend = GatttoolBackend + _LOGGER.debug('MiTempBt is using %s backend.', backend.__name__) + + cache = config.get(CONF_CACHE) + poller = mitemp_bt_poller.MiTempBtPoller( + config.get(CONF_MAC), cache_timeout=cache, + adapter=config.get(CONF_ADAPTER), backend=backend) + force_update = config.get(CONF_FORCE_UPDATE) + median = config.get(CONF_MEDIAN) + poller.ble_timeout = config.get(CONF_TIMEOUT) + poller.retries = config.get(CONF_RETRIES) + + devs = [] + + for parameter in config[CONF_MONITORED_CONDITIONS]: + name = SENSOR_TYPES[parameter][0] + unit = SENSOR_TYPES[parameter][1] + + prefix = config.get(CONF_NAME) + if prefix: + name = "{} {}".format(prefix, name) + + devs.append(MiTempBtSensor( + poller, parameter, name, unit, force_update, median)) + + add_devices(devs) + + +class MiTempBtSensor(Entity): + """Implementing the MiTempBt sensor.""" + + def __init__(self, poller, parameter, name, unit, force_update, median): + """Initialize the sensor.""" + self.poller = poller + self.parameter = parameter + self._unit = unit + self._name = name + self._state = None + self.data = [] + self._force_update = force_update + # Median is used to filter out outliers. median of 3 will filter + # single outliers, while median of 5 will filter double outliers + # Use median_count = 1 if no filtering is required. + self.median_count = median + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return self._unit + + @property + def force_update(self): + """Force update.""" + return self._force_update + + def update(self): + """ + Update current conditions. + + This uses a rolling median over 3 values to filter out outliers. + """ + from btlewrap.base import BluetoothBackendException + try: + _LOGGER.debug("Polling data for %s", self.name) + data = self.poller.parameter_value(self.parameter) + except IOError as ioerr: + _LOGGER.warning("Polling error %s", ioerr) + return + except BluetoothBackendException as bterror: + _LOGGER.warning("Polling error %s", bterror) + return + + if data is not None: + _LOGGER.debug("%s = %s", self.name, data) + self.data.append(data) + else: + _LOGGER.warning("Did not receive any data from Mi Temp sensor %s", + self.name) + # Remove old data from median list or set sensor value to None + # if no data is available anymore + if self.data: + self.data = self.data[1:] + else: + self._state = None + return + + if len(self.data) > self.median_count: + self.data = self.data[1:] + + if len(self.data) == self.median_count: + median = sorted(self.data)[int((self.median_count - 1) / 2)] + _LOGGER.debug("Median is: %s", median) + self._state = median + else: + _LOGGER.debug("Not yet enough data for median calculation") diff --git a/requirements_all.txt b/requirements_all.txt index b277e638bd9..fe5901d6577 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -527,6 +527,9 @@ mficlient==0.3.0 # homeassistant.components.sensor.miflora miflora==0.4.0 +# homeassistant.components.sensor.mitemp_bt +mitemp_bt==0.0.1 + # homeassistant.components.sensor.mopar motorparts==1.0.2 From 1d41321f8f7c9fd35796aad4f6ba37b34e20c064 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 14:03:09 +0200 Subject: [PATCH 507/924] Upgrade colorlog to 3.1.4 (#14132) --- homeassistant/scripts/check_config.py | 4 ++-- requirements_all.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 8c78602f3d0..4375d973a0b 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -21,7 +21,7 @@ from homeassistant.config import ( import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError -REQUIREMENTS = ('colorlog==3.1.2',) +REQUIREMENTS = ('colorlog==3.1.4',) if system() == 'Windows': # Ensure colorama installed for colorlog on Windows REQUIREMENTS += ('colorama<=1',) @@ -58,7 +58,7 @@ def color(the_color, *args, reset=None): def run(script_args: List) -> int: """Handle ensure config commandline script.""" parser = argparse.ArgumentParser( - description=("Check Home Assistant configuration.")) + description="Check Home Assistant configuration.") parser.add_argument( '--script', choices=['check_config']) parser.add_argument( diff --git a/requirements_all.txt b/requirements_all.txt index fe5901d6577..d6cb477ab51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -202,7 +202,7 @@ coinbase==2.1.0 coinmarketcap==4.2.1 # homeassistant.scripts.check_config -colorlog==3.1.2 +colorlog==3.1.4 # homeassistant.components.alarm_control_panel.concord232 # homeassistant.components.binary_sensor.concord232 From 8bc497ba1d8b588fcb165b8c7b4773741ed47d32 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 28 Apr 2018 07:46:58 -0600 Subject: [PATCH 508/924] Move RainMachine to component/hub model (#14085) * Moves RainMachine to component/hub model * Updated requirements * Updated coverage * Hound violations * Collaborator-requested changes * Small formatting updates * Removed references to remote API * Collaborator-requested changes * Collaborator-requested changes * Fixed attribution --- .coveragerc | 4 +- homeassistant/components/rainmachine.py | 71 ++++++++ .../components/switch/rainmachine.py | 167 +++++------------- requirements_all.txt | 2 +- 4 files changed, 117 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/rainmachine.py diff --git a/.coveragerc b/.coveragerc index 452dbec7559..c1c879aef09 100644 --- a/.coveragerc +++ b/.coveragerc @@ -208,6 +208,9 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py + homeassistant/components/rainmachine.py + homeassistant/components/*/rainmachine.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py @@ -711,7 +714,6 @@ omit = homeassistant/components/switch/orvibo.py homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rainbird.py - homeassistant/components/switch/rainmachine.py homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py new file mode 100644 index 00000000000..4c8b8a1114f --- /dev/null +++ b/homeassistant/components/rainmachine.py @@ -0,0 +1,71 @@ +""" +This component provides support for RainMachine sprinkler controllers. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainmachine/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol +from requests.exceptions import ConnectTimeout + +from homeassistant.helpers import config_validation as cv +from homeassistant.const import ( + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL) + +REQUIREMENTS = ['regenmaschine==0.4.1'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_PORT = 8080 +DEFAULT_SSL = True + +MIN_SCAN_TIME = timedelta(seconds=1) +MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import HTTPError + + conf = config[DOMAIN] + ip_address = conf[CONF_IP_ADDRESS] + password = conf[CONF_PASSWORD] + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + + try: + auth = Authenticator.create_local( + ip_address, password, port=port, https=ssl) + client = Client(auth) + hass.data[DATA_RAINMACHINE] = client + except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: + _LOGGER.error('An error occurred: %s', str(exc_info)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc_info), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 99d41bdd9c3..cdada7ce274 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,130 +1,61 @@ """Implements a RainMachine sprinkler controller for Home Assistant.""" -from datetime import timedelta from logging import getLogger import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_EMAIL, CONF_IP_ADDRESS, - CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL) +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS from homeassistant.util import Throttle _LOGGER = getLogger(__name__) -REQUIREMENTS = ['regenmaschine==0.4.1'] +DEPENDENCIES = ['rainmachine'] ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' CONF_ZONE_RUN_TIME = 'zone_run_time' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True DEFAULT_ZONE_RUN_SECONDS = 60 * 10 -MIN_SCAN_TIME_LOCAL = timedelta(seconds=1) -MIN_SCAN_TIME_REMOTE = timedelta(seconds=5) -MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) - -PLATFORM_SCHEMA = vol.Schema( - vol.All( - cv.has_at_least_one_key(CONF_IP_ADDRESS, CONF_EMAIL), - { - vol.Required(CONF_PLATFORM): cv.string, - vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, - vol.Exclusive(CONF_IP_ADDRESS, 'auth'): cv.string, - vol.Exclusive(CONF_EMAIL, 'auth'): - vol.Email(), # pylint: disable=no-value-for-parameter - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): - cv.positive_int - }), - extra=vol.ALLOW_EXTRA) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): + cv.positive_int +}) def setup_platform(hass, config, add_devices, discovery_info=None): """Set this component up under its platform.""" - import regenmaschine as rm + client = hass.data.get(DATA_RAINMACHINE) + device_name = client.provision.device_name()['name'] + device_mac = client.provision.wifi()['macAddress'] - _LOGGER.debug('Config data: %s', config) + _LOGGER.debug('Config received: %s', config) - ip_address = config.get(CONF_IP_ADDRESS, None) - email_address = config.get(CONF_EMAIL, None) - password = config[CONF_PASSWORD] zone_run_time = config[CONF_ZONE_RUN_TIME] - try: - if ip_address: - _LOGGER.debug('Configuring local API') + entities = [] + for program in client.programs.all().get('programs', {}): + if not program.get('active'): + continue - port = config[CONF_PORT] - ssl = config[CONF_SSL] - auth = rm.Authenticator.create_local( - ip_address, password, port=port, https=ssl) - elif email_address: - _LOGGER.debug('Configuring remote API') - auth = rm.Authenticator.create_remote(email_address, password) + _LOGGER.debug('Adding program: %s', program) + entities.append( + RainMachineProgram(client, device_name, device_mac, program)) - _LOGGER.debug('Querying against: %s', auth.url) + for zone in client.zones.all().get('zones', {}): + if not zone.get('active'): + continue - client = rm.Client(auth) - device_name = client.provision.device_name()['name'] - device_mac = client.provision.wifi()['macAddress'] + _LOGGER.debug('Adding zone: %s', zone) + entities.append( + RainMachineZone(client, device_name, device_mac, zone, + zone_run_time)) - entities = [] - for program in client.programs.all().get('programs', {}): - if not program.get('active'): - continue - - _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_name, device_mac, program)) - - for zone in client.zones.all().get('zones', {}): - if not zone.get('active'): - continue - - _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_name, device_mac, zone, - zone_run_time)) - - add_devices(entities) - except rm.exceptions.HTTPError as exc_info: - _LOGGER.error('An HTTP error occurred while talking with RainMachine') - _LOGGER.debug(exc_info) - return False - except UnboundLocalError as exc_info: - _LOGGER.error('Could not authenticate against RainMachine') - _LOGGER.debug(exc_info) - return False - - -def aware_throttle(api_type): - """Create an API type-aware throttler.""" - _decorator = None - if api_type == 'local': - - @Throttle(MIN_SCAN_TIME_LOCAL, MIN_SCAN_TIME_FORCED) - def decorator(function): - """Create a local API throttler.""" - return function - - _decorator = decorator - else: - - @Throttle(MIN_SCAN_TIME_REMOTE, MIN_SCAN_TIME_FORCED) - def decorator(function): - """Create a remote API throttler.""" - return function - - _decorator = decorator - - return _decorator + add_devices(entities, True) class RainMachineEntity(SwitchDevice): @@ -135,19 +66,24 @@ class RainMachineEntity(SwitchDevice): self._api_type = 'remote' if client.auth.using_remote_api else 'local' self._client = client self._entity_json = entity_json + self.device_mac = device_mac self.device_name = device_name self._attrs = { - ATTR_ATTRIBUTION: '© RainMachine', + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_DEVICE_CLASS: self.device_name } @property def device_state_attributes(self) -> dict: """Return the state attributes.""" - if self._client: - return self._attrs + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: @@ -159,27 +95,6 @@ class RainMachineEntity(SwitchDevice): """Return the RainMachine ID for this entity.""" return self._entity_json.get('uid') - @aware_throttle('local') - def _local_update(self) -> None: - """Call an update with scan times appropriate for the local API.""" - self._update() - - @aware_throttle('remote') - def _remote_update(self) -> None: - """Call an update with scan times appropriate for the remote API.""" - self._update() - - def _update(self) -> None: # pylint: disable=no-self-use - """Logic for update method, regardless of API type.""" - raise NotImplementedError() - - def update(self) -> None: - """Determine how the entity updates itself.""" - if self._api_type == 'remote': - self._remote_update() - else: - self._local_update() - class RainMachineProgram(RainMachineEntity): """A RainMachine program.""" @@ -192,7 +107,7 @@ class RainMachineProgram(RainMachineEntity): @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -224,7 +139,8 @@ class RainMachineProgram(RainMachineEntity): _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) - def _update(self) -> None: + @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) + def update(self) -> None: """Update info for the program.""" import regenmaschine.exceptions as exceptions @@ -258,7 +174,7 @@ class RainMachineZone(RainMachineEntity): @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -287,7 +203,8 @@ class RainMachineZone(RainMachineEntity): _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) - def _update(self) -> None: + @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) + def update(self) -> None: """Update info for the zone.""" import regenmaschine.exceptions as exceptions diff --git a/requirements_all.txt b/requirements_all.txt index d6cb477ab51..f6fdf81a2e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1116,7 +1116,7 @@ raincloudy==0.0.4 # homeassistant.components.raspihats # raspihats==2.2.3 -# homeassistant.components.switch.rainmachine +# homeassistant.components.rainmachine regenmaschine==0.4.1 # homeassistant.components.python_script From c78e8eb578846356fd629ae989c7d8fbcd754711 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 28 Apr 2018 17:14:34 +0200 Subject: [PATCH 509/924] Add support for light sensors with 'lx' unit to HomeKit (#14131) * add support for light sensors with lx unit * add test for light sensor with 'lx' unit --- homeassistant/components/homekit/__init__.py | 2 +- tests/components/homekit/test_get_accessories.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 24c6dfa8a76..025ef4069e9 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -126,7 +126,7 @@ def get_accessory(hass, state, aid, config): or DEVICE_CLASS_CO2 in state.entity_id: a_type = 'CarbonDioxideSensor' elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \ - unit == 'lux': + unit == 'lux' or unit == 'lx': a_type = 'LightSensor' elif state.domain == 'switch' or state.domain == 'remote' \ diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index c26982e170b..76736ce45ad 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -125,6 +125,13 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) get_accessory(None, state, 2, {}) + def test_light_sensor_unit_lx(self): + """Test light sensor with lx as unit.""" + with patch.dict(TYPES, {'LightSensor': self.mock_type}): + state = State('sensor.light', '900', + {ATTR_UNIT_OF_MEASUREMENT: 'lx'}) + get_accessory(None, state, 2, {}) + def test_binary_sensor(self): """Test binary sensor with opening class.""" with patch.dict(TYPES, {'BinarySensor': self.mock_type}): From ea5c336ab4bb7e8479481525ca4180093ae273e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 19:21:37 +0200 Subject: [PATCH 510/924] Upgrade restrictedpython to 4.0b3 (#14140) --- homeassistant/components/python_script.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index dedc39ef3a2..1d33740d4a4 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b2'] +REQUIREMENTS = ['restrictedpython==4.0b3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f6fdf81a2e4..dc4997d49ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1120,7 +1120,7 @@ raincloudy==0.0.4 regenmaschine==0.4.1 # homeassistant.components.python_script -restrictedpython==4.0b2 +restrictedpython==4.0b3 # homeassistant.components.rflink rflink==0.0.37 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876aba4574d..36e8df39bd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ pyupnp-async==0.1.0.1 pywebpush==1.6.0 # homeassistant.components.python_script -restrictedpython==4.0b2 +restrictedpython==4.0b3 # homeassistant.components.rflink rflink==0.0.37 From 8d87b9fed5437f8e619a997c286f7269c331104b Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 29 Apr 2018 04:39:21 +1000 Subject: [PATCH 511/924] Logitech Pop support for emulated_hue component (#12833) * Update hue_api.py add dummy group handler for logitech-pop * Update __init__.py add HueGroupView for logitech pop * Update __init__.py removed whitespace on blankline * fix line limit and space * fix indents * fix more docstring and formatting issues. * fix more whitespace issues * Fix pylint issue --- .../components/emulated_hue/__init__.py | 3 ++- .../components/emulated_hue/hue_api.py | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index fa558cf299f..fd7f7147fdb 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json from .hue_api import ( HueUsernameView, HueAllLightsStateView, HueOneLightStateView, - HueOneLightChangeView) + HueOneLightChangeView, HueGroupView) from .upnp import DescriptionXmlView, UPNPResponderThread DOMAIN = 'emulated_hue' @@ -104,6 +104,7 @@ def setup(hass, yaml_config): server.register_view(HueAllLightsStateView(config)) server.register_view(HueOneLightStateView(config)) server.register_view(HueOneLightChangeView(config)) + server.register_view(HueGroupView(config)) upnp_listener = UPNPResponderThread( config.host_ip_addr, config.listen_port, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 5d97ef3cea4..2b74984e4ca 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -51,6 +51,29 @@ class HueUsernameView(HomeAssistantView): return self.json([{'success': {'username': '12345678901234567890'}}]) +class HueGroupView(HomeAssistantView): + """Group handler to get Logitech Pop working.""" + + url = '/api/{username}/groups/0/action' + name = 'emulated_hue:groups:state' + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def put(self, request, username): + """Process a request to make the Logitech Pop working.""" + return self.json([{ + 'error': { + 'address': '/groups/0/action/scene', + 'type': 7, + 'description': 'invalid value, dummy for parameter, scene' + } + }]) + + class HueAllLightsStateView(HomeAssistantView): """Handle requests for getting and setting info about entities.""" From b352b761f386fe007ffb33409c2f2514f10180b3 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Sat, 28 Apr 2018 15:05:27 -0400 Subject: [PATCH 512/924] Bump pyvizio to 0.0.3 (#14147) * Bumping pyvizio version * Bump pyvizio version --- homeassistant/components/media_player/vizio.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/vizio.py b/homeassistant/components/media_player/vizio.py index 64d1f642e6e..381482a4839 100644 --- a/homeassistant/components/media_player/vizio.py +++ b/homeassistant/components/media_player/vizio.py @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv import homeassistant.util as util -REQUIREMENTS = ['pyvizio==0.0.2'] +REQUIREMENTS = ['pyvizio==0.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index dc4997d49ec..baf907c0459 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ pyvera==0.2.42 pyvesync==0.1.1 # homeassistant.components.media_player.vizio -pyvizio==0.0.2 +pyvizio==0.0.3 # homeassistant.components.velux pyvlx==0.1.3 From 93fe61bf13652639ece8b44f88feb9fe01088bd3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Apr 2018 17:09:38 -0400 Subject: [PATCH 513/924] System log: make firing event optional (#14102) * Syste log: make firing event optional * Add test * Lint * Doc string --- .../components/system_log/__init__.py | 12 +- tests/components/test_system_log.py | 142 ++++++++++-------- 2 files changed, 88 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 1dad1f3a1eb..5994184d815 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -19,12 +19,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP CONF_MAX_ENTRIES = 'max_entries' +CONF_FIRE_EVENT = 'fire_event' CONF_MESSAGE = 'message' CONF_LEVEL = 'level' CONF_LOGGER = 'logger' DATA_SYSTEM_LOG = 'system_log' DEFAULT_MAX_ENTRIES = 50 +DEFAULT_FIRE_EVENT = False DEPENDENCIES = ['http'] DOMAIN = 'system_log' @@ -37,6 +39,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): cv.positive_int, + vol.Optional(CONF_FIRE_EVENT, default=DEFAULT_FIRE_EVENT): cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -97,11 +100,12 @@ def _exception_as_string(exc_info): class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen): + def __init__(self, hass, maxlen, fire_event): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = deque(maxlen=maxlen) + self.fire_event = fire_event def _create_entry(self, record, call_stack): return { @@ -130,7 +134,8 @@ class LogErrorHandler(logging.Handler): entry = self._create_entry(record, stack) self.records.appendleft(entry) - self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) + if self.fire_event: + self.hass.bus.fire(EVENT_SYSTEM_LOG, entry) @asyncio.coroutine @@ -140,7 +145,8 @@ def async_setup(hass, config): if conf is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - handler = LogErrorHandler(hass, conf.get(CONF_MAX_ENTRIES)) + handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], + conf[CONF_FIRE_EVENT]) logging.getLogger().addHandler(handler) hass.http.register_view(AllErrorsView(handler)) diff --git a/tests/components/test_system_log.py b/tests/components/test_system_log.py index c440ef9c30c..59e99e5c1b5 100644 --- a/tests/components/test_system_log.py +++ b/tests/components/test_system_log.py @@ -1,33 +1,26 @@ """Test system log component.""" -import asyncio import logging from unittest.mock import MagicMock, patch -import pytest - from homeassistant.core import callback from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log _LOGGER = logging.getLogger('test_logger') +BASIC_CONFIG = { + 'system_log': { + 'max_entries': 2, + } +} -@pytest.fixture(autouse=True) -@asyncio.coroutine -def setup_test_case(hass, aiohttp_client): - """Setup system_log component before test case.""" - config = {'system_log': {'max_entries': 2}} - yield from async_setup_component(hass, system_log.DOMAIN, config) - - -@asyncio.coroutine -def get_error_log(hass, aiohttp_client, expected_count): +async def get_error_log(hass, aiohttp_client, expected_count): """Fetch all entries from system_log via the API.""" - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.get('/api/error/all') + client = await aiohttp_client(hass.http.app) + resp = await client.get('/api/error/all') assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert len(data) == expected_count return data @@ -52,43 +45,43 @@ def get_frame(name): return (name, None, None, None) -@asyncio.coroutine -def test_normal_logs(hass, aiohttp_client): +async def test_normal_logs(hass, aiohttp_client): """Test that debug and info are not logged.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.debug('debug') _LOGGER.info('info') # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_exception(hass, aiohttp_client): +async def test_exception(hass, aiohttp_client): """Test that exceptions are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _generate_and_log_exception('exception message', 'log message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, 'exception message', 'log message', 'ERROR') -@asyncio.coroutine -def test_warning(hass, aiohttp_client): +async def test_warning(hass, aiohttp_client): """Test that warning are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.warning('warning message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'warning message', 'WARNING') -@asyncio.coroutine -def test_error(hass, aiohttp_client): +async def test_error(hass, aiohttp_client): """Test that errors are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_error_posted_as_event(hass, aiohttp_client): - """Test that error are posted as events.""" +async def test_config_not_fire_event(hass): + """Test that errors are not posted as events with default config.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) events = [] @callback @@ -99,77 +92,100 @@ def test_error_posted_as_event(hass, aiohttp_client): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) _LOGGER.error('error message') - yield from hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_error_posted_as_event(hass): + """Test that error are posted as events.""" + await async_setup_component(hass, system_log.DOMAIN, { + 'system_log': { + 'max_entries': 2, + 'fire_event': True, + } + }) + events = [] + + @callback + def event_listener(event): + """Listen to events of type system_log_event.""" + events.append(event) + + hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) + + _LOGGER.error('error message') + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, '', 'error message', 'ERROR') -@asyncio.coroutine -def test_critical(hass, aiohttp_client): +async def test_critical(hass, aiohttp_client): """Test that critical are logged and retrieved correctly.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.critical('critical message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert_log(log, '', 'critical message', 'CRITICAL') -@asyncio.coroutine -def test_remove_older_logs(hass, aiohttp_client): +async def test_remove_older_logs(hass, aiohttp_client): """Test that older logs are rotated out.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message 1') _LOGGER.error('error message 2') _LOGGER.error('error message 3') - log = yield from get_error_log(hass, aiohttp_client, 2) + log = await get_error_log(hass, aiohttp_client, 2) assert_log(log[0], '', 'error message 3', 'ERROR') assert_log(log[1], '', 'error message 2', 'ERROR') -@asyncio.coroutine -def test_clear_logs(hass, aiohttp_client): +async def test_clear_logs(hass, aiohttp_client): """Test that the log can be cleared via a service call.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.error('error message') hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_CLEAR, {})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # Assert done by get_error_log - yield from get_error_log(hass, aiohttp_client, 0) + await get_error_log(hass, aiohttp_client, 0) -@asyncio.coroutine -def test_write_log(hass): +async def test_write_log(hass): """Test that error propagates to logger.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger) as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'homeassistant.components.system_log.external') assert logger.method_calls[0] == ('error', ('test_message',)) -@asyncio.coroutine -def test_write_choose_logger(hass): +async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('logging.getLogger') as mock_logging: hass.async_add_job( hass.services.async_call( system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'logger': 'myLogger'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() mock_logging.assert_called_once_with( 'myLogger') -@asyncio.coroutine -def test_write_choose_level(hass): +async def test_write_choose_level(hass): """Test that correct logger is chosen.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) logger = MagicMock() with patch('logging.getLogger', return_value=logger): hass.async_add_job( @@ -177,17 +193,17 @@ def test_write_choose_level(hass): system_log.DOMAIN, system_log.SERVICE_WRITE, {'message': 'test_message', 'level': 'debug'})) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert logger.method_calls[0] == ('debug', ('test_message',)) -@asyncio.coroutine -def test_unknown_path(hass, aiohttp_client): +async def test_unknown_path(hass, aiohttp_client): """Test error logged from unknown path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) _LOGGER.findCaller = MagicMock( return_value=('unknown_path', 0, None, None)) _LOGGER.error('error message') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'unknown_path' @@ -206,31 +222,31 @@ def log_error_from_test_path(path): _LOGGER.error('error message') -@asyncio.coroutine -def test_homeassistant_path(hass, aiohttp_client): +async def test_homeassistant_path(hass, aiohttp_client): """Test error logged from homeassistant path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch('homeassistant.components.system_log.HOMEASSISTANT_PATH', new=['venv_path/homeassistant']): log_error_from_test_path( 'venv_path/homeassistant/component/component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'component/component.py' -@asyncio.coroutine -def test_config_path(hass, aiohttp_client): +async def test_config_path(hass, aiohttp_client): """Test error logged from config path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.object(hass.config, 'config_dir', new='config'): log_error_from_test_path('config/custom_component/test.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'custom_component/test.py' -@asyncio.coroutine -def test_netdisco_path(hass, aiohttp_client): +async def test_netdisco_path(hass, aiohttp_client): """Test error logged from netdisco path.""" + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) with patch.dict('sys.modules', netdisco=MagicMock(__path__=['venv_path/netdisco'])): log_error_from_test_path('venv_path/netdisco/disco_component.py') - log = (yield from get_error_log(hass, aiohttp_client, 1))[0] + log = (await get_error_log(hass, aiohttp_client, 1))[0] assert log['source'] == 'disco_component.py' From e6d4501ee360c79716f5b50f2c8e557d33f77b54 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 28 Apr 2018 23:12:11 +0200 Subject: [PATCH 514/924] Fix color setting of tplink lights (#14108) --- homeassistant/components/light/tplink.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 0bbec010282..4101eab2150 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin @@ -90,15 +90,15 @@ class TPLinkSmartBulb(Light): if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - if ATTR_KELVIN in kwargs: - self.smartbulb.color_temp = kwargs[ATTR_KELVIN] - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - self.smartbulb.brightness = brightness_to_percentage(brightness) + + brightness = brightness_to_percentage( + kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)) if ATTR_HS_COLOR in kwargs: hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (hue, sat, 100) + hsv = (int(hue), int(sat), brightness) self.smartbulb.hsv = hsv + elif ATTR_BRIGHTNESS in kwargs: + self.smartbulb.brightness = brightness def turn_off(self, **kwargs): """Turn the light off.""" From 7bdd4dd9603a603afd4964f365c4ea012a10adbf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:15:32 +0200 Subject: [PATCH 515/924] Upgrade pylast to 2.2.0 (#14139) --- homeassistant/components/sensor/lastfm.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/lastfm.py b/homeassistant/components/sensor/lastfm.py index 9d305973ecf..9fec4b4b5e3 100644 --- a/homeassistant/components/sensor/lastfm.py +++ b/homeassistant/components/sensor/lastfm.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pylast==2.1.0'] +REQUIREMENTS = ['pylast==2.2.0'] ATTR_LAST_PLAYED = 'last_played' ATTR_PLAY_COUNT = 'play_count' diff --git a/requirements_all.txt b/requirements_all.txt index baf907c0459..6eb9f91f0e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -823,7 +823,7 @@ pykwb==0.0.8 pylacrosse==0.3.1 # homeassistant.components.sensor.lastfm -pylast==2.1.0 +pylast==2.2.0 # homeassistant.components.media_player.webostv # homeassistant.components.notify.webostv From 95f2ad2299363cb8943b1ad1b423acc6cc607a74 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:16:01 +0200 Subject: [PATCH 516/924] Upgrade sqlalchemy to 1.2.7 (#14138) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 12 ++++++------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 64e2b85f611..8e69c2cfcd8 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.6'] +REQUIREMENTS = ['sqlalchemy==1.2.7'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index eeca31fa36b..7d18bb3f049 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -19,11 +19,11 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.6'] +REQUIREMENTS = ['sqlalchemy==1.2.7'] +CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' CONF_QUERY = 'query' -CONF_COLUMN_NAME = 'column' def validate_sql_select(value): @@ -34,9 +34,9 @@ def validate_sql_select(value): _QUERY_SCHEME = vol.Schema({ + vol.Required(CONF_COLUMN_NAME): cv.string, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), - vol.Required(CONF_COLUMN_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }) @@ -48,7 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Setup the sensor platform.""" + """Set up the SQL sensor platform.""" db_url = config.get(CONF_DB_URL, None) if not db_url: db_url = DEFAULT_URL.format( @@ -90,10 +90,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SQLSensor(Entity): - """An SQL sensor.""" + """Representation of an SQL sensor.""" def __init__(self, name, sessmaker, query, column, unit, value_template): - """Initialize SQL sensor.""" + """Initialize the SQL sensor.""" self._name = name if "LIMIT" in query: self._query = query diff --git a/requirements_all.txt b/requirements_all.txt index 6eb9f91f0e2..d8ee0286c69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1218,7 +1218,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.6 +sqlalchemy==1.2.7 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 36e8df39bd3..5d6fe9e2f63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.6 +sqlalchemy==1.2.7 # homeassistant.components.statsd statsd==3.2.1 From 449085313b63b0a1896636c7455459647be6b9ed Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:16:34 +0200 Subject: [PATCH 517/924] Upgrade tapsaff to 0.2.0 (#14137) --- homeassistant/components/binary_sensor/tapsaff.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/tapsaff.py b/homeassistant/components/binary_sensor/tapsaff.py index 09d28b96f72..c0f6ca3f112 100644 --- a/homeassistant/components/binary_sensor/tapsaff.py +++ b/homeassistant/components/binary_sensor/tapsaff.py @@ -14,7 +14,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['tapsaff==0.1.3'] +REQUIREMENTS = ['tapsaff==0.2.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d8ee0286c69..f85c44a2965 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1236,7 +1236,7 @@ tahoma-api==0.0.13 tank_utility==1.4.0 # homeassistant.components.binary_sensor.tapsaff -tapsaff==0.1.3 +tapsaff==0.2.0 # homeassistant.components.tellstick tellcore-net==0.4 From 9a9161477fbdece538882625d906785423ff335a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:16:51 +0200 Subject: [PATCH 518/924] Upgrade python-telegram-bot to 10.0.2 (#14144) --- homeassistant/components/telegram_bot/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index e43640e4df2..af0fe5bd572 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.0.1'] +REQUIREMENTS = ['python-telegram-bot==10.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f85c44a2965..87c6aeff8c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ python-synology==0.1.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.0.1 +python-telegram-bot==10.0.2 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From 84f163252aece208703597164d9b4fd042bb8f77 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:17:10 +0200 Subject: [PATCH 519/924] Upgrade youtube_dl to 2018.04.25 (#14136) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index b5fd26b0bcb..fe6ebe8e618 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.04.16'] +REQUIREMENTS = ['youtube_dl==2018.04.25'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 87c6aeff8c2..ec2182fd24f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1364,7 +1364,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.16 +youtube_dl==2018.04.25 # homeassistant.components.light.zengge zengge==0.2 From 2091f86e25285af2860b2709fec0c2f5aef172bb Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sat, 28 Apr 2018 17:17:30 -0400 Subject: [PATCH 520/924] Clean up HomeKit accessory information characteristics (#14114) * Update accessory information characteristics * Add firmware revision characteristic --- .../components/homekit/accessories.py | 21 ++++++++++----- homeassistant/components/homekit/const.py | 8 +++--- tests/components/homekit/test_accessories.py | 26 +++++++++++-------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index d9b90a77d68..c7703b221d8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -7,15 +7,17 @@ import logging from pyhap.accessory import Accessory, Bridge, Category from pyhap.accessory_driver import AccessoryDriver +from homeassistant.const import __version__ from homeassistant.core import callback as ha_callback +from homeassistant.core import split_entity_id from homeassistant.helpers.event import ( async_track_state_change, track_point_in_utc_time) from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, MANUFACTURER, - SERV_ACCESSORY_INFO, CHAR_MANUFACTURER, - CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) from .util import ( show_setup_message, dismiss_setup_message) @@ -84,14 +86,17 @@ def setup_char(char_name, service, value=None, properties=None, callback=None): return char -def set_accessory_info(acc, name, model, manufacturer=MANUFACTURER, - serial_number='0000'): +def set_accessory_info(acc, name, model, serial_number, + manufacturer=MANUFACTURER, + firmware_revision=__version__): """Set the default accessory information.""" service = acc.get_service(SERV_ACCESSORY_INFO) service.get_characteristic(CHAR_NAME).set_value(name) service.get_characteristic(CHAR_MODEL).set_value(model) service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) + service.get_characteristic(CHAR_FIRMWARE_REVISION) \ + .set_value(firmware_revision) class HomeAccessory(Accessory): @@ -100,7 +105,8 @@ class HomeAccessory(Accessory): def __init__(self, hass, name, entity_id, aid, category): """Initialize a Accessory object.""" super().__init__(name, aid=aid) - set_accessory_info(self, name, model=entity_id) + domain = split_entity_id(entity_id)[0].replace("_", " ").title() + set_accessory_info(self, name, model=domain, serial_number=entity_id) self.category = getattr(Category, category, Category.OTHER) self.entity_id = entity_id self.hass = hass @@ -137,7 +143,8 @@ class HomeBridge(Bridge): def __init__(self, hass, name=BRIDGE_NAME): """Initialize a Bridge object.""" super().__init__(name) - set_accessory_info(self, name, model=BRIDGE_MODEL) + set_accessory_info(self, name, model=BRIDGE_MODEL, + serial_number=BRIDGE_SERIAL_NUMBER) self.hass = hass def _set_services(self): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 59444c75421..9c9f60eef94 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -18,9 +18,10 @@ DEFAULT_PORT = 51827 SERVICE_HOMEKIT_START = 'start' # #### STRING CONSTANTS #### -BRIDGE_MODEL = 'homekit.bridge' -BRIDGE_NAME = 'Home Assistant' -MANUFACTURER = 'HomeAssistant' +BRIDGE_MODEL = 'Bridge' +BRIDGE_NAME = 'Home Assistant Bridge' +BRIDGE_SERIAL_NUMBER = 'homekit.bridge' +MANUFACTURER = 'Home Assistant' # #### Categories #### CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' @@ -74,6 +75,7 @@ CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' +CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' CHAR_HUE = 'Hue' # arcdegress | [0, 360] CHAR_LEAK_DETECTED = 'LeakDetected' diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index f8e026483aa..3df76185a51 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -10,8 +10,8 @@ from homeassistant.components.homekit.accessories import ( add_preload_service, set_accessory_info, debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, MANUFACTURER) from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util @@ -92,26 +92,30 @@ class TestAccessories(unittest.TestCase): """Test setting the basic accessory information.""" # Test HomeAccessory acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') serv = acc.get_service(SERV_ACCESSORY_INFO) self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') self.assertEqual( serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') # Test HomeBridge acc = HomeBridge('hass') - set_accessory_info(acc, 'name', 'model', 'manufacturer', '0000') + set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') serv = acc.get_service(SERV_ACCESSORY_INFO) self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, 'manufacturer') self.assertEqual( serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') def test_home_accessory(self): """Test HomeAccessory class.""" @@ -124,7 +128,7 @@ class TestAccessories(unittest.TestCase): self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'homekit.accessory') + serv.get_characteristic(CHAR_MODEL).value, 'Homekit') hass.states.set('homekit.accessory', 'on') hass.block_till_done() @@ -132,13 +136,13 @@ class TestAccessories(unittest.TestCase): hass.states.set('homekit.accessory', 'off') hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model', 2, '') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, '') self.assertEqual(acc.display_name, 'test_name') self.assertEqual(acc.aid, 2) self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'test_model') + serv.get_characteristic(CHAR_MODEL).value, 'Test Model') hass.stop() From 4205dc0f7c6fa227e1b76d88ccf171dc149aae2b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 28 Apr 2018 23:17:38 +0200 Subject: [PATCH 521/924] Upgrade psutil to 5.4.5 (#14135) --- homeassistant/components/sensor/systemmonitor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/systemmonitor.py b/homeassistant/components/sensor/systemmonitor.py index 2f970796fe1..0b85de8e4f2 100644 --- a/homeassistant/components/sensor/systemmonitor.py +++ b/homeassistant/components/sensor/systemmonitor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['psutil==5.4.3'] +REQUIREMENTS = ['psutil==5.4.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ec2182fd24f..e9bd0d71de1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -642,7 +642,7 @@ proliphix==0.4.1 prometheus_client==0.1.0 # homeassistant.components.sensor.systemmonitor -psutil==5.4.3 +psutil==5.4.5 # homeassistant.components.wink pubnubsub-handler==1.0.2 From 07f94eaa928bb91f18de4ec5be6fc4599bd28992 Mon Sep 17 00:00:00 2001 From: Gabe Date: Sat, 28 Apr 2018 17:12:40 -0500 Subject: [PATCH 522/924] Fixed datetime values (#14153) --- homeassistant/components/sensor/sql.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index 7d18bb3f049..b7ece1bdb87 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sql/ """ import decimal +import datetime import logging import voluptuous as vol @@ -145,6 +146,8 @@ class SQLSensor(Entity): for key, value in res.items(): if isinstance(value, decimal.Decimal): value = float(value) + if isinstance(value, datetime.date): + value = str(value) self._attributes[key] = value except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Error executing query %s: %s", self._query, err) From 44ddc6ba62e624ace7870791a1452f67332f17e0 Mon Sep 17 00:00:00 2001 From: engrbm87 Date: Sun, 29 Apr 2018 02:16:22 +0400 Subject: [PATCH 523/924] deluge-components-update (#14016) --- homeassistant/components/sensor/deluge.py | 27 ++++++++++++++----- homeassistant/components/switch/deluge.py | 32 +++++++++++++++++------ requirements_all.txt | 2 +- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/deluge.py b/homeassistant/components/sensor/deluge.py index f4793867d4c..8acbda74d7d 100644 --- a/homeassistant/components/sensor/deluge.py +++ b/homeassistant/components/sensor/deluge.py @@ -14,8 +14,9 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, STATE_IDLE) from homeassistant.helpers.entity import Entity +from homeassistant.exceptions import PlatformNotReady -REQUIREMENTS = ['deluge-client==1.0.5'] +REQUIREMENTS = ['deluge-client==1.4.0'] _LOGGER = logging.getLogger(__name__) _THROTTLED_REFRESH = None @@ -24,7 +25,6 @@ DEFAULT_NAME = 'Deluge' DEFAULT_PORT = 58846 DHT_UPLOAD = 1000 DHT_DOWNLOAD = 1000 - SENSOR_TYPES = { 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'kB/s'], @@ -58,8 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): deluge_api.connect() except ConnectionRefusedError: _LOGGER.error("Connection to Deluge Daemon failed") - return - + raise PlatformNotReady dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(DelugeSensor(variable, deluge_api, name)) @@ -79,6 +78,7 @@ class DelugeSensor(Entity): self._state = None self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] self.data = None + self._available = False @property def name(self): @@ -90,6 +90,11 @@ class DelugeSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def available(self): + """Return true if device is available.""" + return self._available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -97,9 +102,17 @@ class DelugeSensor(Entity): def update(self): """Get the latest data from Deluge and updates the state.""" - self.data = self.client.call('core.get_session_status', - ['upload_rate', 'download_rate', - 'dht_upload_rate', 'dht_download_rate']) + from deluge_client import FailedToReconnectException + try: + self.data = self.client.call('core.get_session_status', + ['upload_rate', 'download_rate', + 'dht_upload_rate', + 'dht_download_rate']) + self._available = True + except FailedToReconnectException: + _LOGGER.error("Connection to Deluge Daemon Lost") + self._available = False + return upload = self.data[b'upload_rate'] - self.data[b'dht_upload_rate'] download = self.data[b'download_rate'] - self.data[ diff --git a/homeassistant/components/switch/deluge.py b/homeassistant/components/switch/deluge.py index 30287a2669e..da0b3bf3228 100644 --- a/homeassistant/components/switch/deluge.py +++ b/homeassistant/components/switch/deluge.py @@ -9,15 +9,16 @@ import logging import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, CONF_PASSWORD, CONF_USERNAME, STATE_OFF, STATE_ON) from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['deluge-client==1.0.5'] +REQUIREMENTS = ['deluge-client==1.4.0'] -_LOGGING = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Deluge Switch' DEFAULT_PORT = 58846 @@ -46,8 +47,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: deluge_api.connect() except ConnectionRefusedError: - _LOGGING.error("Connection to Deluge Daemon failed") - return + _LOGGER.error("Connection to Deluge Daemon failed") + raise PlatformNotReady add_devices([DelugeSwitch(deluge_api, name)]) @@ -60,6 +61,7 @@ class DelugeSwitch(ToggleEntity): self._name = name self.deluge_client = deluge_client self._state = STATE_OFF + self._available = False @property def name(self): @@ -76,18 +78,32 @@ class DelugeSwitch(ToggleEntity): """Return true if device is on.""" return self._state == STATE_ON + @property + def available(self): + """Return true if device is available.""" + return self._available + def turn_on(self, **kwargs): """Turn the device on.""" - self.deluge_client.call('core.resume_all_torrents') + torrent_ids = self.deluge_client.call('core.get_session_state') + self.deluge_client.call('core.resume_torrent', torrent_ids) def turn_off(self, **kwargs): """Turn the device off.""" - self.deluge_client.call('core.pause_all_torrents') + torrent_ids = self.deluge_client.call('core.get_session_state') + self.deluge_client.call('core.pause_torrent', torrent_ids) def update(self): """Get the latest data from deluge and updates the state.""" - torrent_list = self.deluge_client.call('core.get_torrents_status', {}, - ['paused']) + from deluge_client import FailedToReconnectException + try: + torrent_list = self.deluge_client.call('core.get_torrents_status', + {}, ['paused']) + self._available = True + except FailedToReconnectException: + _LOGGER.error("Connection to Deluge Daemon Lost") + self._available = False + return for torrent in torrent_list.values(): item = torrent.popitem() if not item[1]: diff --git a/requirements_all.txt b/requirements_all.txt index e9bd0d71de1..f0be99705c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ defusedxml==0.5.0 # homeassistant.components.sensor.deluge # homeassistant.components.switch.deluge -deluge-client==1.0.5 +deluge-client==1.4.0 # homeassistant.components.media_player.denonavr denonavr==0.6.1 From a0b14c29137a4643243b14c7851a9e7b45edfe7c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 29 Apr 2018 00:33:10 +0200 Subject: [PATCH 524/924] Light mqtt_json: Add HS color support (#14029) * Light mqtt_json HS color support * Lint * Catch float ValueError --- homeassistant/components/light/mqtt_json.py | 27 ++++++++++++++-- tests/components/light/test_mqtt_json.py | 36 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index 20e49e40bae..a0bfc5a0787 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -44,12 +44,14 @@ DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_WHITE_VALUE = False DEFAULT_XY = False +DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 CONF_EFFECT_LIST = 'effect_list' CONF_FLASH_TIME_LONG = 'flash_time_long' CONF_FLASH_TIME_SHORT = 'flash_time_short' +CONF_HS = 'hs' # Stealing some of these from the base MQTT configs. PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -72,6 +74,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, + vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -99,6 +102,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_RGB), config.get(CONF_WHITE_VALUE), config.get(CONF_XY), + config.get(CONF_HS), { key: config.get(key) for key in ( CONF_FLASH_TIME_SHORT, @@ -116,7 +120,7 @@ class MqttJson(MqttAvailability, Light): """Representation of a MQTT JSON light.""" def __init__(self, name, effect_list, topic, qos, retain, optimistic, - brightness, color_temp, effect, rgb, white_value, xy, + brightness, color_temp, effect, rgb, white_value, xy, hs, flash_times, availability_topic, payload_available, payload_not_available, brightness_scale): """Initialize MQTT JSON light.""" @@ -131,6 +135,7 @@ class MqttJson(MqttAvailability, Light): self._state = False self._rgb = rgb self._xy = xy + self._hs_support = hs if brightness: self._brightness = 255 else: @@ -146,7 +151,7 @@ class MqttJson(MqttAvailability, Light): else: self._effect = None - if rgb or xy: + if hs or rgb or xy: self._hs = [0, 0] else: self._hs = None @@ -166,6 +171,7 @@ class MqttJson(MqttAvailability, Light): self._supported_features |= (effect and SUPPORT_EFFECT) self._supported_features |= (white_value and SUPPORT_WHITE_VALUE) self._supported_features |= (xy and SUPPORT_COLOR) + self._supported_features |= (hs and SUPPORT_COLOR) @asyncio.coroutine def async_added_to_hass(self): @@ -193,6 +199,7 @@ class MqttJson(MqttAvailability, Light): pass except ValueError: _LOGGER.warning("Invalid RGB color value received") + try: x_color = float(values['color']['x']) y_color = float(values['color']['y']) @@ -203,6 +210,16 @@ class MqttJson(MqttAvailability, Light): except ValueError: _LOGGER.warning("Invalid XY color value received") + try: + hue = float(values['color']['h']) + saturation = float(values['color']['s']) + + self._hs = (hue, saturation) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid HS color value received") + if self._brightness is not None: try: self._brightness = int(values['brightness'] / @@ -309,7 +326,8 @@ class MqttJson(MqttAvailability, Light): message = {'state': 'ON'} - if ATTR_HS_COLOR in kwargs and (self._rgb or self._xy): + if ATTR_HS_COLOR in kwargs and (self._hs_support + or self._rgb or self._xy): hs_color = kwargs[ATTR_HS_COLOR] message['color'] = {} if self._rgb: @@ -325,6 +343,9 @@ class MqttJson(MqttAvailability, Light): xy_color = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) message['color']['x'] = xy_color[0] message['color']['y'] = xy_color[1] + if self._hs_support: + message['color']['h'] = hs_color[0] + message['color']['s'] = hs_color[1] if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index d6835b00be0..5bae1061b7f 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -146,6 +146,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON"}') self.hass.block_till_done() @@ -158,6 +159,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) def test_controlling_state_via_topic(self): \ # pylint: disable=invalid-name @@ -174,6 +176,7 @@ class TestLightMQTTJSON(unittest.TestCase): 'rgb': True, 'white_value': True, 'xy': True, + 'hs': True, 'qos': '0' } }) @@ -187,6 +190,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertIsNone(state.attributes.get('effect')) self.assertIsNone(state.attributes.get('white_value')) self.assertIsNone(state.attributes.get('xy_color')) + self.assertIsNone(state.attributes.get('hs_color')) self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) # Turn on the light, full white @@ -207,6 +211,7 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual('colorloop', state.attributes.get('effect')) self.assertEqual(150, state.attributes.get('white_value')) self.assertEqual((0.323, 0.329), state.attributes.get('xy_color')) + self.assertEqual((0.0, 0.0), state.attributes.get('hs_color')) # Turn the light off fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"OFF"}') @@ -243,6 +248,15 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual((0.141, 0.14), light_state.attributes.get('xy_color')) + fire_mqtt_message(self.hass, 'test_light_rgb', + '{"state":"ON",' + '"color":{"h":180,"s":50}}') + self.hass.block_till_done() + + light_state = self.hass.states.get('light.test') + self.assertEqual((180.0, 50.0), + light_state.attributes.get('hs_color')) + fire_mqtt_message(self.hass, 'test_light_rgb', '{"state":"ON",' '"color_temp":155}') @@ -361,6 +375,28 @@ class TestLightMQTTJSON(unittest.TestCase): self.assertEqual(50, state.attributes['brightness']) self.assertEqual((125, 100), state.attributes['hs_color']) + def test_sending_hs_color(self): + """Test light.turn_on with hs color sends hs color parameters.""" + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'hs': True, + } + }) + + light.turn_on(self.hass, 'light.test', hs_color=(180.0, 50.0)) + self.hass.block_till_done() + + message_json = json.loads( + self.mock_publish.async_publish.mock_calls[0][1][1]) + self.assertEqual("ON", message_json["state"]) + self.assertEqual({ + 'h': 180.0, + 's': 50.0, + }, message_json["color"]) + def test_flash_short_and_long(self): \ # pylint: disable=invalid-name """Test for flash length being sent when included.""" From a4bf42104489c2ee4558596ae19b106812dafbcb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 29 Apr 2018 01:26:20 +0200 Subject: [PATCH 525/924] Convert more files to async/await syntax (#14142) * Move more files to async/await syntax * Attempt Work around pylint bug Using lazytox :P --- homeassistant/bootstrap.py | 68 +++++++++--------- homeassistant/components/api.py | 50 ++++++------- .../components/device_tracker/gpslogger.py | 9 ++- homeassistant/components/dialogflow.py | 16 ++--- homeassistant/components/fan/mqtt.py | 31 ++++---- .../components/google_assistant/__init__.py | 10 ++- .../components/google_assistant/auth.py | 4 +- .../components/google_assistant/http.py | 8 +-- homeassistant/components/group/__init__.py | 71 ++++++++----------- homeassistant/components/history.py | 14 ++-- homeassistant/components/history_graph.py | 6 +- homeassistant/components/input_boolean.py | 25 +++---- homeassistant/components/light/mqtt_json.py | 19 +++-- homeassistant/components/logbook.py | 12 ++-- homeassistant/components/logger.py | 7 +- homeassistant/components/map.py | 7 +- .../components/media_player/__init__.py | 46 +++++------- .../components/media_player/universal.py | 20 +++--- homeassistant/components/recorder/__init__.py | 8 +-- homeassistant/components/sensor/mqtt.py | 15 ++-- .../components/sensor/wunderground.py | 7 +- homeassistant/components/switch/mqtt.py | 18 ++--- homeassistant/components/timer/__init__.py | 40 +++++------ homeassistant/components/tts/google.py | 12 ++-- homeassistant/components/updater.py | 24 +++---- homeassistant/util/logging.py | 7 +- tests/test_bootstrap.py | 2 +- 27 files changed, 229 insertions(+), 327 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e0962568a66..0abe5a7811e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -67,16 +67,15 @@ def from_config_dict(config: Dict[str, Any], return hass -@asyncio.coroutine -def async_from_config_dict(config: Dict[str, Any], - hass: core.HomeAssistant, - config_dir: Optional[str] = None, - enable_log: bool = True, - verbose: bool = False, - skip_pip: bool = False, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False) \ +async def async_from_config_dict(config: Dict[str, Any], + hass: core.HomeAssistant, + config_dir: Optional[str] = None, + enable_log: bool = True, + verbose: bool = False, + skip_pip: bool = False, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False) \ -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. @@ -92,12 +91,12 @@ def async_from_config_dict(config: Dict[str, Any], core_config = config.get(core.DOMAIN, {}) try: - yield from conf_util.async_process_ha_core_config(hass, core_config) + await conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as ex: conf_util.async_log_exception(ex, 'homeassistant', core_config, hass) return None - yield from hass.async_add_job(conf_util.process_ha_config_upgrade, hass) + await hass.async_add_job(conf_util.process_ha_config_upgrade, hass) hass.config.skip_pip = skip_pip if skip_pip: @@ -105,7 +104,7 @@ def async_from_config_dict(config: Dict[str, Any], "This may cause issues") if not loader.PREPARED: - yield from hass.async_add_job(loader.prepare, hass) + await hass.async_add_job(loader.prepare, hass) # Make a copy because we are mutating it. config = OrderedDict(config) @@ -120,7 +119,7 @@ def async_from_config_dict(config: Dict[str, Any], config[key] = {} hass.config_entries = config_entries.ConfigEntries(hass, config) - yield from hass.config_entries.async_load() + await hass.config_entries.async_load() # Filter out the repeating and common config section [homeassistant] components = set(key.split(' ')[0] for key in config.keys() @@ -129,13 +128,13 @@ def async_from_config_dict(config: Dict[str, Any], # setup components # pylint: disable=not-an-iterable - res = yield from core_components.async_setup(hass, config) + res = await core_components.async_setup(hass, config) if not res: _LOGGER.error("Home Assistant core failed to initialize. " "further initialization aborted") return hass - yield from persistent_notification.async_setup(hass, config) + await persistent_notification.async_setup(hass, config) _LOGGER.info("Home Assistant core initialized") @@ -145,7 +144,7 @@ def async_from_config_dict(config: Dict[str, Any], continue hass.async_add_job(async_setup_component(hass, component, config)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() # stage 2 for component in components: @@ -153,7 +152,7 @@ def async_from_config_dict(config: Dict[str, Any], continue hass.async_add_job(async_setup_component(hass, component, config)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop-start) @@ -187,14 +186,13 @@ def from_config_file(config_path: str, return hass -@asyncio.coroutine -def async_from_config_file(config_path: str, - hass: core.HomeAssistant, - verbose: bool = False, - skip_pip: bool = True, - log_rotate_days: Any = None, - log_file: Any = None, - log_no_color: bool = False): +async def async_from_config_file(config_path: str, + hass: core.HomeAssistant, + verbose: bool = False, + skip_pip: bool = True, + log_rotate_days: Any = None, + log_file: Any = None, + log_no_color: bool = False): """Read the configuration file and try to start all the functionality. Will add functionality to 'hass' parameter. @@ -203,13 +201,13 @@ def async_from_config_file(config_path: str, # Set config dir to directory holding config file config_dir = os.path.abspath(os.path.dirname(config_path)) hass.config.config_dir = config_dir - yield from async_mount_local_lib_path(config_dir, hass.loop) + await async_mount_local_lib_path(config_dir, hass.loop) async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) try: - config_dict = yield from hass.async_add_job( + config_dict = await hass.async_add_job( conf_util.load_yaml_config_file, config_path) except HomeAssistantError as err: _LOGGER.error("Error loading %s: %s", config_path, err) @@ -217,7 +215,7 @@ def async_from_config_file(config_path: str, finally: clear_secret_cache() - hass = yield from async_from_config_dict( + hass = await async_from_config_dict( config_dict, hass, enable_log=False, skip_pip=skip_pip) return hass @@ -294,11 +292,10 @@ def async_enable_logging(hass: core.HomeAssistant, async_handler = AsyncHandler(hass.loop, err_handler) - @asyncio.coroutine - def async_stop_async_handler(event): + async def async_stop_async_handler(event): """Cleanup async handler.""" logging.getLogger('').removeHandler(async_handler) - yield from async_handler.async_close(blocking=True) + await async_handler.async_close(blocking=True) hass.bus.async_listen_once( EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) @@ -323,15 +320,14 @@ def mount_local_lib_path(config_dir: str) -> str: return deps_dir -@asyncio.coroutine -def async_mount_local_lib_path(config_dir: str, - loop: asyncio.AbstractEventLoop) -> str: +async def async_mount_local_lib_path(config_dir: str, + loop: asyncio.AbstractEventLoop) -> str: """Add local library to Python Path. This function is a coroutine. """ deps_dir = os.path.join(config_dir, 'deps') - lib_dir = yield from async_get_user_site(deps_dir, loop=loop) + lib_dir = await async_get_user_site(deps_dir, loop=loop) if lib_dir not in sys.path: sys.path.insert(0, lib_dir) return deps_dir diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 6fdf0c027a4..83e05dae641 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -76,8 +76,7 @@ class APIEventStream(HomeAssistantView): url = URL_API_STREAM name = "api:stream" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Provide a streaming interface for the event bus.""" # pylint: disable=no-self-use hass = request.app['hass'] @@ -88,8 +87,7 @@ class APIEventStream(HomeAssistantView): if restrict: restrict = restrict.split(',') + [EVENT_HOMEASSISTANT_STOP] - @asyncio.coroutine - def forward_events(event): + async def forward_events(event): """Forward events to the open request.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -104,11 +102,11 @@ class APIEventStream(HomeAssistantView): else: data = json.dumps(event, cls=rem.JSONEncoder) - yield from to_write.put(data) + await to_write.put(data) response = web.StreamResponse() response.content_type = 'text/event-stream' - yield from response.prepare(request) + await response.prepare(request) unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) @@ -116,13 +114,13 @@ class APIEventStream(HomeAssistantView): _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) # Fire off one message so browsers fire open event right away - yield from to_write.put(STREAM_PING_PAYLOAD) + await to_write.put(STREAM_PING_PAYLOAD) while True: try: with async_timeout.timeout(STREAM_PING_INTERVAL, loop=hass.loop): - payload = yield from to_write.get() + payload = await to_write.get() if payload is stop_obj: break @@ -130,9 +128,9 @@ class APIEventStream(HomeAssistantView): msg = "data: {}\n\n".format(payload) _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), msg.strip()) - yield from response.write(msg.encode("UTF-8")) + await response.write(msg.encode("UTF-8")) except asyncio.TimeoutError: - yield from to_write.put(STREAM_PING_PAYLOAD) + await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: _LOGGER.debug('STREAM %s ABORT', id(stop_obj)) @@ -200,12 +198,11 @@ class APIEntityStateView(HomeAssistantView): return self.json(state) return self.json_message('Entity not found', HTTP_NOT_FOUND) - @asyncio.coroutine - def post(self, request, entity_id): + async def post(self, request, entity_id): """Update state of entity.""" hass = request.app['hass'] try: - data = yield from request.json() + data = await request.json() except ValueError: return self.json_message('Invalid JSON specified', HTTP_BAD_REQUEST) @@ -257,10 +254,9 @@ class APIEventView(HomeAssistantView): url = '/api/events/{event_type}' name = "api:event" - @asyncio.coroutine - def post(self, request, event_type): + async def post(self, request, event_type): """Fire events.""" - body = yield from request.text() + body = await request.text() try: event_data = json.loads(body) if body else None except ValueError: @@ -292,10 +288,9 @@ class APIServicesView(HomeAssistantView): url = URL_API_SERVICES name = "api:services" - @asyncio.coroutine - def get(self, request): + async def get(self, request): """Get registered services.""" - services = yield from async_services_json(request.app['hass']) + services = await async_services_json(request.app['hass']) return self.json(services) @@ -305,14 +300,13 @@ class APIDomainServicesView(HomeAssistantView): url = "/api/services/{domain}/{service}" name = "api:domain-services" - @asyncio.coroutine - def post(self, request, domain, service): + async def post(self, request, domain, service): """Call a service. Returns a list of changed states. """ hass = request.app['hass'] - body = yield from request.text() + body = await request.text() try: data = json.loads(body) if body else None except ValueError: @@ -320,7 +314,7 @@ class APIDomainServicesView(HomeAssistantView): HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: - yield from hass.services.async_call(domain, service, data, True) + await hass.services.async_call(domain, service, data, True) return self.json(changed_states) @@ -343,11 +337,10 @@ class APITemplateView(HomeAssistantView): url = URL_API_TEMPLATE name = "api:template" - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Render a template.""" try: - data = yield from request.json() + data = await request.json() tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: @@ -366,10 +359,9 @@ class APIErrorLog(HomeAssistantView): return await self.file(request, request.app['hass'].data[DATA_LOGGING]) -@asyncio.coroutine -def async_services_json(hass): +async def async_services_json(hass): """Generate services data to JSONify.""" - descriptions = yield from async_get_all_descriptions(hass) + descriptions = await async_get_all_descriptions(hass) return [{"domain": key, "services": value} for key, value in descriptions.items()] diff --git a/homeassistant/components/device_tracker/gpslogger.py b/homeassistant/components/device_tracker/gpslogger.py index 1952e6d676d..68ea9ac88ae 100644 --- a/homeassistant/components/device_tracker/gpslogger.py +++ b/homeassistant/components/device_tracker/gpslogger.py @@ -4,7 +4,6 @@ Support for the GPSLogger platform. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.gpslogger/ """ -import asyncio import logging from hmac import compare_digest @@ -22,6 +21,7 @@ from homeassistant.components.http import ( from homeassistant.components.device_tracker import ( # NOQA DOMAIN, PLATFORM_SCHEMA ) +from homeassistant.helpers.typing import HomeAssistantType, ConfigType _LOGGER = logging.getLogger(__name__) @@ -32,8 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner(hass: HomeAssistantType, config: ConfigType, + async_see, discovery_info=None): """Set up an endpoint for the GPSLogger application.""" hass.http.register_view(GPSLoggerView(async_see, config)) @@ -54,8 +54,7 @@ class GPSLoggerView(HomeAssistantView): # password is set self.requires_auth = self._password is None - @asyncio.coroutine - def get(self, request: Request): + async def get(self, request: Request): """Handle for GPSLogger message received as GET.""" hass = request.app['hass'] data = request.query diff --git a/homeassistant/components/dialogflow.py b/homeassistant/components/dialogflow.py index 63205c5479c..7a0918aab25 100644 --- a/homeassistant/components/dialogflow.py +++ b/homeassistant/components/dialogflow.py @@ -4,7 +4,6 @@ Support for Dialogflow webhook. For more details about this component, please refer to the documentation at https://home-assistant.io/components/dialogflow/ """ -import asyncio import logging import voluptuous as vol @@ -37,8 +36,7 @@ class DialogFlowError(HomeAssistantError): """Raised when a DialogFlow error happens.""" -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up Dialogflow component.""" hass.http.register_view(DialogflowIntentsView) @@ -51,16 +49,15 @@ class DialogflowIntentsView(HomeAssistantView): url = INTENTS_API_ENDPOINT name = 'api:dialogflow' - @asyncio.coroutine - def post(self, request): + async def post(self, request): """Handle Dialogflow.""" hass = request.app['hass'] - message = yield from request.json() + message = await request.json() _LOGGER.debug("Received Dialogflow request: %s", message) try: - response = yield from async_handle_message(hass, message) + response = await async_handle_message(hass, message) return b'' if response is None else self.json(response) except DialogFlowError as err: @@ -93,8 +90,7 @@ def dialogflow_error_response(hass, message, error): return dialogflow_response.as_dict() -@asyncio.coroutine -def async_handle_message(hass, message): +async def async_handle_message(hass, message): """Handle a DialogFlow message.""" req = message.get('result') action_incomplete = req['actionIncomplete'] @@ -110,7 +106,7 @@ def async_handle_message(hass, message): raise DialogFlowError( "You have not defined an action in your Dialogflow intent.") - intent_response = yield from intent.async_handle( + intent_response = await intent.async_handle( hass, DOMAIN, action, {key: {'value': value} for key, value in parameters.items()}) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 95ff587c613..6fa506edec6 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT fans. For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -19,6 +18,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, @@ -77,8 +77,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the MQTT fan platform.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -149,10 +149,9 @@ class MqttFan(MqttAvailability, FanEntity): self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() templates = {} for key, tpl in list(self._templates.items()): @@ -173,7 +172,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) @@ -190,7 +189,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_SPEED_STATE_TOPIC], speed_received, self._qos) self._speed = SPEED_OFF @@ -206,7 +205,7 @@ class MqttFan(MqttAvailability, FanEntity): self.async_schedule_update_ha_state() if self._topic[CONF_OSCILLATION_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_OSCILLATION_STATE_TOPIC], oscillation_received, self._qos) self._oscillation = False @@ -251,8 +250,7 @@ class MqttFan(MqttAvailability, FanEntity): """Return the oscillation state.""" return self._oscillation - @asyncio.coroutine - def async_turn_on(self, speed: str = None, **kwargs) -> None: + async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn on the entity. This method is a coroutine. @@ -261,10 +259,9 @@ class MqttFan(MqttAvailability, FanEntity): self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload[STATE_ON], self._qos, self._retain) if speed: - yield from self.async_set_speed(speed) + await self.async_set_speed(speed) - @asyncio.coroutine - def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn off the entity. This method is a coroutine. @@ -273,8 +270,7 @@ class MqttFan(MqttAvailability, FanEntity): self.hass, self._topic[CONF_COMMAND_TOPIC], self._payload[STATE_OFF], self._qos, self._retain) - @asyncio.coroutine - def async_set_speed(self, speed: str) -> None: + async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan. This method is a coroutine. @@ -299,8 +295,7 @@ class MqttFan(MqttAvailability, FanEntity): self._speed = speed self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_oscillate(self, oscillating: bool) -> None: + async def async_oscillate(self, oscillating: bool) -> None: """Set oscillation. This method is a coroutine. diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 676654c2c91..1c6d11a7c99 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -70,8 +70,7 @@ def request_sync(hass): hass.services.call(DOMAIN, SERVICE_REQUEST_SYNC) -@asyncio.coroutine -def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): +async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Actions component.""" config = yaml_config.get(DOMAIN, {}) agent_user_id = config.get(CONF_AGENT_USER_ID) @@ -79,20 +78,19 @@ def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): hass.http.register_view(GoogleAssistantAuthView(hass, config)) async_register_http(hass, config) - @asyncio.coroutine - def request_sync_service_handler(call): + async def request_sync_service_handler(call): """Handle request sync service calls.""" websession = async_get_clientsession(hass) try: with async_timeout.timeout(5, loop=hass.loop): - res = yield from websession.post( + res = await websession.post( REQUEST_SYNC_BASE_URL, params={'key': api_key}, json={'agent_user_id': agent_user_id}) _LOGGER.info("Submitted request_sync request to Google") res.raise_for_status() except aiohttp.ClientResponseError: - body = yield from res.read() + body = await res.read() _LOGGER.error( 'request_sync request failed: %d %s', res.status, body) except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/google_assistant/auth.py b/homeassistant/components/google_assistant/auth.py index 1ed27403797..a21dd0e6738 100644 --- a/homeassistant/components/google_assistant/auth.py +++ b/homeassistant/components/google_assistant/auth.py @@ -1,6 +1,5 @@ """Google Assistant OAuth View.""" -import asyncio import logging # Typing imports @@ -44,8 +43,7 @@ class GoogleAssistantAuthView(HomeAssistantView): self.client_id = cfg.get(CONF_CLIENT_ID) self.access_token = cfg.get(CONF_ACCESS_TOKEN) - @asyncio.coroutine - def get(self, request: Request) -> Response: + async def get(self, request: Request) -> Response: """Handle oauth token request.""" query = request.query redirect_uri = query.get('redirect_uri') diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 0caea3aadf4..0ea5f7d9fa4 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -4,7 +4,6 @@ Support for Google Actions Smart Home Control. For more details about this component, please refer to the documentation at https://home-assistant.io/components/google_assistant/ """ -import asyncio import logging from aiohttp.hdrs import AUTHORIZATION @@ -77,14 +76,13 @@ class GoogleAssistantView(HomeAssistantView): self.access_token = access_token self.gass_config = gass_config - @asyncio.coroutine - def post(self, request: Request) -> Response: + async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" auth = request.headers.get(AUTHORIZATION, None) if 'Bearer {}'.format(self.access_token) != auth: return self.json_message("missing authorization", status_code=401) - message = yield from request.json() # type: dict - result = yield from async_handle_message( + message = await request.json() # type: dict + result = await async_handle_message( request.app['hass'], self.gass_config, message) return self.json(result) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 67ad8066aff..f70a2d29351 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -245,34 +245,31 @@ def get_entity_ids(hass, entity_id, domain_filter=None): if ent_id.startswith(domain_filter)] -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up all groups found defined in the configuration.""" component = hass.data.get(DOMAIN) if component is None: component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) - yield from _async_process_config(hass, config, component) + await _async_process_config(hass, config, component) - @asyncio.coroutine - def reload_service_handler(service): + async def reload_service_handler(service): """Remove all user-defined groups and load new ones from config.""" auto = list(filter(lambda e: not e.user_defined, component.entities)) - conf = yield from component.async_prepare_reload() + conf = await component.async_prepare_reload() if conf is None: return - yield from _async_process_config(hass, conf, component) + await _async_process_config(hass, conf, component) - yield from component.async_add_entities(auto) + await component.async_add_entities(auto) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA) - @asyncio.coroutine - def groups_service_handler(service): + async def groups_service_handler(service): """Handle dynamic group service functions.""" object_id = service.data[ATTR_OBJECT_ID] entity_id = ENTITY_ID_FORMAT.format(object_id) @@ -287,7 +284,7 @@ def async_setup(hass, config): ATTR_VISIBLE, ATTR_ICON, ATTR_VIEW, ATTR_CONTROL ) if service.data.get(attr) is not None} - yield from Group.async_create_group( + await Group.async_create_group( hass, service.data.get(ATTR_NAME, object_id), object_id=object_id, entity_ids=entity_ids, @@ -308,11 +305,11 @@ def async_setup(hass, config): if ATTR_ADD_ENTITIES in service.data: delta = service.data[ATTR_ADD_ENTITIES] entity_ids = set(group.tracking) | set(delta) - yield from group.async_update_tracked_entity_ids(entity_ids) + await group.async_update_tracked_entity_ids(entity_ids) if ATTR_ENTITIES in service.data: entity_ids = service.data[ATTR_ENTITIES] - yield from group.async_update_tracked_entity_ids(entity_ids) + await group.async_update_tracked_entity_ids(entity_ids) if ATTR_NAME in service.data: group.name = service.data[ATTR_NAME] @@ -335,13 +332,13 @@ def async_setup(hass, config): need_update = True if need_update: - yield from group.async_update_ha_state() + await group.async_update_ha_state() return # remove group if service.service == SERVICE_REMOVE: - yield from component.async_remove_entity(entity_id) + await component.async_remove_entity(entity_id) hass.services.async_register( DOMAIN, SERVICE_SET, groups_service_handler, @@ -351,8 +348,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_REMOVE, groups_service_handler, schema=REMOVE_SERVICE_SCHEMA) - @asyncio.coroutine - def visibility_service_handler(service): + async def visibility_service_handler(service): """Change visibility of a group.""" visible = service.data.get(ATTR_VISIBLE) @@ -363,7 +359,7 @@ def async_setup(hass, config): tasks.append(group.async_update_ha_state()) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_SET_VISIBILITY, visibility_service_handler, @@ -372,8 +368,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def _async_process_config(hass, config, component): +async def _async_process_config(hass, config, component): """Process group configuration.""" for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) @@ -384,7 +379,7 @@ def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. - yield from Group.async_create_group( + await Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, control=control, object_id=object_id) @@ -428,10 +423,9 @@ class Group(Entity): hass.loop).result() @staticmethod - @asyncio.coroutine - def async_create_group(hass, name, entity_ids=None, user_defined=True, - visible=True, icon=None, view=False, control=None, - object_id=None): + async def async_create_group(hass, name, entity_ids=None, + user_defined=True, visible=True, icon=None, + view=False, control=None, object_id=None): """Initialize a group. This method must be run in the event loop. @@ -453,7 +447,7 @@ class Group(Entity): component = hass.data[DOMAIN] = \ EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([group], True) + await component.async_add_entities([group], True) return group @@ -520,17 +514,16 @@ class Group(Entity): self.async_update_tracked_entity_ids(entity_ids), self.hass.loop ).result() - @asyncio.coroutine - def async_update_tracked_entity_ids(self, entity_ids): + async def async_update_tracked_entity_ids(self, entity_ids): """Update the member entity IDs. This method must be run in the event loop. """ - yield from self.async_stop() + await self.async_stop() self.tracking = tuple(ent_id.lower() for ent_id in entity_ids) self.group_on, self.group_off = None, None - yield from self.async_update_ha_state(True) + await self.async_update_ha_state(True) self.async_start() @callback @@ -544,8 +537,7 @@ class Group(Entity): self.hass, self.tracking, self._async_state_changed_listener ) - @asyncio.coroutine - def async_stop(self): + async def async_stop(self): """Unregister the group from Home Assistant. This method must be run in the event loop. @@ -554,27 +546,24 @@ class Group(Entity): self._async_unsub_state_changed() self._async_unsub_state_changed = None - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Query all members and determine current group state.""" self._state = STATE_UNKNOWN self._async_update_group_state() - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Callback when added to HASS.""" if self.tracking: self.async_start() - @asyncio.coroutine - def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self): """Callback when removed from HASS.""" if self._async_unsub_state_changed: self._async_unsub_state_changed() self._async_unsub_state_changed = None - @asyncio.coroutine - def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, entity_id, old_state, + new_state): """Respond to a member state changing. This method must be run in the event loop. @@ -584,7 +573,7 @@ class Group(Entity): return self._async_update_group_state(new_state) - yield from self.async_update_ha_state() + await self.async_update_ha_state() @property def _tracking_states(self): diff --git a/homeassistant/components/history.py b/homeassistant/components/history.py index b5ac37b1451..c27e394ce28 100644 --- a/homeassistant/components/history.py +++ b/homeassistant/components/history.py @@ -4,7 +4,6 @@ Provide pre-made queries on top of the recorder component. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history/ """ -import asyncio from collections import defaultdict from datetime import timedelta from itertools import groupby @@ -259,8 +258,7 @@ def get_state(hass, utc_point_in_time, entity_id, run=None): return states[0] if states else None -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the history hooks.""" filters = Filters() conf = config.get(DOMAIN, {}) @@ -275,7 +273,7 @@ def async_setup(hass, config): use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'history', 'history', 'mdi:poll-box') return True @@ -293,8 +291,7 @@ class HistoryPeriodView(HomeAssistantView): self.filters = filters self.use_include_order = use_include_order - @asyncio.coroutine - def get(self, request, datetime=None): + async def get(self, request, datetime=None): """Return history over a period of time.""" timer_start = time.perf_counter() if datetime: @@ -330,7 +327,7 @@ class HistoryPeriodView(HomeAssistantView): hass = request.app['hass'] - result = yield from hass.async_add_job( + result = await hass.async_add_job( get_significant_states, hass, start_time, end_time, entity_ids, self.filters, include_start_time_state) result = list(result.values()) @@ -353,8 +350,7 @@ class HistoryPeriodView(HomeAssistantView): sorted_result.extend(result) result = sorted_result - response = yield from hass.async_add_job(self.json, result) - return response + return await hass.async_add_job(self.json, result) class Filters(object): diff --git a/homeassistant/components/history_graph.py b/homeassistant/components/history_graph.py index e6977d60c30..fa7d615dce2 100644 --- a/homeassistant/components/history_graph.py +++ b/homeassistant/components/history_graph.py @@ -4,7 +4,6 @@ Support to graphs card in the UI. For more details about this component, please refer to the documentation at https://home-assistant.io/components/history_graph/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Load graph configurations.""" component = EntityComponent( _LOGGER, DOMAIN, hass) @@ -51,7 +49,7 @@ def async_setup(hass, config): graph = HistoryGraphEntity(name, cfg) graphs.append(graph) - yield from component.async_add_entities(graphs) + await component.async_add_entities(graphs) return True diff --git a/homeassistant/components/input_boolean.py b/homeassistant/components/input_boolean.py index 56761b5af4e..9c8435614a2 100644 --- a/homeassistant/components/input_boolean.py +++ b/homeassistant/components/input_boolean.py @@ -65,8 +65,7 @@ def toggle(hass, entity_id): hass.services.call(DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: entity_id}) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -85,8 +84,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a calls to the input boolean services.""" target_inputs = component.async_extract_from_service(service) @@ -99,7 +97,7 @@ def async_setup(hass, config): tasks = [getattr(input_b, attr)() for input_b in target_inputs] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_TURN_OFF, async_handler_service, @@ -111,7 +109,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_TOGGLE, async_handler_service, schema=SERVICE_SCHEMA) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -145,24 +143,21 @@ class InputBoolean(ToggleEntity): """Return true if entity is on.""" return self._state - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" # If not None, we got an initial value. if self._state is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) self._state = state and state.state == STATE_ON - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on.""" self._state = True - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off.""" self._state = False - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index a0bfc5a0787..ca5c76e905f 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -4,7 +4,6 @@ Support for MQTT JSON lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_json/ """ -import asyncio import logging import json import voluptuous as vol @@ -26,6 +25,7 @@ from homeassistant.components.mqtt import ( CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, MqttAvailability) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -79,8 +79,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up a MQTT JSON Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -173,10 +173,9 @@ class MqttJson(MqttAvailability, Light): self._supported_features |= (xy and SUPPORT_COLOR) self._supported_features |= (hs and SUPPORT_COLOR) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def state_received(topic, payload, qos): @@ -257,7 +256,7 @@ class MqttJson(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) @@ -316,8 +315,7 @@ class MqttJson(MqttAvailability, Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -404,8 +402,7 @@ class MqttJson(MqttAvailability, Light): if should_update: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 1c3e8ed1f19..8bab6fe0440 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,7 +4,6 @@ Event parser and human readable log generator. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ -import asyncio import logging from datetime import timedelta from itertools import groupby @@ -88,8 +87,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) -@asyncio.coroutine -def setup(hass, config): +async def setup(hass, config): """Listen for download events to download files.""" @callback def log_message(service): @@ -105,7 +103,7 @@ def setup(hass, config): hass.http.register_view(LogbookView(config.get(DOMAIN, {}))) - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'logbook', 'logbook', 'mdi:format-list-bulleted-type') hass.services.async_register( @@ -124,8 +122,7 @@ class LogbookView(HomeAssistantView): """Initialize the logbook view.""" self.config = config - @asyncio.coroutine - def get(self, request, datetime=None): + async def get(self, request, datetime=None): """Retrieve logbook entries.""" if datetime: datetime = dt_util.parse_datetime(datetime) @@ -144,8 +141,7 @@ class LogbookView(HomeAssistantView): return self.json(list( _get_events(hass, self.config, start_day, end_day))) - response = yield from hass.async_add_job(json_events) - return response + return await hass.async_add_job(json_events) class Entry(object): diff --git a/homeassistant/components/logger.py b/homeassistant/components/logger.py index c2309401977..6e8995a0444 100644 --- a/homeassistant/components/logger.py +++ b/homeassistant/components/logger.py @@ -4,7 +4,6 @@ Component that will help set the level of logging for components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logger/ """ -import asyncio import logging from collections import OrderedDict @@ -73,8 +72,7 @@ class HomeAssistantLogFilter(logging.Filter): return record.levelno >= default -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the logger component.""" logfilter = {} @@ -116,8 +114,7 @@ def async_setup(hass, config): if LOGGER_LOGS in config.get(DOMAIN): set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Handle logger services.""" set_log_levels(service.data) diff --git a/homeassistant/components/map.py b/homeassistant/components/map.py index b8293f64fc0..30cb00af69e 100644 --- a/homeassistant/components/map.py +++ b/homeassistant/components/map.py @@ -4,14 +4,11 @@ Provides a map panel for showing device locations. For more details about this component, please refer to the documentation at https://home-assistant.io/components/map/ """ -import asyncio - DOMAIN = 'map' -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Register the built-in map panel.""" - yield from hass.components.frontend.async_register_built_in_panel( + await hass.components.frontend.async_register_built_in_panel( 'map', 'map', 'mdi:account-location') return True diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 615c758cd1a..20fd3b875c8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -361,18 +361,16 @@ def set_shuffle(hass, shuffle, entity_id=None): hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Track states and offer events for media_players.""" component = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) hass.http.register_view(MediaPlayerImageView(component)) - yield from component.async_setup(config) + await component.async_setup(config) - @asyncio.coroutine - def async_service_handler(service): + async def async_service_handler(service): """Map services to methods on MediaPlayerDevice.""" method = SERVICE_TO_METHOD.get(service.service) if not method: @@ -400,13 +398,13 @@ def async_setup(hass, config): update_tasks = [] for player in target_players: - yield from getattr(player, method['method'])(**params) + await getattr(player, method['method'])(**params) if not player.should_poll: continue update_tasks.append(player.async_update_ha_state(True)) if update_tasks: - yield from asyncio.wait(update_tasks, loop=hass.loop) + await asyncio.wait(update_tasks, loop=hass.loop) for service in SERVICE_TO_METHOD: schema = SERVICE_TO_METHOD[service].get( @@ -490,14 +488,13 @@ class MediaPlayerDevice(Entity): return None - @asyncio.coroutine - def async_get_media_image(self): + async def async_get_media_image(self): """Fetch media image of current playing image.""" url = self.media_image_url if url is None: return None, None - return (yield from _async_fetch_image(self.hass, url)) + return await _async_fetch_image(self.hass, url) @property def media_title(self): @@ -808,34 +805,31 @@ class MediaPlayerDevice(Entity): return self.async_turn_on() return self.async_turn_off() - @asyncio.coroutine - def async_volume_up(self): + async def async_volume_up(self): """Turn volume up for media player. This method is a coroutine. """ if hasattr(self, 'volume_up'): # pylint: disable=no-member - yield from self.hass.async_add_job(self.volume_up) + await self.hass.async_add_job(self.volume_up) return if self.volume_level < 1: - yield from self.async_set_volume_level( - min(1, self.volume_level + .1)) + await self.async_set_volume_level(min(1, self.volume_level + .1)) - @asyncio.coroutine - def async_volume_down(self): + async def async_volume_down(self): """Turn volume down for media player. This method is a coroutine. """ if hasattr(self, 'volume_down'): # pylint: disable=no-member - yield from self.hass.async_add_job(self.volume_down) + await self.hass.async_add_job(self.volume_down) return if self.volume_level > 0: - yield from self.async_set_volume_level( + await self.async_set_volume_level( max(0, self.volume_level - .1)) def async_media_play_pause(self): @@ -879,8 +873,7 @@ class MediaPlayerDevice(Entity): return state_attr -@asyncio.coroutine -def _async_fetch_image(hass, url): +async def _async_fetch_image(hass, url): """Fetch image. Images are cached in memory (the images are typically 10-100kB in size). @@ -891,7 +884,7 @@ def _async_fetch_image(hass, url): if url not in cache_images: cache_images[url] = {CACHE_LOCK: asyncio.Lock(loop=hass.loop)} - with (yield from cache_images[url][CACHE_LOCK]): + async with cache_images[url][CACHE_LOCK]: if CACHE_CONTENT in cache_images[url]: return cache_images[url][CACHE_CONTENT] @@ -899,10 +892,10 @@ def _async_fetch_image(hass, url): websession = async_get_clientsession(hass) try: with async_timeout.timeout(10, loop=hass.loop): - response = yield from websession.get(url) + response = await websession.get(url) if response.status == 200: - content = yield from response.read() + content = await response.read() content_type = response.headers.get(CONTENT_TYPE) if content_type: content_type = content_type.split(';')[0] @@ -928,8 +921,7 @@ class MediaPlayerImageView(HomeAssistantView): """Initialize a media player view.""" self.component = component - @asyncio.coroutine - def get(self, request, entity_id): + async def get(self, request, entity_id): """Start a get request.""" player = self.component.get_entity(entity_id) if player is None: @@ -942,7 +934,7 @@ class MediaPlayerImageView(HomeAssistantView): if not authenticated: return web.Response(status=401) - data, content_type = yield from player.async_get_media_image() + data, content_type = await player.async_get_media_image() if data is None: return web.Response(status=500) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index 27a0714527d..fa4f03f1179 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -4,7 +4,6 @@ Combination of multiple media players into one for a universal controller. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.universal/ """ -import asyncio import logging # pylint: disable=import-error from copy import copy @@ -63,8 +62,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }, extra=vol.REMOVE_EXTRA) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the universal media players.""" player = UniversalMediaPlayer( hass, @@ -99,8 +98,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): if state_template is not None: self._state_template.hass = hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to children and template state changes. This method must be run in the event loop and returns a coroutine. @@ -144,15 +142,14 @@ class UniversalMediaPlayer(MediaPlayerDevice): active_child = self._child_state return active_child.attributes.get(attr_name) if active_child else None - @asyncio.coroutine - def _async_call_service(self, service_name, service_data=None, - allow_override=False): + async def _async_call_service(self, service_name, service_data=None, + allow_override=False): """Call either a specified or active child's service.""" if service_data is None: service_data = {} if allow_override and service_name in self._cmds: - yield from async_call_from_config( + await async_call_from_config( self.hass, self._cmds[service_name], variables=service_data, blocking=True, validate_config=False) @@ -165,7 +162,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): service_data[ATTR_ENTITY_ID] = active_child.entity_id - yield from self.hass.services.async_call( + await self.hass.services.async_call( DOMAIN, service_name, service_data, blocking=True) @property @@ -506,8 +503,7 @@ class UniversalMediaPlayer(MediaPlayerDevice): return self._async_call_service( SERVICE_SHUFFLE_SET, data, allow_override=True) - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update state in HA.""" for child_name in self._children: child_state = self.hass.states.get(child_name) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 8e69c2cfcd8..9b5bea043f4 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -111,8 +111,7 @@ def run_information(hass, point_in_time: Optional[datetime] = None): return res -@asyncio.coroutine -def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" conf = config.get(DOMAIN, {}) keep_days = conf.get(CONF_PURGE_KEEP_DAYS) @@ -131,8 +130,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_initialize() instance.start() - @asyncio.coroutine - def async_handle_purge_service(service): + async def async_handle_purge_service(service): """Handle calls to the purge service.""" instance.do_adhoc_purge(**service.data) @@ -140,7 +138,7 @@ def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA) - return (yield from instance.async_db_ready) + return await instance.async_db_ready PurgeTask = namedtuple('PurgeTask', ['keep_days', 'repack']) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index c4f64e9e015..d7d66a3a145 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt/ """ -import asyncio import logging import json from datetime import timedelta @@ -22,6 +21,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util @@ -48,8 +48,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up MQTT Sensor.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -100,10 +100,9 @@ class MqttSensor(MqttAvailability, Entity): self._unique_id = unique_id self._attributes = None - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def message_received(topic, payload, qos): @@ -142,8 +141,8 @@ class MqttSensor(MqttAvailability, Entity): self._state = payload self.async_schedule_update_ha_state() - yield from mqtt.async_subscribe( - self.hass, self._state_topic, message_received, self._qos) + await mqtt.async_subscribe(self.hass, self._state_topic, + message_received, self._qos) @callback def value_is_expired(self, *_): diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index 7938b17e4d6..bbee167d4b0 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -639,9 +639,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' -@asyncio.coroutine -def async_setup_platform(hass: HomeAssistantType, config: ConfigType, - async_add_devices, discovery_info=None): +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) @@ -656,7 +655,7 @@ def async_setup_platform(hass: HomeAssistantType, config: ConfigType, for variable in config[CONF_MONITORED_CONDITIONS]: sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) - yield from rest.async_update() + await rest.async_update() if not rest.data: raise PlatformNotReady diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index f3bd0bef012..15dc6f1d0f4 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT switches. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -39,8 +38,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the MQTT switch.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -88,10 +87,9 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._optimistic = optimistic self._template = value_template - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() @callback def state_message_received(topic, payload, qos): @@ -110,7 +108,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): # Force into optimistic mode. self._optimistic = True else: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._state_topic, state_message_received, self._qos) @@ -139,8 +137,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return the icon.""" return self._icon - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -153,8 +150,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._state = True self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 84d2d3f349d..5a363e84d7b 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -122,8 +122,7 @@ def async_finish(hass, entity_id): DOMAIN, SERVICE_FINISH, {ATTR_ENTITY_ID: entity_id})) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -142,8 +141,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a call to the timer services.""" target_timers = component.async_extract_from_service(service) @@ -162,7 +160,7 @@ def async_setup(hass, config): timer.async_start(service.data.get(ATTR_DURATION)) ) if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_START, async_handler_service, @@ -177,7 +175,7 @@ def async_setup(hass, config): DOMAIN, SERVICE_FINISH, async_handler_service, schema=SERVICE_SCHEMA) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -224,19 +222,17 @@ class Timer(Entity): ATTR_REMAINING: str(self._remaining) } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity is about to be added to Home Assistant.""" # If not None, we got an initial value. if self._state is not None: return restore_state = self._hass.helpers.restore_state - state = yield from restore_state.async_get_last_state(self.entity_id) + state = await restore_state.async_get_last_state(self.entity_id) self._state = state and state.state == state - @asyncio.coroutine - def async_start(self, duration): + async def async_start(self, duration): """Start a timer.""" if self._listener: self._listener() @@ -260,10 +256,9 @@ class Timer(Entity): self._listener = async_track_point_in_utc_time(self._hass, self.async_finished, self._end) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_pause(self): + async def async_pause(self): """Pause a timer.""" if self._listener is None: return @@ -273,10 +268,9 @@ class Timer(Entity): self._remaining = self._end - dt_util.utcnow() self._state = STATUS_PAUSED self._end = None - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_cancel(self): + async def async_cancel(self): """Cancel a timer.""" if self._listener: self._listener() @@ -286,10 +280,9 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_finish(self): + async def async_finish(self): """Reset and updates the states, fire finished event.""" if self._state != STATUS_ACTIVE: return @@ -299,10 +292,9 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_finished(self, time): + async def async_finished(self, time): """Reset and updates the states, fire finished event.""" if self._state != STATUS_ACTIVE: return @@ -312,4 +304,4 @@ class Timer(Entity): self._remaining = timedelta() self._hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id}) - yield from self.async_update_ha_state() + await self.async_update_ha_state() diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index 084a7229212..bf03ec1adad 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -39,8 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_get_engine(hass, config): +async def async_get_engine(hass, config): """Set up Google speech component.""" return GoogleProvider(hass, config[CONF_LANG]) @@ -70,8 +69,7 @@ class GoogleProvider(Provider): """Return list of supported languages.""" return SUPPORT_LANGUAGES - @asyncio.coroutine - def async_get_tts_audio(self, message, language, options=None): + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" from gtts_token import gtts_token @@ -81,7 +79,7 @@ class GoogleProvider(Provider): data = b'' for idx, part in enumerate(message_parts): - part_token = yield from self.hass.async_add_job( + part_token = await self.hass.async_add_job( token.calculate_token, part) url_param = { @@ -97,7 +95,7 @@ class GoogleProvider(Provider): try: with async_timeout.timeout(10, loop=self.hass.loop): - request = yield from websession.get( + request = await websession.get( GOOGLE_SPEECH_URL, params=url_param, headers=self.headers ) @@ -106,7 +104,7 @@ class GoogleProvider(Provider): _LOGGER.error("Error %d on load url %s", request.status, request.url) return (None, None) - data += yield from request.read() + data += await request.read() except (asyncio.TimeoutError, aiohttp.ClientError): _LOGGER.error("Timeout for google speech.") diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index f7bf9774e42..9ccf280ed04 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -72,8 +72,7 @@ def _load_uuid(hass, filename=UPDATER_UUID_FILE): return _create_uuid(hass, filename) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Set up the updater component.""" if 'dev' in current_version: # This component only makes sense in release versions @@ -81,16 +80,15 @@ def async_setup(hass, config): config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): - huuid = yield from hass.async_add_job(_load_uuid, hass) + huuid = await hass.async_add_job(_load_uuid, hass) else: huuid = None include_components = config.get(CONF_COMPONENT_REPORTING) - @asyncio.coroutine - def check_new_version(now): + async def check_new_version(now): """Check if a new version is available and report if one is.""" - result = yield from get_newest_version(hass, huuid, include_components) + result = await get_newest_version(hass, huuid, include_components) if result is None: return @@ -125,8 +123,7 @@ def async_setup(hass, config): return True -@asyncio.coroutine -def get_system_info(hass, include_components): +async def get_system_info(hass, include_components): """Return info about the system.""" info_object = { 'arch': platform.machine(), @@ -151,7 +148,7 @@ def get_system_info(hass, include_components): info_object['os_version'] = platform.release() elif platform.system() == 'Linux': import distro - linux_dist = yield from hass.async_add_job( + linux_dist = await hass.async_add_job( distro.linux_distribution, False) info_object['distribution'] = linux_dist[0] info_object['os_version'] = linux_dist[1] @@ -160,11 +157,10 @@ def get_system_info(hass, include_components): return info_object -@asyncio.coroutine -def get_newest_version(hass, huuid, include_components): +async def get_newest_version(hass, huuid, include_components): """Get the newest Home Assistant version.""" if huuid: - info_object = yield from get_system_info(hass, include_components) + info_object = await get_system_info(hass, include_components) info_object['huuid'] = huuid else: info_object = {} @@ -172,7 +168,7 @@ def get_newest_version(hass, huuid, include_components): session = async_get_clientsession(hass) try: with async_timeout.timeout(5, loop=hass.loop): - req = yield from session.post(UPDATER_URL, json=info_object) + req = await session.post(UPDATER_URL, json=info_object) _LOGGER.info(("Submitted analytics to Home Assistant servers. " "Information submitted includes %s"), info_object) except (asyncio.TimeoutError, aiohttp.ClientError): @@ -181,7 +177,7 @@ def get_newest_version(hass, huuid, include_components): return None try: - res = yield from req.json() + res = await req.json() except ValueError: _LOGGER.error("Received invalid JSON from Home Assistant Update") return None diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index f7306cae98b..10b43445184 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -49,17 +49,16 @@ class AsyncHandler(object): """Wrap close to handler.""" self.emit(None) - @asyncio.coroutine - def async_close(self, blocking=False): + async def async_close(self, blocking=False): """Close the handler. When blocking=True, will wait till closed. """ - yield from self._queue.put(None) + await self._queue.put(None) if blocking: while self._thread.is_alive(): - yield from asyncio.sleep(0, loop=self.loop) + await asyncio.sleep(0, loop=self.loop) def emit(self, record): """Process a record.""" diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c109ae30aad..3e4d4739779 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -40,9 +40,9 @@ def test_from_config_file(hass): assert components == hass.config.components -@asyncio.coroutine @patch('homeassistant.bootstrap.async_enable_logging', Mock()) @patch('homeassistant.bootstrap.async_register_signal_handling', Mock()) +@asyncio.coroutine def test_home_assistant_core_config_validation(hass): """Test if we pass in wrong information for HA conf.""" # Extensive HA conf validation testing is done From fd038b6de9299c063cd707952bd1a775c3d2ca44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Apr 2018 20:15:00 -0400 Subject: [PATCH 526/924] Disable eliqonline requirement (#14156) * Disable eliqonline requirement * Disable pylint import error --- homeassistant/components/sensor/eliqonline.py | 3 ++- requirements_all.txt | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 3e736ed719f..23c397053c5 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,7 +14,8 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['eliqonline==1.0.13'] +# pylint: disable=import-error, no-member +REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f0be99705c1..a3519f29283 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,9 +276,6 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 -# homeassistant.components.sensor.eliqonline -eliqonline==1.0.13 - # homeassistant.components.enocean enocean==0.40 From ef48a7ca2c2ce938cd5bf8669cdf02b3535d67d7 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 29 Apr 2018 00:46:36 -0700 Subject: [PATCH 527/924] Fix Python 3.6 compatibility for HomeKit controller (#14160) Python 3.6's http client passes an additional argument to _send_output, so add that to the function definition. --- homeassistant/components/homekit_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 164e7d50e4d..e36e7439e09 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN) _LOGGER = logging.getLogger(__name__) -def homekit_http_send(self, message_body=None): +def homekit_http_send(self, message_body=None, encode_chunked=False): r"""Send the currently buffered request and clear the buffer. Appends an extra \r\n to the buffer. From 3fd4987baf3a11b7caba7f97227ac9f96dd882a9 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Sun, 29 Apr 2018 16:16:20 +0200 Subject: [PATCH 528/924] deCONZ allow unloading of config entry (#14115) * Working but incomplete * Remove events on unload * Add unload test * Fix failing sensor test * Improve unload test * Move DeconzEvent to init * Fix visual under-indentation --- .../components/binary_sensor/__init__.py | 5 ++ homeassistant/components/deconz/__init__.py | 55 +++++++++++++++++-- homeassistant/components/deconz/const.py | 1 + homeassistant/components/scene/__init__.py | 5 ++ homeassistant/components/sensor/__init__.py | 5 ++ homeassistant/components/sensor/deconz.py | 29 +--------- tests/components/deconz/test_init.py | 16 ++++++ tests/components/sensor/test_deconz.py | 1 + 8 files changed, 86 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ee2a0ce712d..d72211d5ad1 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -62,6 +62,11 @@ async def async_setup_entry(hass, entry): return await hass.data[DOMAIN].async_setup_entry(entry) +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + # pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index d68edac9e59..75414598693 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -7,14 +7,17 @@ https://home-assistant.io/components/deconz/ import voluptuous as vol from homeassistant.const import ( - CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) -from homeassistant.core import callback + CONF_API_KEY, CONF_EVENT, CONF_HOST, + CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) +from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.util import slugify from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts -from .const import CONFIG_FILE, DATA_DECONZ_ID, DOMAIN, _LOGGER +from .const import ( + CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DOMAIN, _LOGGER) REQUIREMENTS = ['pydeconz==36'] @@ -26,6 +29,8 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +SERVICE_DECONZ = 'configure' + SERVICE_FIELD = 'field' SERVICE_ENTITY = 'entity' SERVICE_DATA = 'data' @@ -64,6 +69,7 @@ async def async_setup_entry(hass, config_entry): Start websocket for push notification of state changes from deCONZ. """ from pydeconz import DeconzSession + from pydeconz.sensor import SWITCH as DECONZ_REMOTE if DOMAIN in hass.data: _LOGGER.error( "Config entry failed since one deCONZ instance already exists") @@ -82,6 +88,11 @@ async def async_setup_entry(hass, config_entry): for component in ['binary_sensor', 'light', 'scene', 'sensor']: hass.async_add_job(hass.config_entries.async_forward_entry_setup( config_entry, component)) + + hass.data[DATA_DECONZ_EVENT] = [DeconzEvent( + hass, sensor) for sensor in deconz.sensors.values() + if sensor.type in DECONZ_REMOTE] + deconz.start() async def async_configure(call): @@ -112,7 +123,7 @@ async def async_setup_entry(hass, config_entry): return await deconz.async_put_state(field, data) hass.services.async_register( - DOMAIN, 'configure', async_configure, schema=SERVICE_SCHEMA) + DOMAIN, SERVICE_DECONZ, async_configure, schema=SERVICE_SCHEMA) @callback def deconz_shutdown(event): @@ -127,3 +138,39 @@ async def async_setup_entry(hass, config_entry): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, deconz_shutdown) return True + + +async def async_unload_entry(hass, config_entry): + """Unload deCONZ config entry.""" + deconz = hass.data.pop(DOMAIN) + hass.services.async_remove(DOMAIN, SERVICE_DECONZ) + deconz.close() + for component in ['binary_sensor', 'light', 'scene', 'sensor']: + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_ID] = [] + return True + + +class DeconzEvent(object): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, hass, device): + """Register callback that will be used for signals.""" + self._hass = hass + self._device = device + self._device.register_async_callback(self.async_update_callback) + self._event = 'deconz_{}'.format(CONF_EVENT) + self._id = slugify(self._device.name) + + @callback + def async_update_callback(self, reason): + """Fire the event if reason is that state is updated.""" + if reason['state']: + data = {CONF_ID: self._id, CONF_EVENT: self._device.state} + self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index c5820c971f6..e6d393c8ee7 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -5,4 +5,5 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz') DOMAIN = 'deconz' CONFIG_FILE = 'deconz.conf' +DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index a3e3a5b38a7..2394d538f2f 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -95,6 +95,11 @@ async def async_setup_entry(hass, entry): return await hass.data[DOMAIN].async_setup_entry(entry) +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) + + class Scene(Entity): """A scene is a group of entities and the states we want them to be.""" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2887d32b987..bed1850b34d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -41,3 +41,8 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Setup a config entry.""" return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index dc28a181aa0..69be7f52d6c 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,9 +6,8 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, CONF_EVENT, CONF_ID) -from homeassistant.core import EventOrigin, callback +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_VOLTAGE +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify @@ -35,7 +34,6 @@ async def async_setup_entry(hass, config_entry, async_add_devices): for sensor in sensors.values(): if sensor and sensor.type in DECONZ_SENSOR: if sensor.type in DECONZ_REMOTE: - DeconzEvent(hass, sensor) if sensor.battery: entities.append(DeconzBattery(sensor)) else: @@ -184,26 +182,3 @@ class DeconzBattery(Entity): ATTR_EVENT_ID: slugify(self._device.name), } return attr - - -class DeconzEvent(object): - """When you want signals instead of entities. - - Stateless sensors such as remotes are expected to generate an event - instead of a sensor entity in hass. - """ - - def __init__(self, hass, device): - """Register callback that will be used for signals.""" - self._hass = hass - self._device = device - self._device.register_async_callback(self.async_update_callback) - self._event = 'deconz_{}'.format(CONF_EVENT) - self._id = slugify(self._device.name) - - @callback - def async_update_callback(self, reason): - """Fire the event if reason is that state is updated.""" - if reason['state']: - data = {CONF_ID: self._id, CONF_EVENT: self._device.state} - self._hass.bus.async_fire(self._event, data, EventOrigin.remote) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index ce231e3d162..b09edf42a87 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -107,3 +107,19 @@ async def test_setup_entry_successful(hass): (entry, 'scene') assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ (entry, 'sensor') + + +async def test_unload_entry(hass): + """Test being able to unload an entry.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + assert deconz.DATA_DECONZ_EVENT in hass.data + hass.data[deconz.DATA_DECONZ_EVENT].append(Mock()) + hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} + assert await deconz.async_unload_entry(hass, entry) + assert deconz.DOMAIN not in hass.data + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index b70fb396686..d6c026e88bd 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -51,6 +51,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') From d352dee9b7d0b7b9c39d7d6d512e74a636753783 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 29 Apr 2018 16:21:46 +0200 Subject: [PATCH 529/924] Upgrade netdisco to 1.4.0 (#14152) --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index f0ebcba8366..69d0f4796ff 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.3.1'] +REQUIREMENTS = ['netdisco==1.4.0'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index a3519f29283..74835077054 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -550,7 +550,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.3.1 +netdisco==1.4.0 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 8e7f500f28332d218dcfa1b316279d5822127d95 Mon Sep 17 00:00:00 2001 From: escoand Date: Sun, 29 Apr 2018 17:50:49 +0200 Subject: [PATCH 530/924] Add precipitation to OpenWeatherMap forecast (#13971) * add initial precipitation support * move attr to component * remove blank line * add forecast attributes to platform and update demo * add tests * break long lines * calc lower temp correctly * move all new attributes to component * convert temp low only when existing --- homeassistant/components/weather/__init__.py | 7 +++++++ homeassistant/components/weather/buienradar.py | 6 ++---- homeassistant/components/weather/demo.py | 18 ++++++++++++++---- homeassistant/components/weather/ecobee.py | 5 ++--- .../components/weather/openweathermap.py | 5 +++-- homeassistant/components/weather/yweather.py | 5 ++--- tests/components/weather/test_weather.py | 12 +++++++++++- 7 files changed, 41 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index b200d634ba9..467a106a6a2 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -22,7 +22,10 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' ATTR_CONDITION_CLASS = 'condition_class' ATTR_FORECAST = 'forecast' +ATTR_FORECAST_CONDITION = 'condition' +ATTR_FORECAST_PRECIPITATION = 'precipitation' ATTR_FORECAST_TEMP = 'temperature' +ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TIME = 'datetime' ATTR_WEATHER_ATTRIBUTION = 'attribution' ATTR_WEATHER_HUMIDITY = 'humidity' @@ -144,6 +147,10 @@ class WeatherEntity(Entity): forecast_entry[ATTR_FORECAST_TEMP] = show_temp( self.hass, forecast_entry[ATTR_FORECAST_TEMP], self.temperature_unit, self.precision) + if ATTR_FORECAST_TEMP_LOW in forecast_entry: + forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( + self.hass, forecast_entry[ATTR_FORECAST_TEMP_LOW], + self.temperature_unit, self.precision) forecast.append(forecast_entry) data[ATTR_FORECAST] = forecast diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index bf1864a9c0f..9b9707e87f6 100644 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -10,7 +10,8 @@ import asyncio import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import \ CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv @@ -28,9 +29,6 @@ DEFAULT_TIMEFRAME = 60 CONF_FORECAST = 'forecast' -ATTR_FORECAST_CONDITION = 'condition' -ATTR_FORECAST_TEMP_LOW = 'templow' - CONDITION_CLASSES = { 'cloudy': ['c', 'p'], diff --git a/homeassistant/components/weather/demo.py b/homeassistant/components/weather/demo.py index 02e07996213..fffdf03d07d 100644 --- a/homeassistant/components/weather/demo.py +++ b/homeassistant/components/weather/demo.py @@ -7,7 +7,8 @@ https://home-assistant.io/components/demo/ from datetime import datetime, timedelta from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT) CONDITION_CLASSES = { @@ -32,9 +33,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo weather.""" add_devices([ DemoWeather('South', 'Sunshine', 21.6414, 92, 1099, 0.5, TEMP_CELSIUS, - [22, 19, 15, 12, 14, 18, 21]), + [['rainy', 1, 22, 15], ['rainy', 5, 19, 8], + ['cloudy', 0, 15, 9], ['sunny', 0, 12, 6], + ['partlycloudy', 2, 14, 7], ['rainy', 15, 18, 7], + ['fog', 0.2, 21, 12]]), DemoWeather('North', 'Shower rain', -12, 54, 987, 4.8, TEMP_FAHRENHEIT, - [-10, -13, -18, -23, -19, -14, -9]) + [['snowy', 2, -10, -15], ['partlycloudy', 1, -13, -14], + ['sunny', 0, -18, -22], ['sunny', 0.1, -23, -23], + ['snowy', 4, -19, -20], ['sunny', 0.3, -14, -19], + ['sunny', 0, -9, -12]]) ]) @@ -108,7 +115,10 @@ class DemoWeather(WeatherEntity): for entry in self._forecast: data_dict = { ATTR_FORECAST_TIME: reftime.isoformat(), - ATTR_FORECAST_TEMP: entry + ATTR_FORECAST_CONDITION: entry[0], + ATTR_FORECAST_PRECIPITATION: entry[1], + ATTR_FORECAST_TEMP: entry[2], + ATTR_FORECAST_TEMP_LOW: entry[3] } reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) diff --git a/homeassistant/components/weather/ecobee.py b/homeassistant/components/weather/ecobee.py index 379f5c1211b..80ee4c29fbe 100644 --- a/homeassistant/components/weather/ecobee.py +++ b/homeassistant/components/weather/ecobee.py @@ -6,14 +6,13 @@ https://home-assistant.io/components/weather.ecobee/ """ from homeassistant.components import ecobee from homeassistant.components.weather import ( - WeatherEntity, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) + WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) from homeassistant.const import (TEMP_FAHRENHEIT) DEPENDENCIES = ['ecobee'] -ATTR_FORECAST_CONDITION = 'condition' -ATTR_FORECAST_TEMP_LOW = 'templow' ATTR_FORECAST_TEMP_HIGH = 'temphigh' ATTR_FORECAST_PRESSURE = 'pressure' ATTR_FORECAST_VISIBILITY = 'visibility' diff --git a/homeassistant/components/weather/openweathermap.py b/homeassistant/components/weather/openweathermap.py index a8e26d39cb3..909f123b52c 100644 --- a/homeassistant/components/weather/openweathermap.py +++ b/homeassistant/components/weather/openweathermap.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS) @@ -21,7 +22,6 @@ REQUIREMENTS = ['pyowm==2.8.0'] _LOGGER = logging.getLogger(__name__) -ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = 'Data provided by OpenWeatherMap' DEFAULT_NAME = 'OpenWeatherMap' @@ -144,6 +144,7 @@ class OpenWeatherMapWeather(WeatherEntity): ATTR_FORECAST_TIME: entry.get_reference_time('unix') * 1000, ATTR_FORECAST_TEMP: entry.get_temperature('celsius').get('temp'), + ATTR_FORECAST_PRECIPITATION: entry.get_rain().get('3h'), ATTR_FORECAST_CONDITION: [k for k, v in CONDITION_CLASSES.items() if entry.get_weather_code() in v][0] diff --git a/homeassistant/components/weather/yweather.py b/homeassistant/components/weather/yweather.py index 5987cf7621f..f9befece5a4 100644 --- a/homeassistant/components/weather/yweather.py +++ b/homeassistant/components/weather/yweather.py @@ -10,7 +10,8 @@ import logging import voluptuous as vol from homeassistant.components.weather import ( - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) + ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv @@ -20,10 +21,8 @@ _LOGGER = logging.getLogger(__name__) DATA_CONDITION = 'yahoo_condition' -ATTR_FORECAST_CONDITION = 'condition' ATTRIBUTION = "Weather details provided by Yahoo! Inc." -ATTR_FORECAST_TEMP_LOW = 'templow' CONF_WOEID = 'woeid' diff --git a/tests/components/weather/test_weather.py b/tests/components/weather/test_weather.py index 9d22b1ad0ae..a88e9979551 100644 --- a/tests/components/weather/test_weather.py +++ b/tests/components/weather/test_weather.py @@ -5,7 +5,8 @@ from homeassistant.components import weather from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_TEMP) + ATTR_WEATHER_WIND_SPEED, ATTR_FORECAST, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW) from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component @@ -45,8 +46,17 @@ class TestWeather(unittest.TestCase): assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_WEATHER_ATTRIBUTION) == \ 'Powered by Home Assistant' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_CONDITION) == \ + 'rainy' + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_PRECIPITATION) == 1 assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP) == 22 + assert data.get(ATTR_FORECAST)[0].get(ATTR_FORECAST_TEMP_LOW) == 15 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_CONDITION) == \ + 'fog' + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION) \ + == 0.2 assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP) == 21 + assert data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_TEMP_LOW) == 12 assert len(data.get(ATTR_FORECAST)) == 7 def test_temperature_convert(self): From 113bdc493a89314b0263c67dcfb1ba07942f4935 Mon Sep 17 00:00:00 2001 From: Hate-Usernames Date: Sun, 29 Apr 2018 16:54:44 +0100 Subject: [PATCH 531/924] Allow transitioning to colour temp for tradfri (#14157) --- homeassistant/components/light/tradfri.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/light/tradfri.py b/homeassistant/components/light/tradfri.py index 95082bb4d19..ab53c3669cb 100644 --- a/homeassistant/components/light/tradfri.py +++ b/homeassistant/components/light/tradfri.py @@ -253,6 +253,8 @@ class TradfriLight(Light): params[ATTR_BRIGHTNESS] = brightness hue = int(kwargs[ATTR_HS_COLOR][0] * (65535 / 360)) sat = int(kwargs[ATTR_HS_COLOR][1] * (65279 / 100)) + if brightness is None: + params[ATTR_TRANSITION_TIME] = transition_time await self._api( self._light_control.set_hsb(hue, sat, **params)) return From 74320306a1195969c21dcecd2bd78cdde51f7398 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Apr 2018 10:22:28 -0400 Subject: [PATCH 532/924] Add mitemp_bt to coverage --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index c1c879aef09..e7aa9a2b4d2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/sensor/lyft.py homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py + homeassistant/components/sensor/mitemp_bt.py homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/mopar.py homeassistant/components/sensor/mqtt_room.py From 4c0024fd972ace494fe365cff57e887a66d445af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 29 Apr 2018 14:15:39 -0400 Subject: [PATCH 533/924] Another coverage fix --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index e7aa9a2b4d2..1852d7d7365 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,7 +29,7 @@ omit = homeassistant/components/arduino.py homeassistant/components/*/arduino.py - homeassistant/components/bmw_connected_drive.py + homeassistant/components/bmw_connected_drive/*.py homeassistant/components/*/bmw_connected_drive.py homeassistant/components/android_ip_webcam.py From aa8bd3714388478273709a7c9b7f2ec291054f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20=C3=98stergaard=20Nielsen?= Date: Sun, 29 Apr 2018 20:57:57 +0200 Subject: [PATCH 534/924] Added update_interval to maxcube (#14143) --- homeassistant/components/maxcube.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/maxcube.py b/homeassistant/components/maxcube.py index cf5091fc308..bca7a1b4ab7 100644 --- a/homeassistant/components/maxcube.py +++ b/homeassistant/components/maxcube.py @@ -13,7 +13,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SCAN_INTERVAL REQUIREMENTS = ['maxcube-api==0.1.0'] @@ -32,6 +32,7 @@ CONF_GATEWAYS = 'gateways' CONFIG_GATEWAY = vol.Schema({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SCAN_INTERVAL, default=300): cv.time_period, }) CONFIG_SCHEMA = vol.Schema({ @@ -54,10 +55,11 @@ def setup(hass, config): for gateway in gateways: host = gateway[CONF_HOST] port = gateway[CONF_PORT] + scan_interval = gateway[CONF_SCAN_INTERVAL].total_seconds() try: cube = MaxCube(MaxCubeConnection(host, port)) - hass.data[DATA_KEY][host] = MaxCubeHandle(cube) + hass.data[DATA_KEY][host] = MaxCubeHandle(cube, scan_interval) except timeout as ex: _LOGGER.error("Unable to connect to Max!Cube gateway: %s", str(ex)) hass.components.persistent_notification.create( @@ -80,9 +82,10 @@ def setup(hass, config): class MaxCubeHandle(object): """Keep the cube instance in one place and centralize the update.""" - def __init__(self, cube): + def __init__(self, cube, scan_interval): """Initialize the Cube Handle.""" self.cube = cube + self.scan_interval = scan_interval self.mutex = Lock() self._updatets = time.time() @@ -90,8 +93,8 @@ class MaxCubeHandle(object): """Pull the latest data from the MAX! Cube.""" # Acquire mutex to prevent simultaneous update from multiple threads with self.mutex: - # Only update every 60s - if (time.time() - self._updatets) >= 60: + # Only update every update_interval + if (time.time() - self._updatets) >= self.scan_interval: _LOGGER.debug("Updating") try: From 30d987f59ff8f321b7c9f1d9baa1fc3870b15d79 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 30 Apr 2018 00:49:19 +0200 Subject: [PATCH 535/924] Revert Hue color state to be xy-based (#14154) --- homeassistant/components/light/hue.py | 5 +---- tests/components/light/test_hue.py | 13 ------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6b4908b02d4..9f662718514 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -245,12 +245,9 @@ class HueLight(Light): mode = self._color_mode source = self.light.action if self.is_group else self.light.state - if mode == 'xy' and 'xy' in source: + if mode in ('xy', 'hs'): return color.color_xy_to_hs(*source['xy']) - if mode == 'hs' and 'hue' in source and 'sat' in source: - return source['hue'] / 65535 * 360, source['sat'] / 255 * 100 - return None @property diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index d36548e1e91..8f5b52ea6de 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -650,19 +650,6 @@ def test_hs_color(): assert light.hs_color is None - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'hs', - 'hue': 1234, - 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', From 02a12a0bb4c87ee2406983a2459d9a98947b6f9d Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sun, 29 Apr 2018 23:31:27 -0700 Subject: [PATCH 536/924] zha: Support remotes/buttons (#12528) --- homeassistant/components/binary_sensor/zha.py | 152 +++++++++++++++++- homeassistant/components/zha/__init__.py | 97 +++++++---- homeassistant/components/zha/const.py | 19 ++- 3 files changed, 232 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index bf038a62465..e1e6689d1eb 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -31,12 +31,21 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return + from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasZone + if IasZone.cluster_id in discovery_info['in_clusters']: + await _async_setup_iaszone(hass, config, async_add_devices, + discovery_info) + elif OnOff.cluster_id in discovery_info['out_clusters']: + await _async_setup_remote(hass, config, async_add_devices, + discovery_info) - in_clusters = discovery_info['in_clusters'] +async def _async_setup_iaszone(hass, config, async_add_devices, + discovery_info): device_class = None - cluster = in_clusters[IasZone.cluster_id] + from zigpy.zcl.clusters.security import IasZone + cluster = discovery_info['in_clusters'][IasZone.cluster_id] if discovery_info['new_join']: await cluster.bind() ieee = cluster.endpoint.device.application.ieee @@ -53,8 +62,34 @@ async def async_setup_platform(hass, config, async_add_devices, async_add_devices([sensor], update_before_add=True) +async def _async_setup_remote(hass, config, async_add_devices, discovery_info): + + async def safe(coro): + """Run coro, catching ZigBee delivery errors, and ignoring them.""" + import zigpy.exceptions + try: + await coro + except zigpy.exceptions.DeliveryError as exc: + _LOGGER.warning("Ignoring error during setup: %s", exc) + + if discovery_info['new_join']: + from zigpy.zcl.clusters.general import OnOff, LevelControl + out_clusters = discovery_info['out_clusters'] + if OnOff.cluster_id in out_clusters: + cluster = out_clusters[OnOff.cluster_id] + await safe(cluster.bind()) + await safe(cluster.configure_reporting(0, 0, 600, 1)) + if LevelControl.cluster_id in out_clusters: + cluster = out_clusters[LevelControl.cluster_id] + await safe(cluster.bind()) + await safe(cluster.configure_reporting(0, 1, 600, 1)) + + sensor = Switch(**discovery_info) + async_add_devices([sensor], update_before_add=True) + + class BinarySensor(zha.Entity, BinarySensorDevice): - """THe ZHA Binary Sensor.""" + """The ZHA Binary Sensor.""" _domain = DOMAIN @@ -102,3 +137,114 @@ class BinarySensor(zha.Entity, BinarySensorDevice): state = result.get('zone_status', self._state) if isinstance(state, (int, uint16_t)): self._state = result.get('zone_status', self._state) & 3 + + +class Switch(zha.Entity, BinarySensorDevice): + """ZHA switch/remote controller/button.""" + + _domain = DOMAIN + + class OnOffListener: + """Listener for the OnOff ZigBee cluster.""" + + def __init__(self, entity): + """Initialize OnOffListener.""" + self._entity = entity + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id in (0x0000, 0x0040): + self._entity.set_state(False) + elif command_id in (0x0001, 0x0041, 0x0042): + self._entity.set_state(True) + elif command_id == 0x0002: + self._entity.set_state(not self._entity.is_on) + + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 0: + self._entity.set_state(value) + self._entity.schedule_update_ha_state() + + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + class LevelListener: + """Listener for the LevelControl ZigBee cluster.""" + + def __init__(self, entity): + """Initialize LevelListener.""" + self._entity = entity + + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + if command_id in (0x0000, 0x0004): # move_to_level, -with_on_off + self._entity.set_level(args[0]) + elif command_id in (0x0001, 0x0005): # move, -with_on_off + # We should dim slowly -- for now, just step once + rate = args[1] + if args[0] == 0xff: + rate = 10 # Should read default move rate + self._entity.move_level(-rate if args[0] else rate) + elif command_id == 0x0002: # step + # Step (technically shouldn't change on/off) + self._entity.move_level(-args[1] if args[0] else args[1]) + + def attribute_update(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == 0: + self._entity.set_level(value) + + def zdo_command(self, *args, **kwargs): + """Handle ZDO commands on this cluster.""" + pass + + def __init__(self, **kwargs): + """Initialize Switch.""" + self._state = True + self._level = 255 + from zigpy.zcl.clusters import general + self._out_listeners = { + general.OnOff.cluster_id: self.OnOffListener(self), + general.LevelControl.cluster_id: self.LevelListener(self), + } + super().__init__(**kwargs) + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return {'level': self._state and self._level or 0} + + def move_level(self, change): + """Increment the level, setting state if appropriate.""" + if not self._state and change > 0: + self._level = 0 + self._level = min(255, max(0, self._level + change)) + self._state = bool(self._level) + self.schedule_update_ha_state() + + def set_level(self, level): + """Set the level, setting state if appropriate.""" + self._level = level + self._state = bool(self._level) + self.schedule_update_ha_state() + + def set_state(self, state): + """Set the state.""" + self._state = state + if self._level == 0: + self._level = 255 + self.schedule_update_ha_state() + + async def async_update(self): + """Retrieve latest state.""" + from zigpy.zcl.clusters.general import OnOff + result = await zha.safe_read( + self._endpoint.out_clusters[OnOff.cluster_id], ['on_off']) + self._state = result.get('on_off', self._state) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 73c1fdf9075..dc9cb26462d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -221,44 +221,78 @@ class ApplicationListener: self._config, ) - for cluster_id, cluster in endpoint.in_clusters.items(): - cluster_type = type(cluster) - if cluster_id in profile_clusters[0]: - continue - if cluster_type not in zha_const.SINGLE_CLUSTER_DEVICE_CLASS: - continue + for cluster in endpoint.in_clusters.values(): + await self._attempt_single_cluster_device( + endpoint, + cluster, + profile_clusters[0], + device_key, + zha_const.SINGLE_INPUT_CLUSTER_DEVICE_CLASS, + 'in_clusters', + discovered_info, + join, + ) - component = zha_const.SINGLE_CLUSTER_DEVICE_CLASS[cluster_type] - cluster_key = "{}-{}".format(device_key, cluster_id) - discovery_info = { - 'application_listener': self, - 'endpoint': endpoint, - 'in_clusters': {cluster.cluster_id: cluster}, - 'out_clusters': {}, - 'new_join': join, - 'unique_id': cluster_key, - 'entity_suffix': '_{}'.format(cluster_id), - } - discovery_info.update(discovered_info) - self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info - - await discovery.async_load_platform( - self._hass, - component, - DOMAIN, - {'discovery_key': cluster_key}, - self._config, + for cluster in endpoint.out_clusters.values(): + await self._attempt_single_cluster_device( + endpoint, + cluster, + profile_clusters[1], + device_key, + zha_const.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + 'out_clusters', + discovered_info, + join, ) def register_entity(self, ieee, entity_obj): """Record the creation of a hass entity associated with ieee.""" self._device_registry[ieee].append(entity_obj) + async def _attempt_single_cluster_device(self, endpoint, cluster, + profile_clusters, device_key, + device_classes, discovery_attr, + entity_info, is_new_join): + """Try to set up an entity from a "bare" cluster.""" + if cluster.cluster_id in profile_clusters: + return + # pylint: disable=unidiomatic-typecheck + if type(cluster) not in device_classes: + return + + component = device_classes[type(cluster)] + cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + discovery_info = { + 'application_listener': self, + 'endpoint': endpoint, + 'in_clusters': {}, + 'out_clusters': {}, + 'new_join': is_new_join, + 'unique_id': cluster_key, + 'entity_suffix': '_{}'.format(cluster.cluster_id), + } + discovery_info[discovery_attr] = {cluster.cluster_id: cluster} + discovery_info.update(entity_info) + self._hass.data[DISCOVERY_KEY][cluster_key] = discovery_info + + await discovery.async_load_platform( + self._hass, + component, + DOMAIN, + {'discovery_key': cluster_key}, + self._config, + ) + class Entity(entity.Entity): """A base class for ZHA entities.""" _domain = None # Must be overridden by subclasses + # Normally the entity itself is the listener. Base classes may set this to + # a dict of cluster ID -> listener to receive messages for specific + # clusters separately + _in_listeners = {} + _out_listeners = {} def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, model, application_listener, unique_id, **kwargs): @@ -287,10 +321,11 @@ class Entity(entity.Entity): kwargs.get('entity_suffix', ''), ) - for cluster in in_clusters.values(): - cluster.add_listener(self) - for cluster in out_clusters.values(): - cluster.add_listener(self) + for cluster_id, cluster in in_clusters.items(): + cluster.add_listener(self._in_listeners.get(cluster_id, self)) + for cluster_id, cluster in out_clusters.items(): + cluster.add_listener(self._out_listeners.get(cluster_id, self)) + self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters @@ -379,7 +414,7 @@ async def safe_read(cluster, attributes): try: result, _ = await cluster.read_attributes( attributes, - allow_cache=False, + allow_cache=True, ) return result except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 4fe3581d5b2..36eb4d55c97 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -1,7 +1,8 @@ """All constants related to the ZHA component.""" DEVICE_CLASS = {} -SINGLE_CLUSTER_DEVICE_CLASS = {} +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} +SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} COMPONENT_CLUSTERS = {} @@ -15,11 +16,17 @@ def populate_data(): from zigpy.profiles import PROFILES, zha, zll DEVICE_CLASS[zha.PROFILE_ID] = { + zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor', + zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor', + zha.DeviceType.REMOTE_CONTROL: 'binary_sensor', zha.DeviceType.SMART_PLUG: 'switch', zha.DeviceType.ON_OFF_LIGHT: 'light', zha.DeviceType.DIMMABLE_LIGHT: 'light', zha.DeviceType.COLOR_DIMMABLE_LIGHT: 'light', + zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor', + zha.DeviceType.DIMMER_SWITCH: 'binary_sensor', + zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor', } DEVICE_CLASS[zll.PROFILE_ID] = { zll.DeviceType.ON_OFF_LIGHT: 'light', @@ -29,15 +36,23 @@ def populate_data(): zll.DeviceType.COLOR_LIGHT: 'light', zll.DeviceType.EXTENDED_COLOR_LIGHT: 'light', zll.DeviceType.COLOR_TEMPERATURE_LIGHT: 'light', + zll.DeviceType.COLOR_CONTROLLER: 'binary_sensor', + zll.DeviceType.COLOR_SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.CONTROLLER: 'binary_sensor', + zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor', + zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor', } - SINGLE_CLUSTER_DEVICE_CLASS.update({ + SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({ zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) + SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ + zcl.clusters.general.OnOff: 'binary_sensor', + }) # A map of hass components to all Zigbee clusters it could use for profile_id, classes in DEVICE_CLASS.items(): From d7eced95fa59da1a1efcd767f87acd9f8e650404 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 30 Apr 2018 09:28:00 +0200 Subject: [PATCH 537/924] Upgrade numpy to 1.14.3 (#14187) --- homeassistant/components/binary_sensor/trend.py | 2 +- homeassistant/components/image_processing/opencv.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index 9b4598f3c42..5405a6a77ba 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -23,7 +23,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.util import utcnow -REQUIREMENTS = ['numpy==1.14.2'] +REQUIREMENTS = ['numpy==1.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/image_processing/opencv.py b/homeassistant/components/image_processing/opencv.py index 18e74966a59..c3e34b4d42b 100644 --- a/homeassistant/components/image_processing/opencv.py +++ b/homeassistant/components/image_processing/opencv.py @@ -16,7 +16,7 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['numpy==1.14.2'] +REQUIREMENTS = ['numpy==1.14.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 74835077054..796f80c3bd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -563,7 +563,7 @@ nuheat==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.2 +numpy==1.14.3 # homeassistant.components.google oauth2client==4.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d6fe9e2f63..28265bdb5f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -99,7 +99,7 @@ mficlient==0.3.0 # homeassistant.components.binary_sensor.trend # homeassistant.components.image_processing.opencv -numpy==1.14.2 +numpy==1.14.3 # homeassistant.components.mqtt # homeassistant.components.shiftr From 76c9c0179b5a0ffae6637d10bc58a974519053b4 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 30 Apr 2018 14:46:44 +0200 Subject: [PATCH 538/924] Improve chromecast disconnection logic (#14190) * Attempt Cast Fix * Cleanup --- homeassistant/components/media_player/cast.py | 26 +++++++++++++------ tests/components/media_player/test_cast.py | 16 +++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 632ab4214b8..a9bea9e4c1d 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -306,13 +306,18 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) self.hass.async_add_job(self.async_set_cast_info(discover)) + async def async_stop(event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" - self._async_disconnect() + await self._async_disconnect() if self._cast_info.uuid is not None: # Remove the entity from the added casts so that it can dynamically # be re-added again. @@ -328,7 +333,7 @@ class CastDevice(MediaPlayerDevice): if old_cast_info.host_port == cast_info.host_port: # Nothing connection-related updated return - self._async_disconnect() + await self._async_disconnect() # Failed connection will unfortunately never raise an exception, it # will instead just try connecting indefinitely. @@ -348,22 +353,27 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Connection successful!") self.async_schedule_update_ha_state() - @callback - def _async_disconnect(self): + async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return - _LOGGER.debug("Disconnecting from previous chromecast socket.") + _LOGGER.debug("Disconnecting from chromecast socket.") self._available = False - self._chromecast.disconnect(blocking=False) + self.async_schedule_update_ha_state() + + await self.hass.async_add_job(self._chromecast.disconnect) + # Invalidate some attributes self._chromecast = None self.cast_status = None self.media_status = None self.media_status_received = None - self._status_listener.invalidate() - self._status_listener = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + self.async_schedule_update_ha_state() # ========== Callbacks ========== def new_cast_status(self, cast_status): diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index ee69ec1c85d..41cf6749b71 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -346,8 +346,16 @@ async def test_switched_host(hass: HomeAssistantType): async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) await hass.async_block_till_done() assert get_chromecast.call_count == 1 - chromecast.disconnect.assert_called_once_with(blocking=False) + assert chromecast.disconnect.call_count == 1 - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - chromecast.disconnect.assert_called_once_with(blocking=False) + +async def test_disconnect_on_stop(hass: HomeAssistantType): + """Test cast device disconnects socket on stop.""" + info = get_fake_chromecast_info() + + with patch('pychromecast.dial.get_device_status', return_value=info): + chromecast, _ = await async_setup_media_player_cast(hass, info) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert chromecast.disconnect.call_count == 1 From 46c260fd8512d7c34fe4c2482cd4aa49d369061b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 30 Apr 2018 14:58:17 +0200 Subject: [PATCH 539/924] Added CONF_IP_ADDRESS to HomeKit (#14163) --- homeassistant/components/homekit/__init__.py | 16 +++++++---- tests/components/homekit/test_homekit.py | 28 +++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 025ef4069e9..4984cfee959 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,6 +3,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ +import ipaddress import logging from zlib import adler32 @@ -12,8 +13,8 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -35,6 +36,8 @@ REQUIREMENTS = ['HAP-python==1.1.9'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): + vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, @@ -48,11 +51,12 @@ async def async_setup(hass, config): conf = config[DOMAIN] port = conf[CONF_PORT] + ip_address = conf.get(CONF_IP_ADDRESS) auto_start = conf[CONF_AUTO_START] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) homekit.setup() if auto_start: @@ -151,10 +155,11 @@ def generate_aid(entity_id): class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port, entity_filter, entity_config): + def __init__(self, hass, port, ip_address, entity_filter, entity_config): """Initialize a HomeKit object.""" self.hass = hass self._port = port + self._ip_address = ip_address self._filter = entity_filter self._config = entity_config self.started = False @@ -169,9 +174,10 @@ class HomeKit(): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self.stop) + ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index d1ad232d279..7ae37becbd5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -11,7 +11,8 @@ from homeassistant.components.homekit.const import ( DEFAULT_PORT, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( - CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_IP_ADDRESS, CONF_PORT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce @@ -59,7 +60,7 @@ class TestHomeKit(unittest.TestCase): self.hass, DOMAIN, {DOMAIN: {}})) self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, ANY, {}), + call(self.hass, DEFAULT_PORT, None, ANY, {}), call().setup()]) # Test auto start enabled @@ -74,7 +75,8 @@ class TestHomeKit(unittest.TestCase): """Test async_setup with auto start disabled and test service calls.""" mock_homekit.return_value = homekit = Mock() - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) @@ -82,7 +84,7 @@ class TestHomeKit(unittest.TestCase): self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, ANY, {}), + call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) # Test start call with driver stopped. @@ -101,7 +103,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_setup(self): """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: @@ -117,9 +119,17 @@ class TestHomeKit(unittest.TestCase): self.assertEqual( self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + def test_homekit_setup_ip_address(self): + """Test setup with given IP address.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + homekit.setup() + mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) + def test_homekit_add_accessory(self): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) homekit.bridge = HomeBridge(self.hass) with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ @@ -142,7 +152,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_entity_filter(self): """Test the entity filter.""" entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, entity_filter, {}) + homekit = HomeKit(self.hass, None, None, entity_filter, {}) with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.return_value = None @@ -162,7 +172,7 @@ class TestHomeKit(unittest.TestCase): @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) homekit.bridge = HomeBridge(self.hass) homekit.driver = Mock() @@ -184,7 +194,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None) + homekit = HomeKit(None, None, None, None, None) homekit.driver = Mock() # Test if started = False From 5dcad89a0d58ed40993b6b8d27a5d5d2daee63b7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 09:18:18 -0400 Subject: [PATCH 540/924] Do not sync entities with an empty name (#14181) --- .../components/google_assistant/smart_home.py | 11 +++++--- .../google_assistant/test_smart_home.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 7e746d48bed..27d993aee76 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -102,18 +102,23 @@ class _GoogleEntity: if state.state == STATE_UNAVAILABLE: return None + entity_config = self.config.entity_config.get(state.entity_id, {}) + name = (entity_config.get(CONF_NAME) or state.name).strip() + + # If an empty string + if not name: + return None + traits = self.traits() # Found no supported traits for this entity if not traits: return None - entity_config = self.config.entity_config.get(state.entity_id, {}) - device = { 'id': state.entity_id, 'name': { - 'name': entity_config.get(CONF_NAME) or state.name + 'name': name }, 'attributes': {}, 'traits': [trait.name for trait in traits], diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index e284b026ad8..cdaf4200c97 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -286,3 +286,29 @@ async def test_unavailable_state_doesnt_sync(hass): 'devices': [] } } + + +async def test_empty_name_doesnt_sync(hass): + """Test that an entity with empty name does not sync over.""" + light = DemoLight( + None, ' ', + state=False, + ) + light.hass = hass + light.entity_id = 'light.demo_light' + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From 853a16938b98ea94463f1c5866da38b2d0832a18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 09:56:42 -0400 Subject: [PATCH 541/924] Fix poorly formatted automations (#14196) --- homeassistant/components/config/automation.py | 8 ++- tests/components/config/test_automation.py | 67 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 1e260854687..223159eb415 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,7 @@ """Provide configuration end points for Automations.""" import asyncio from collections import OrderedDict +import uuid from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView @@ -29,7 +30,12 @@ class EditAutomationConfigView(EditIdBasedConfigView): """Set value.""" index = None for index, cur_value in enumerate(data): - if cur_value[CONF_ID] == config_key: + # When people copy paste their automations to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: break else: cur_value = OrderedDict() diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 327283e74aa..2c888dd2dd2 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -42,13 +42,13 @@ async def test_update_device_config(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) orig_data = [ - { - 'id': 'sun', - }, - { - 'id': 'moon', - } - ] + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] def mock_read(path): """Mock reading data.""" @@ -81,3 +81,56 @@ async def test_update_device_config(hass, aiohttp_client): 'action': [], } assert written[0] == orig_data + + +async def test_bad_formatted_automations(hass, aiohttp_client): + """Test that we handle automations without ID.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + # No ID + 'action': { + 'event': 'hello' + } + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + # Verify ID added to orig_data + assert 'id' in orig_data[0] + + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } From eceece866d5bde62d44eb8d214be820d11e71959 Mon Sep 17 00:00:00 2001 From: Mahesh Subramaniya Date: Mon, 30 Apr 2018 10:48:51 -0500 Subject: [PATCH 542/924] Updating darksky default update interval to 5 mins (#14195) With Darksky allowing only 1000 API requests per day, 2 minutes retry seems to be bit closer to running over the limit and actually it did for 5 days in my account. Hence proposing a change to 5 minutes to keep the API happy and also it doesn't hurt to check the weather for every 5 mins than 2 mins someone lives in Jupiter :-P --- homeassistant/components/sensor/darksky.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index 7d535c5f1d9..ac09de9c699 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -146,7 +146,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 'Latitude and longitude must exist together'): cv.latitude, vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, - vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=120)): ( + vol.Optional(CONF_UPDATE_INTERVAL, default=timedelta(seconds=300)): ( vol.All(cv.time_period, cv.positive_timedelta)), vol.Optional(CONF_FORECAST): vol.All(cv.ensure_list, [vol.Range(min=1, max=7)]), From 6e0a3abf66d89833394d55b2ac25af4068ab2a01 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Mon, 30 Apr 2018 19:27:45 +0200 Subject: [PATCH 543/924] Fix TypeError on round(self.humidity) (fixes #13116) (#14174) * Fix TypeError on round(self.humidity) Some weather platforms postpone the first data fetch for a while on init. As a result round(self.humidity is called before it is assigned a value, producing an error. This is a fix for that. * Rewrite to avoid false negative evaluation As per the suggestion from @OttoWinter, rewrite to avoid matching e.g. 0.0 as false. --- homeassistant/components/weather/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 467a106a6a2..c36c960c4fc 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -113,9 +113,12 @@ class WeatherEntity(Entity): ATTR_WEATHER_TEMPERATURE: show_temp( self.hass, self.temperature, self.temperature_unit, self.precision), - ATTR_WEATHER_HUMIDITY: round(self.humidity) } + humidity = self.humidity + if humidity is not None: + data[ATTR_WEATHER_HUMIDITY] = round(humidity) + ozone = self.ozone if ozone is not None: data[ATTR_WEATHER_OZONE] = ozone From a06f61034cb3a8debbc4da9817a6f7d1f87ef582 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 28 Apr 2018 23:12:11 +0200 Subject: [PATCH 544/924] Fix color setting of tplink lights (#14108) --- homeassistant/components/light/tplink.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/tplink.py b/homeassistant/components/light/tplink.py index 0bbec010282..4101eab2150 100644 --- a/homeassistant/components/light/tplink.py +++ b/homeassistant/components/light/tplink.py @@ -11,8 +11,8 @@ import voluptuous as vol from homeassistant.const import (CONF_HOST, CONF_NAME) from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_KELVIN, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) + Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, SUPPORT_COLOR, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin @@ -90,15 +90,15 @@ class TPLinkSmartBulb(Light): if ATTR_COLOR_TEMP in kwargs: self.smartbulb.color_temp = \ mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - if ATTR_KELVIN in kwargs: - self.smartbulb.color_temp = kwargs[ATTR_KELVIN] - if ATTR_BRIGHTNESS in kwargs: - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - self.smartbulb.brightness = brightness_to_percentage(brightness) + + brightness = brightness_to_percentage( + kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255)) if ATTR_HS_COLOR in kwargs: hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (hue, sat, 100) + hsv = (int(hue), int(sat), brightness) self.smartbulb.hsv = hsv + elif ATTR_BRIGHTNESS in kwargs: + self.smartbulb.brightness = brightness def turn_off(self, **kwargs): """Turn the light off.""" From 52a48b3ac9cb3584f0e3b97854381b0d8356fe50 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 27 Apr 2018 13:18:58 +0200 Subject: [PATCH 545/924] Improve precision of Hue color state (#14113) --- homeassistant/components/light/hue.py | 20 +++++--------------- tests/components/light/test_hue.py | 17 ++--------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6eb8de99c99..6b4908b02d4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -242,26 +242,16 @@ class HueLight(Light): @property def hs_color(self): """Return the hs color value.""" - # pylint: disable=redefined-outer-name mode = self._color_mode - - if mode not in ('hs', 'xy'): - return - source = self.light.action if self.is_group else self.light.state - hue = source.get('hue') - sat = source.get('sat') + if mode == 'xy' and 'xy' in source: + return color.color_xy_to_hs(*source['xy']) - # Sometimes the state will not include valid hue/sat values. - # Reported as issue 13434 - if hue is not None and sat is not None: - return hue / 65535 * 360, sat / 255 * 100 + if mode == 'hs' and 'hue' in source and 'sat' in source: + return source['hue'] / 65535 * 360, source['sat'] / 255 * 100 - if 'xy' not in source: - return None - - return color.color_xy_to_hs(*source['xy']) + return None @property def color_temp(self): diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 712cd17a7c7..d36548e1e91 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -237,7 +237,7 @@ async def test_lights(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) lamp_2 = hass.states.get('light.hue_lamp_2') assert lamp_2 is not None @@ -253,7 +253,7 @@ async def test_lights_color_mode(hass, mock_bridge): assert lamp_1 is not None assert lamp_1.state == 'on' assert lamp_1.attributes['brightness'] == 144 - assert lamp_1.attributes['hs_color'] == (71.896, 83.137) + assert lamp_1.attributes['hs_color'] == (36.067, 69.804) assert 'color_temp' not in lamp_1.attributes new_light1_on = LIGHT_1_ON.copy() @@ -668,19 +668,6 @@ def test_hs_color(): 'colormode': 'xy', 'hue': 1234, 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'xy', - 'hue': None, - 'sat': 123, 'xy': [0.4, 0.5] }), request_bridge_update=None, From b5bae17c6640d201c407defa341cf3431fe9ba31 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 30 Apr 2018 00:49:19 +0200 Subject: [PATCH 546/924] Revert Hue color state to be xy-based (#14154) --- homeassistant/components/light/hue.py | 5 +---- tests/components/light/test_hue.py | 13 ------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 6b4908b02d4..9f662718514 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -245,12 +245,9 @@ class HueLight(Light): mode = self._color_mode source = self.light.action if self.is_group else self.light.state - if mode == 'xy' and 'xy' in source: + if mode in ('xy', 'hs'): return color.color_xy_to_hs(*source['xy']) - if mode == 'hs' and 'hue' in source and 'sat' in source: - return source['hue'] / 65535 * 360, source['sat'] / 255 * 100 - return None @property diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index d36548e1e91..8f5b52ea6de 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -650,19 +650,6 @@ def test_hs_color(): assert light.hs_color is None - light = hue_light.HueLight( - light=Mock(state={ - 'colormode': 'hs', - 'hue': 1234, - 'sat': 123, - }), - request_bridge_update=None, - bridge=Mock(), - is_group=False, - ) - - assert light.hs_color == (1234 / 65535 * 360, 123 / 255 * 100) - light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', From f2a17a5462c7173f9002dad32444cf334ba4b678 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Sun, 29 Apr 2018 00:46:36 -0700 Subject: [PATCH 547/924] Fix Python 3.6 compatibility for HomeKit controller (#14160) Python 3.6's http client passes an additional argument to _send_output, so add that to the function definition. --- homeassistant/components/homekit_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 164e7d50e4d..e36e7439e09 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ KNOWN_DEVICES = "{}-devices".format(DOMAIN) _LOGGER = logging.getLogger(__name__) -def homekit_http_send(self, message_body=None): +def homekit_http_send(self, message_body=None, encode_chunked=False): r"""Send the currently buffered request and clear the buffer. Appends an extra \r\n to the buffer. From 03c34804bc04e98f3e5884b675cd3854429af34c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 30 Apr 2018 14:58:17 +0200 Subject: [PATCH 548/924] Added CONF_IP_ADDRESS to HomeKit (#14163) --- homeassistant/components/homekit/__init__.py | 16 +++++++---- tests/components/homekit/test_homekit.py | 28 +++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 24c6dfa8a76..6af470e80be 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -3,6 +3,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/homekit/ """ +import ipaddress import logging from zlib import adler32 @@ -12,8 +13,8 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -35,6 +36,8 @@ REQUIREMENTS = ['HAP-python==1.1.9'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_IP_ADDRESS): + vol.All(ipaddress.ip_address, cv.string), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, @@ -48,11 +51,12 @@ async def async_setup(hass, config): conf = config[DOMAIN] port = conf[CONF_PORT] + ip_address = conf.get(CONF_IP_ADDRESS) auto_start = conf[CONF_AUTO_START] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] - homekit = HomeKit(hass, port, entity_filter, entity_config) + homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) homekit.setup() if auto_start: @@ -151,10 +155,11 @@ def generate_aid(entity_id): class HomeKit(): """Class to handle all actions between HomeKit and Home Assistant.""" - def __init__(self, hass, port, entity_filter, entity_config): + def __init__(self, hass, port, ip_address, entity_filter, entity_config): """Initialize a HomeKit object.""" self.hass = hass self._port = port + self._ip_address = ip_address self._filter = entity_filter self._config = entity_config self.started = False @@ -169,9 +174,10 @@ class HomeKit(): self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self.stop) + ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, get_local_ip(), path) + self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index d1ad232d279..7ae37becbd5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -11,7 +11,8 @@ from homeassistant.components.homekit.const import ( DEFAULT_PORT, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( - CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + CONF_IP_ADDRESS, CONF_PORT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce @@ -59,7 +60,7 @@ class TestHomeKit(unittest.TestCase): self.hass, DOMAIN, {DOMAIN: {}})) self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, ANY, {}), + call(self.hass, DEFAULT_PORT, None, ANY, {}), call().setup()]) # Test auto start enabled @@ -74,7 +75,8 @@ class TestHomeKit(unittest.TestCase): """Test async_setup with auto start disabled and test service calls.""" mock_homekit.return_value = homekit = Mock() - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111}} + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) @@ -82,7 +84,7 @@ class TestHomeKit(unittest.TestCase): self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, ANY, {}), + call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) # Test start call with driver stopped. @@ -101,7 +103,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_setup(self): """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, {}, {}) + homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: @@ -117,9 +119,17 @@ class TestHomeKit(unittest.TestCase): self.assertEqual( self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + def test_homekit_setup_ip_address(self): + """Test setup with given IP address.""" + homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) + + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + homekit.setup() + mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) + def test_homekit_add_accessory(self): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, lambda entity_id: True, {}) + homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) homekit.bridge = HomeBridge(self.hass) with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ @@ -142,7 +152,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_entity_filter(self): """Test the entity filter.""" entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, entity_filter, {}) + homekit = HomeKit(self.hass, None, None, entity_filter, {}) with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.return_value = None @@ -162,7 +172,7 @@ class TestHomeKit(unittest.TestCase): @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, {}, {'cover.demo': {}}) + homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) homekit.bridge = HomeBridge(self.hass) homekit.driver = Mock() @@ -184,7 +194,7 @@ class TestHomeKit(unittest.TestCase): def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None) + homekit = HomeKit(None, None, None, None, None) homekit.driver = Mock() # Test if started = False From aba143ac9ff874452ffcb5e4c82dd45a15454623 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 09:18:18 -0400 Subject: [PATCH 549/924] Do not sync entities with an empty name (#14181) --- .../components/google_assistant/smart_home.py | 11 +++++--- .../google_assistant/test_smart_home.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 7e746d48bed..27d993aee76 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -102,18 +102,23 @@ class _GoogleEntity: if state.state == STATE_UNAVAILABLE: return None + entity_config = self.config.entity_config.get(state.entity_id, {}) + name = (entity_config.get(CONF_NAME) or state.name).strip() + + # If an empty string + if not name: + return None + traits = self.traits() # Found no supported traits for this entity if not traits: return None - entity_config = self.config.entity_config.get(state.entity_id, {}) - device = { 'id': state.entity_id, 'name': { - 'name': entity_config.get(CONF_NAME) or state.name + 'name': name }, 'attributes': {}, 'traits': [trait.name for trait in traits], diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index e284b026ad8..cdaf4200c97 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -286,3 +286,29 @@ async def test_unavailable_state_doesnt_sync(hass): 'devices': [] } } + + +async def test_empty_name_doesnt_sync(hass): + """Test that an entity with empty name does not sync over.""" + light = DemoLight( + None, ' ', + state=False, + ) + light.hass = hass + light.entity_id = 'light.demo_light' + await light.async_update_ha_state() + + result = await sh.async_handle_message(hass, BASIC_CONFIG, { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [] + } + } From 7f1b591fbb79c9adc2bfe4fa8904071e624d84c5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 30 Apr 2018 14:46:44 +0200 Subject: [PATCH 550/924] Improve chromecast disconnection logic (#14190) * Attempt Cast Fix * Cleanup --- homeassistant/components/media_player/cast.py | 26 +++++++++++++------ tests/components/media_player/test_cast.py | 16 +++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/media_player/cast.py b/homeassistant/components/media_player/cast.py index 632ab4214b8..a9bea9e4c1d 100644 --- a/homeassistant/components/media_player/cast.py +++ b/homeassistant/components/media_player/cast.py @@ -306,13 +306,18 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) self.hass.async_add_job(self.async_set_cast_info(discover)) + async def async_stop(event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() + async_dispatcher_connect(self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) self.hass.async_add_job(self.async_set_cast_info(self._cast_info)) async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" - self._async_disconnect() + await self._async_disconnect() if self._cast_info.uuid is not None: # Remove the entity from the added casts so that it can dynamically # be re-added again. @@ -328,7 +333,7 @@ class CastDevice(MediaPlayerDevice): if old_cast_info.host_port == cast_info.host_port: # Nothing connection-related updated return - self._async_disconnect() + await self._async_disconnect() # Failed connection will unfortunately never raise an exception, it # will instead just try connecting indefinitely. @@ -348,22 +353,27 @@ class CastDevice(MediaPlayerDevice): _LOGGER.debug("Connection successful!") self.async_schedule_update_ha_state() - @callback - def _async_disconnect(self): + async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return - _LOGGER.debug("Disconnecting from previous chromecast socket.") + _LOGGER.debug("Disconnecting from chromecast socket.") self._available = False - self._chromecast.disconnect(blocking=False) + self.async_schedule_update_ha_state() + + await self.hass.async_add_job(self._chromecast.disconnect) + # Invalidate some attributes self._chromecast = None self.cast_status = None self.media_status = None self.media_status_received = None - self._status_listener.invalidate() - self._status_listener = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + self.async_schedule_update_ha_state() # ========== Callbacks ========== def new_cast_status(self, cast_status): diff --git a/tests/components/media_player/test_cast.py b/tests/components/media_player/test_cast.py index ee69ec1c85d..41cf6749b71 100644 --- a/tests/components/media_player/test_cast.py +++ b/tests/components/media_player/test_cast.py @@ -346,8 +346,16 @@ async def test_switched_host(hass: HomeAssistantType): async_dispatcher_send(hass, cast.SIGNAL_CAST_DISCOVERED, changed) await hass.async_block_till_done() assert get_chromecast.call_count == 1 - chromecast.disconnect.assert_called_once_with(blocking=False) + assert chromecast.disconnect.call_count == 1 - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - chromecast.disconnect.assert_called_once_with(blocking=False) + +async def test_disconnect_on_stop(hass: HomeAssistantType): + """Test cast device disconnects socket on stop.""" + info = get_fake_chromecast_info() + + with patch('pychromecast.dial.get_device_status', return_value=info): + chromecast, _ = await async_setup_media_player_cast(hass, info) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert chromecast.disconnect.call_count == 1 From daeccfe7643efaaceb19970180275a9d7b47a721 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 09:56:42 -0400 Subject: [PATCH 551/924] Fix poorly formatted automations (#14196) --- homeassistant/components/config/automation.py | 8 ++- tests/components/config/test_automation.py | 67 +++++++++++++++++-- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 1e260854687..223159eb415 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -1,6 +1,7 @@ """Provide configuration end points for Automations.""" import asyncio from collections import OrderedDict +import uuid from homeassistant.const import CONF_ID from homeassistant.components.config import EditIdBasedConfigView @@ -29,7 +30,12 @@ class EditAutomationConfigView(EditIdBasedConfigView): """Set value.""" index = None for index, cur_value in enumerate(data): - if cur_value[CONF_ID] == config_key: + # When people copy paste their automations to the config file, + # they sometimes forget to add IDs. Fix it here. + if CONF_ID not in cur_value: + cur_value[CONF_ID] = uuid.uuid4().hex + + elif cur_value[CONF_ID] == config_key: break else: cur_value = OrderedDict() diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 327283e74aa..2c888dd2dd2 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -42,13 +42,13 @@ async def test_update_device_config(hass, aiohttp_client): client = await aiohttp_client(hass.http.app) orig_data = [ - { - 'id': 'sun', - }, - { - 'id': 'moon', - } - ] + { + 'id': 'sun', + }, + { + 'id': 'moon', + } + ] def mock_read(path): """Mock reading data.""" @@ -81,3 +81,56 @@ async def test_update_device_config(hass, aiohttp_client): 'action': [], } assert written[0] == orig_data + + +async def test_bad_formatted_automations(hass, aiohttp_client): + """Test that we handle automations without ID.""" + with patch.object(config, 'SECTIONS', ['automation']): + await async_setup_component(hass, 'config', {}) + + client = await aiohttp_client(hass.http.app) + + orig_data = [ + { + # No ID + 'action': { + 'event': 'hello' + } + }, + { + 'id': 'moon', + } + ] + + def mock_read(path): + """Mock reading data.""" + return orig_data + + written = [] + + def mock_write(path, data): + """Mock writing data.""" + written.append(data) + + with patch('homeassistant.components.config._read', mock_read), \ + patch('homeassistant.components.config._write', mock_write): + resp = await client.post( + '/api/config/automation/config/moon', data=json.dumps({ + 'trigger': [], + 'action': [], + 'condition': [], + })) + + assert resp.status == 200 + result = await resp.json() + assert result == {'result': 'ok'} + + # Verify ID added to orig_data + assert 'id' in orig_data[0] + + assert orig_data[1] == { + 'id': 'moon', + 'trigger': [], + 'condition': [], + 'action': [], + } From c704ceaeb7c3d5b085721af4003ad560e79ea9fe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 13:37:12 -0400 Subject: [PATCH 552/924] Version bump to 0.68.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0a69f166b43..4014a719912 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 68 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c23cc0e8271bab2dfa60f8c4096907780bf1362d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 Apr 2018 20:15:00 -0400 Subject: [PATCH 553/924] Disable eliqonline requirement (#14156) * Disable eliqonline requirement * Disable pylint import error --- homeassistant/components/sensor/eliqonline.py | 3 ++- requirements_all.txt | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 3e736ed719f..23c397053c5 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,7 +14,8 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['eliqonline==1.0.13'] +# pylint: disable=import-error, no-member +REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7cc644129b3..ff6e680051d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,9 +276,6 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 -# homeassistant.components.sensor.eliqonline -eliqonline==1.0.13 - # homeassistant.components.enocean enocean==0.40 From 12dff5baa8ce643d2ffa996f1202970c335c32f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Apr 2018 15:05:29 -0400 Subject: [PATCH 554/924] Add room hint support to Google Assistant cloud (#14180) --- homeassistant/components/cloud/__init__.py | 4 +++- tests/components/cloud/test_iot.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index e73d043d366..8c1a9751c19 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from homeassistant.components.alexa import smart_home as alexa_sh from homeassistant.components.google_assistant import helpers as ga_h +from homeassistant.components.google_assistant import const as ga_c from . import http_api, iot from .const import CONFIG_DIR, DOMAIN, SERVERS @@ -52,7 +53,8 @@ ALEXA_ENTITY_SCHEMA = vol.Schema({ GOOGLE_ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]) + vol.Optional(CONF_ALIASES): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ga_c.CONF_ROOM_HINT): cv.string, }) ASSISTANT_SCHEMA = vol.Schema({ diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index f4ae81ad2f2..81b1e315085 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -318,7 +318,8 @@ def test_handler_google_actions(hass): 'entity_config': { 'switch.test': { 'name': 'Config name', - 'aliases': 'Config alias' + 'aliases': 'Config alias', + 'room': 'living room' } } } @@ -347,6 +348,7 @@ def test_handler_google_actions(hass): assert device['name']['name'] == 'Config name' assert device['name']['nicknames'] == ['Config alias'] assert device['type'] == 'action.devices.types.SWITCH' + assert device['roomHint'] == 'living room' async def test_refresh_token_expired(hass): From 626d6df545de8cc71128992181e9d09d024aee9e Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Tue, 1 May 2018 10:14:33 +0200 Subject: [PATCH 555/924] Update CODEOWNERS (#14214) --- CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 528716e174d..a62ed67db66 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,8 +54,11 @@ homeassistant/components/device_tracker/tile.py @bachya homeassistant/components/history_graph.py @andrey-git homeassistant/components/light/tplink.py @rytilahti homeassistant/components/light/yeelight.py @rytilahti +homeassistant/components/lock/nello.py @pschmitt +homeassistant/components/lock/nuki.py @pschmitt homeassistant/components/media_player/emby.py @mezz64 homeassistant/components/media_player/kodi.py @armills +homeassistant/components/media_player/liveboxplaytv.py @pschmitt homeassistant/components/media_player/mediaroom.py @dgomes homeassistant/components/media_player/monoprice.py @etsinko homeassistant/components/media_player/sonos.py @amelchio @@ -77,6 +80,7 @@ homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti +homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi homeassistant/components/*/axis.py @kane610 From a4e0c9c251095a171538ca5475c5a14c4e4e4211 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 1 May 2018 13:51:47 +0100 Subject: [PATCH 556/924] Fixes #14169 (Upgrade pyupnp-async to 0.1.0.2) (#14210) * Fixes #14169 (upstream version bump) * bump pyupnp-async version --- homeassistant/components/upnp.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index dd611090c22..26a59746aea 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -15,7 +15,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import get_local_ip -REQUIREMENTS = ['pyupnp-async==0.1.0.1'] +REQUIREMENTS = ['pyupnp-async==0.1.0.2'] DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 796f80c3bd0..93bf26f5239 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1069,7 +1069,7 @@ pytradfri[async]==5.4.2 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.0.1 +pyupnp-async==0.1.0.2 # homeassistant.components.keyboard # pyuserinput==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 28265bdb5f1..a5835392c4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -162,7 +162,7 @@ pythonwhois==2.4.3 pyunifi==2.13 # homeassistant.components.upnp -pyupnp-async==0.1.0.1 +pyupnp-async==0.1.0.2 # homeassistant.components.notify.html5 pywebpush==1.6.0 From 9d4d1c82335e4c40d8502e30bd11c01b177e15d2 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Tue, 1 May 2018 05:55:25 -0700 Subject: [PATCH 557/924] zha: Clean up binary_sensor listener registration/state updates (#14197) - Instead of registering listeners in the entity __init__, do it in async_added_to_hass to avoid errors updating an entity which isn't fully set up yet - Change from schedule_update_ha_state to async_schedule_update_ha_state --- homeassistant/components/binary_sensor/zha.py | 9 +++---- homeassistant/components/sensor/zha.py | 2 +- homeassistant/components/zha/__init__.py | 26 ++++++++++++------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index e1e6689d1eb..756323f41d9 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -164,7 +164,6 @@ class Switch(zha.Entity, BinarySensorDevice): """Handle attribute updates on this cluster.""" if attrid == 0: self._entity.set_state(value) - self._entity.schedule_update_ha_state() def zdo_command(self, *args, **kwargs): """Handle ZDO commands on this cluster.""" @@ -202,6 +201,7 @@ class Switch(zha.Entity, BinarySensorDevice): def __init__(self, **kwargs): """Initialize Switch.""" + super().__init__(**kwargs) self._state = True self._level = 255 from zigpy.zcl.clusters import general @@ -209,7 +209,6 @@ class Switch(zha.Entity, BinarySensorDevice): general.OnOff.cluster_id: self.OnOffListener(self), general.LevelControl.cluster_id: self.LevelListener(self), } - super().__init__(**kwargs) @property def is_on(self) -> bool: @@ -227,20 +226,20 @@ class Switch(zha.Entity, BinarySensorDevice): self._level = 0 self._level = min(255, max(0, self._level + change)) self._state = bool(self._level) - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def set_level(self, level): """Set the level, setting state if appropriate.""" self._level = level self._state = bool(self._level) - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def set_state(self, state): """Set the state.""" self._state = state if self._level == 0: self._level = 255 - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 36cdca2e638..d856ed1a17e 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -71,7 +71,7 @@ class Sensor(zha.Entity): _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) if attribute == self.value_attribute: self._state = value - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() class TemperatureSensor(Sensor): diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index dc9cb26462d..9b66c4c6ded 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -288,11 +288,6 @@ class Entity(entity.Entity): """A base class for ZHA entities.""" _domain = None # Must be overridden by subclasses - # Normally the entity itself is the listener. Base classes may set this to - # a dict of cluster ID -> listener to receive messages for specific - # clusters separately - _in_listeners = {} - _out_listeners = {} def __init__(self, endpoint, in_clusters, out_clusters, manufacturer, model, application_listener, unique_id, **kwargs): @@ -321,19 +316,30 @@ class Entity(entity.Entity): kwargs.get('entity_suffix', ''), ) - for cluster_id, cluster in in_clusters.items(): - cluster.add_listener(self._in_listeners.get(cluster_id, self)) - for cluster_id, cluster in out_clusters.items(): - cluster.add_listener(self._out_listeners.get(cluster_id, self)) - self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters self._state = ha_const.STATE_UNKNOWN self._unique_id = unique_id + # Normally the entity itself is the listener. Sub-classes may set this + # to a dict of cluster ID -> listener to receive messages for specific + # clusters separately + self._in_listeners = {} + self._out_listeners = {} + application_listener.register_entity(ieee, self) + async def async_added_to_hass(self): + """Callback once the entity is added to hass. + + It is now safe to update the entity state + """ + for cluster_id, cluster in self._in_clusters.items(): + cluster.add_listener(self._in_listeners.get(cluster_id, self)) + for cluster_id, cluster in self._out_clusters.items(): + cluster.add_listener(self._out_listeners.get(cluster_id, self)) + @property def unique_id(self) -> str: """Return a unique ID.""" From b994c10d7f35cbf5f35c2f65ac713b4fa3067d6a Mon Sep 17 00:00:00 2001 From: sander76 Date: Tue, 1 May 2018 17:01:13 +0200 Subject: [PATCH 558/924] HomematicIP cloud: Add logic to check accesspoint connection state (#14203) * Add logic to check accesspoint connection state * lint * changes as per @balloobs comments. * pylint fix --- homeassistant/components/homematicip_cloud.py | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 0ed9fe22e27..0b15d7a3dfe 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -7,13 +7,15 @@ https://home-assistant.io/components/homematicip_cloud/ import asyncio import logging + import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity +from homeassistant.core import callback REQUIREMENTS = ['homematicip==0.9.2.4'] @@ -96,6 +98,7 @@ class HomematicipConnector: def __init__(self, hass, config, websession): """Initialize HomematicIP cloud connection.""" from homematicip.async.home import AsyncHome + self._hass = hass self._ws_close_requested = False self._retry_task = None @@ -106,6 +109,9 @@ class HomematicipConnector: self.home = AsyncHome(hass.loop, websession) self.home.set_auth_token(_authtoken) + self.home.on_update(self.async_update) + self._accesspoint_connected = True + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.close()) async def init(self): @@ -113,6 +119,58 @@ class HomematicipConnector: await self.home.init(self._accesspoint) await self.home.get_current_state() + @callback + def async_update(self, *args, **kwargs): + """Async update the home device. + + Triggered when the hmip HOME_CHANGED event has fired. + There are several occasions for this event to happen. + We are only interested to check whether the access point + is still connected. If not, device state changes cannot + be forwarded to hass. So if access point is disconnected all devices + are set to unavailable. + """ + if not self.home.connected: + _LOGGER.error( + "HMIP access point has lost connection with the cloud") + self._accesspoint_connected = False + self.set_all_to_unavailable() + elif not self._accesspoint_connected: + # Explicitly getting an update as device states might have + # changed during access point disconnect.""" + + job = self._hass.async_add_job(self.get_state()) + job.add_done_callback(self.get_state_finished) + + async def get_state(self): + """Update hmip state and tell hass.""" + await self.home.get_current_state() + self.update_all() + + def get_state_finished(self, future): + """Execute when get_state coroutine has finished.""" + from homematicip.base.base_connection import HmipConnectionError + + try: + future.result() + except HmipConnectionError: + # Somehow connection could not recover. Will disconnect and + # so reconnect loop is taking over. + _LOGGER.error( + "updating state after himp access point reconnect failed.") + self._hass.async_add_job(self.home.disable_events()) + + def set_all_to_unavailable(self): + """Set all devices to unavailable and tell Hass.""" + for device in self.home.devices: + device.unreach = True + self.update_all() + + def update_all(self): + """Signal all devices to update their state.""" + for device in self.home.devices: + device.fire_update_event() + async def _handle_connection(self): """Handle websocket connection.""" from homematicip.base.base_connection import HmipConnectionError From cdd45e78783037dfa6449eb91a7dafb2c9ae0c44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 May 2018 12:20:41 -0400 Subject: [PATCH 559/924] Foundation for users (#13968) * Add initial user foundation to Home Assistant * Address comments * Address comments * Allow non-ascii passwords * One more utf-8 hmac compare digest * Add new line --- homeassistant/auth.py | 505 ++++++++++++++++++ homeassistant/auth_providers/__init__.py | 1 + .../auth_providers/insecure_example.py | 116 ++++ homeassistant/components/auth/__init__.py | 344 ++++++++++++ homeassistant/components/auth/client.py | 63 +++ homeassistant/components/http/auth.py | 43 +- homeassistant/config.py | 11 +- homeassistant/config_entries.py | 6 +- homeassistant/const.py | 1 + homeassistant/data_entry_flow.py | 2 +- homeassistant/helpers/data_entry_flow.py | 70 ++- pylintrc | 4 + tests/auth_providers/__init__.py | 1 + tests/auth_providers/test_insecure_example.py | 89 +++ tests/common.py | 34 +- tests/components/auth/__init__.py | 38 ++ tests/components/auth/test_client.py | 70 +++ tests/components/auth/test_init.py | 53 ++ tests/components/auth/test_init_link_user.py | 150 ++++++ tests/components/auth/test_init_login_flow.py | 66 +++ tests/components/http/test_init.py | 7 +- tests/test_auth.py | 159 ++++++ 22 files changed, 1774 insertions(+), 59 deletions(-) create mode 100644 homeassistant/auth.py create mode 100644 homeassistant/auth_providers/__init__.py create mode 100644 homeassistant/auth_providers/insecure_example.py create mode 100644 homeassistant/components/auth/__init__.py create mode 100644 homeassistant/components/auth/client.py create mode 100644 tests/auth_providers/__init__.py create mode 100644 tests/auth_providers/test_insecure_example.py create mode 100644 tests/components/auth/__init__.py create mode 100644 tests/components/auth/test_client.py create mode 100644 tests/components/auth/test_init.py create mode 100644 tests/components/auth/test_init_link_user.py create mode 100644 tests/components/auth/test_init_login_flow.py create mode 100644 tests/test_auth.py diff --git a/homeassistant/auth.py b/homeassistant/auth.py new file mode 100644 index 00000000000..55de9309954 --- /dev/null +++ b/homeassistant/auth.py @@ -0,0 +1,505 @@ +"""Provide an authentication layer for Home Assistant.""" +import asyncio +import binascii +from collections import OrderedDict +from datetime import datetime, timedelta +import os +import importlib +import logging +import uuid + +import attr +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant import data_entry_flow, requirements +from homeassistant.core import callback +from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util.decorator import Registry +from homeassistant.util import dt as dt_util + + +_LOGGER = logging.getLogger(__name__) + + +AUTH_PROVIDERS = Registry() + +AUTH_PROVIDER_SCHEMA = vol.Schema({ + vol.Required(CONF_TYPE): str, + vol.Optional(CONF_NAME): str, + # Specify ID if you have two auth providers for same type. + vol.Optional(CONF_ID): str, +}, extra=vol.ALLOW_EXTRA) + +ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) +DATA_REQS = 'auth_reqs_processed' + + +class AuthError(HomeAssistantError): + """Generic authentication error.""" + + +class InvalidUser(AuthError): + """Raised when an invalid user has been specified.""" + + +class InvalidPassword(AuthError): + """Raised when an invalid password has been supplied.""" + + +class UnknownError(AuthError): + """When an unknown error occurs.""" + + +def generate_secret(entropy=32): + """Generate a secret. + + Backport of secrets.token_hex from Python 3.6 + + Event loop friendly. + """ + return binascii.hexlify(os.urandom(entropy)).decode('ascii') + + +class AuthProvider: + """Provider of user authentication.""" + + DEFAULT_TITLE = 'Unnamed auth provider' + + initialized = False + + def __init__(self, store, config): + """Initialize an auth provider.""" + self.store = store + self.config = config + + @property + def id(self): # pylint: disable=invalid-name + """Return id of the auth provider. + + Optional, can be None. + """ + return self.config.get(CONF_ID) + + @property + def type(self): + """Return type of the provider.""" + return self.config[CONF_TYPE] + + @property + def name(self): + """Return the name of the auth provider.""" + return self.config.get(CONF_NAME, self.DEFAULT_TITLE) + + async def async_credentials(self): + """Return all credentials of this provider.""" + return await self.store.credentials_for_provider(self.type, self.id) + + @callback + def async_create_credentials(self, data): + """Create credentials.""" + return Credentials( + auth_provider_type=self.type, + auth_provider_id=self.id, + data=data, + ) + + # Implement by extending class + + async def async_initialize(self): + """Initialize the auth provider. + + Optional. + """ + + async def async_credential_flow(self): + """Return the data flow for logging in with auth provider.""" + raise NotImplementedError + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + raise NotImplementedError + + async def async_user_meta_for_credentials(self, credentials): + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + return {} + + +@attr.s(slots=True) +class User: + """A user.""" + + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + is_owner = attr.ib(type=bool, default=False) + is_active = attr.ib(type=bool, default=False) + name = attr.ib(type=str, default=None) + # For persisting and see if saved? + # store = attr.ib(type=AuthStore, default=None) + + # List of credentials of a user. + credentials = attr.ib(type=list, default=attr.Factory(list)) + + # Tokens associated with a user. + refresh_tokens = attr.ib(type=dict, default=attr.Factory(dict)) + + def as_dict(self): + """Convert user object to a dictionary.""" + return { + 'id': self.id, + 'is_owner': self.is_owner, + 'is_active': self.is_active, + 'name': self.name, + } + + +@attr.s(slots=True) +class RefreshToken: + """RefreshToken for a user to grant new access tokens.""" + + user = attr.ib(type=User) + client_id = attr.ib(type=str) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) + access_token_expiration = attr.ib(type=timedelta, + default=ACCESS_TOKEN_EXPIRATION) + token = attr.ib(type=str, + default=attr.Factory(lambda: generate_secret(64))) + access_tokens = attr.ib(type=list, default=attr.Factory(list)) + + +@attr.s(slots=True) +class AccessToken: + """Access token to access the API. + + These will only ever be stored in memory and not be persisted. + """ + + refresh_token = attr.ib(type=RefreshToken) + created_at = attr.ib(type=datetime, default=attr.Factory(dt_util.utcnow)) + token = attr.ib(type=str, + default=attr.Factory(generate_secret)) + + @property + def expires(self): + """Return datetime when this token expires.""" + return self.created_at + self.refresh_token.access_token_expiration + + +@attr.s(slots=True) +class Credentials: + """Credentials for a user on an auth provider.""" + + auth_provider_type = attr.ib(type=str) + auth_provider_id = attr.ib(type=str) + + # Allow the auth provider to store data to represent their auth. + data = attr.ib(type=dict) + + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + is_new = attr.ib(type=bool, default=True) + + +@attr.s(slots=True) +class Client: + """Client that interacts with Home Assistant on behalf of a user.""" + + name = attr.ib(type=str) + id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + secret = attr.ib(type=str, default=attr.Factory(generate_secret)) + + +async def load_auth_provider_module(hass, provider): + """Load an auth provider.""" + try: + module = importlib.import_module( + 'homeassistant.auth_providers.{}'.format(provider)) + except ImportError: + _LOGGER.warning('Unable to find auth provider %s', provider) + return None + + if hass.config.skip_pip or not hasattr(module, 'REQUIREMENTS'): + return module + + processed = hass.data.get(DATA_REQS) + + if processed is None: + processed = hass.data[DATA_REQS] = set() + elif provider in processed: + return module + + req_success = await requirements.async_process_requirements( + hass, 'auth provider {}'.format(provider), module.REQUIREMENTS) + + if not req_success: + return None + + return module + + +async def auth_manager_from_config(hass, provider_configs): + """Initialize an auth manager from config.""" + store = AuthStore(hass) + if provider_configs: + providers = await asyncio.gather( + *[_auth_provider_from_config(hass, store, config) + for config in provider_configs]) + else: + providers = [] + # So returned auth providers are in same order as config + provider_hash = OrderedDict() + for provider in providers: + if provider is None: + continue + + key = (provider.type, provider.id) + + if key in provider_hash: + _LOGGER.error( + 'Found duplicate provider: %s. Please add unique IDs if you ' + 'want to have the same provider twice.', key) + continue + + provider_hash[key] = provider + manager = AuthManager(hass, store, provider_hash) + return manager + + +async def _auth_provider_from_config(hass, store, config): + """Initialize an auth provider from a config.""" + provider_name = config[CONF_TYPE] + module = await load_auth_provider_module(hass, provider_name) + + if module is None: + return None + + try: + config = module.CONFIG_SCHEMA(config) + except vol.Invalid as err: + _LOGGER.error('Invalid configuration for auth provider %s: %s', + provider_name, humanize_error(config, err)) + return None + + return AUTH_PROVIDERS[provider_name](store, config) + + +class AuthManager: + """Manage the authentication for Home Assistant.""" + + def __init__(self, hass, store, providers): + """Initialize the auth manager.""" + self._store = store + self._providers = providers + self.login_flow = data_entry_flow.FlowManager( + hass, self._async_create_login_flow, + self._async_finish_login_flow) + self.access_tokens = {} + + @property + def async_auth_providers(self): + """Return a list of available auth providers.""" + return self._providers.values() + + async def async_get_user(self, user_id): + """Retrieve a user.""" + return await self._store.async_get_user(user_id) + + async def async_get_or_create_user(self, credentials): + """Get or create a user.""" + return await self._store.async_get_or_create_user( + credentials, self._async_get_auth_provider(credentials)) + + async def async_link_user(self, user, credentials): + """Link credentials to an existing user.""" + await self._store.async_link_user(user, credentials) + + async def async_remove_user(self, user): + """Remove a user.""" + await self._store.async_remove_user(user) + + async def async_create_refresh_token(self, user, client_id): + """Create a new refresh token for a user.""" + return await self._store.async_create_refresh_token(user, client_id) + + async def async_get_refresh_token(self, token): + """Get refresh token by token.""" + return await self._store.async_get_refresh_token(token) + + @callback + def async_create_access_token(self, refresh_token): + """Create a new access token.""" + access_token = AccessToken(refresh_token) + self.access_tokens[access_token.token] = access_token + return access_token + + @callback + def async_get_access_token(self, token): + """Get an access token.""" + return self.access_tokens.get(token) + + async def async_create_client(self, name): + """Create a new client.""" + return await self._store.async_create_client(name) + + async def async_get_client(self, client_id): + """Get a client.""" + return await self._store.async_get_client(client_id) + + async def _async_create_login_flow(self, handler, *, source, data): + """Create a login flow.""" + auth_provider = self._providers[handler] + + if not auth_provider.initialized: + auth_provider.initialized = True + await auth_provider.async_initialize() + + return await auth_provider.async_credential_flow() + + async def _async_finish_login_flow(self, result): + """Result of a credential login flow.""" + auth_provider = self._providers[result['handler']] + return await auth_provider.async_get_or_create_credentials( + result['data']) + + @callback + def _async_get_auth_provider(self, credentials): + """Helper to get auth provider from a set of credentials.""" + auth_provider_key = (credentials.auth_provider_type, + credentials.auth_provider_id) + return self._providers[auth_provider_key] + + +class AuthStore: + """Stores authentication info. + + Any mutation to an object should happen inside the auth store. + + The auth store is lazy. It won't load the data from disk until a method is + called that needs it. + """ + + def __init__(self, hass): + """Initialize the auth store.""" + self.hass = hass + self.users = None + self.clients = None + self._load_lock = asyncio.Lock(loop=hass.loop) + + async def credentials_for_provider(self, provider_type, provider_id): + """Return credentials for specific auth provider type and id.""" + if self.users is None: + await self.async_load() + + return [ + credentials + for user in self.users.values() + for credentials in user.credentials + if (credentials.auth_provider_type == provider_type and + credentials.auth_provider_id == provider_id) + ] + + async def async_get_user(self, user_id): + """Retrieve a user.""" + if self.users is None: + await self.async_load() + + return self.users.get(user_id) + + async def async_get_or_create_user(self, credentials, auth_provider): + """Get or create a new user for given credentials. + + If link_user is passed in, the credentials will be linked to the passed + in user if the credentials are new. + """ + if self.users is None: + await self.async_load() + + # New credentials, store in user + if credentials.is_new: + info = await auth_provider.async_user_meta_for_credentials( + credentials) + # Make owner and activate user if it's the first user. + if self.users: + is_owner = False + is_active = False + else: + is_owner = True + is_active = True + + new_user = User( + is_owner=is_owner, + is_active=is_active, + name=info.get('name'), + ) + self.users[new_user.id] = new_user + await self.async_link_user(new_user, credentials) + return new_user + + for user in self.users.values(): + for creds in user.credentials: + if (creds.auth_provider_type == credentials.auth_provider_type + and creds.auth_provider_id == + credentials.auth_provider_id): + return user + + raise ValueError('We got credentials with ID but found no user') + + async def async_link_user(self, user, credentials): + """Add credentials to an existing user.""" + user.credentials.append(credentials) + await self.async_save() + credentials.is_new = False + + async def async_remove_user(self, user): + """Remove a user.""" + self.users.pop(user.id) + await self.async_save() + + async def async_create_refresh_token(self, user, client_id): + """Create a new token for a user.""" + refresh_token = RefreshToken(user, client_id) + user.refresh_tokens[refresh_token.token] = refresh_token + await self.async_save() + return refresh_token + + async def async_get_refresh_token(self, token): + """Get refresh token by token.""" + if self.users is None: + await self.async_load() + + for user in self.users.values(): + refresh_token = user.refresh_tokens.get(token) + if refresh_token is not None: + return refresh_token + + return None + + async def async_create_client(self, name): + """Create a new client.""" + if self.clients is None: + await self.async_load() + + client = Client(name) + self.clients[client.id] = client + await self.async_save() + return client + + async def async_get_client(self, client_id): + """Get a client.""" + if self.clients is None: + await self.async_load() + + return self.clients.get(client_id) + + async def async_load(self): + """Load the users.""" + async with self._load_lock: + self.users = {} + self.clients = {} + + async def async_save(self): + """Save users.""" + pass diff --git a/homeassistant/auth_providers/__init__.py b/homeassistant/auth_providers/__init__.py new file mode 100644 index 00000000000..4705e7580ca --- /dev/null +++ b/homeassistant/auth_providers/__init__.py @@ -0,0 +1 @@ +"""Auth providers for Home Assistant.""" diff --git a/homeassistant/auth_providers/insecure_example.py b/homeassistant/auth_providers/insecure_example.py new file mode 100644 index 00000000000..8538e8c2f3e --- /dev/null +++ b/homeassistant/auth_providers/insecure_example.py @@ -0,0 +1,116 @@ +"""Example auth provider.""" +from collections import OrderedDict +import hmac + +import voluptuous as vol + +from homeassistant import auth, data_entry_flow +from homeassistant.core import callback + + +USER_SCHEMA = vol.Schema({ + vol.Required('username'): str, + vol.Required('password'): str, + vol.Optional('name'): str, +}) + + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ + vol.Required('users'): [USER_SCHEMA] +}, extra=vol.PREVENT_EXTRA) + + +@auth.AUTH_PROVIDERS.register('insecure_example') +class ExampleAuthProvider(auth.AuthProvider): + """Example auth provider based on hardcoded usernames and passwords.""" + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + @callback + def async_validate_login(self, username, password): + """Helper to validate a username and password.""" + user = None + + # Compare all users to avoid timing attacks. + for usr in self.config['users']: + if hmac.compare_digest(username.encode('utf-8'), + usr['username'].encode('utf-8')): + user = usr + + if user is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password.encode('utf-8'), + password.encode('utf-8')) + raise auth.InvalidUser + + if not hmac.compare_digest(user['password'].encode('utf-8'), + password.encode('utf-8')): + raise auth.InvalidPassword + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + username = flow_result['username'] + password = flow_result['password'] + + self.async_validate_login(username, password) + + for credential in await self.async_credentials(): + if credential.data['username'] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + 'username': username + }) + + async def async_user_meta_for_credentials(self, credentials): + """Return extra user metadata for credentials. + + Will be used to populate info when creating a new user. + """ + username = credentials.data['username'] + + for user in self.config['users']: + if user['username'] == username: + return { + 'name': user.get('name') + } + + return {} + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + self._auth_provider.async_validate_login( + user_input['username'], user_input['password']) + except (auth.InvalidUser, auth.InvalidPassword): + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data=user_input + ) + + schema = OrderedDict() + schema['username'] = str + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py new file mode 100644 index 00000000000..d4b4b0f4591 --- /dev/null +++ b/homeassistant/components/auth/__init__.py @@ -0,0 +1,344 @@ +"""Component to allow users to login and get tokens. + +All requests will require passing in a valid client ID and secret via HTTP +Basic Auth. + +# GET /auth/providers + +Return a list of auth providers. Example: + +[ + { + "name": "Local", + "id": null, + "type": "local_provider", + } +] + +# POST /auth/login_flow + +Create a login flow. Will return the first step of the flow. + +Pass in parameter 'handler' to specify the auth provider to use. Auth providers +are identified by type and id. + +{ + "handler": ["local_provider", null] +} + +Return value will be a step in a data entry flow. See the docs for data entry +flow for details. + +{ + "data_schema": [ + {"name": "username", "type": "string"}, + {"name": "password", "type": "string"} + ], + "errors": {}, + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "step_id": "init", + "type": "form" +} + +# POST /auth/login_flow/{flow_id} + +Progress the flow. Most flows will be 1 page, but could optionally add extra +login challenges, like TFA. Once the flow has finished, the returned step will +have type "create_entry" and "result" key will contain an authorization code. + +{ + "flow_id": "8f7e42faab604bcab7ac43c44ca34d58", + "handler": ["insecure_example", null], + "result": "411ee2f916e648d691e937ae9344681e", + "source": "user", + "title": "Example", + "type": "create_entry", + "version": 1 +} + +# POST /auth/token + +This is an OAuth2 endpoint for granting tokens. We currently support the grant +types "authorization_code" and "refresh_token". Because we follow the OAuth2 +spec, data should be send in formatted as x-www-form-urlencoded. Examples will +be in JSON as it's more readable. + +## Grant type authorization_code + +Exchange the authorization code retrieved from the login flow for tokens. + +{ + "grant_type": "authorization_code", + "code": "411ee2f916e648d691e937ae9344681e" +} + +Return value will be the access and refresh tokens. The access token will have +a limited expiration. New access tokens can be requested using the refresh +token. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "refresh_token": "IJKLMNOPQRST", + "token_type": "Bearer" +} + +## Grant type refresh_token + +Request a new access token using a refresh token. + +{ + "grant_type": "refresh_token", + "refresh_token": "IJKLMNOPQRST" +} + +Return value will be a new access token. The access token will have +a limited expiration. + +{ + "access_token": "ABCDEFGH", + "expires_in": 1800, + "token_type": "Bearer" +} +""" +import logging +import uuid + +import aiohttp.web +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.core import callback +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, FlowManagerResourceView) +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator + +from .client import verify_client + +DOMAIN = 'auth' +DEPENDENCIES = ['http'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Component to allow users to login.""" + store_credentials, retrieve_credentials = _create_cred_store() + + hass.http.register_view(AuthProvidersView) + hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow)) + hass.http.register_view( + LoginFlowResourceView(hass.auth.login_flow, store_credentials)) + hass.http.register_view(GrantTokenView(retrieve_credentials)) + hass.http.register_view(LinkUserView(retrieve_credentials)) + + return True + + +class AuthProvidersView(HomeAssistantView): + """View to get available auth providers.""" + + url = '/auth/providers' + name = 'api:auth:providers' + requires_auth = False + + @verify_client + async def get(self, request, client_id): + """Get available auth providers.""" + return self.json([{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in request.app['hass'].auth.async_auth_providers]) + + +class LoginFlowIndexView(FlowManagerIndexView): + """View to create a config flow.""" + + url = '/auth/login_flow' + name = 'api:auth:login_flow' + requires_auth = False + + async def get(self, request): + """Do not allow index of flows in progress.""" + return aiohttp.web.Response(status=405) + + # pylint: disable=arguments-differ + @verify_client + async def post(self, request, client_id): + """Create a new login flow.""" + # pylint: disable=no-value-for-parameter + return await super().post(request) + + +class LoginFlowResourceView(FlowManagerResourceView): + """View to interact with the flow manager.""" + + url = '/auth/login_flow/{flow_id}' + name = 'api:auth:login_flow:resource' + requires_auth = False + + def __init__(self, flow_mgr, store_credentials): + """Initialize the login flow resource view.""" + super().__init__(flow_mgr) + self._store_credentials = store_credentials + + # pylint: disable=arguments-differ + async def get(self, request): + """Do not allow getting status of a flow in progress.""" + return self.json_message('Invalid flow specified', 404) + + # pylint: disable=arguments-differ + @verify_client + @RequestDataValidator(vol.Schema(dict), allow_empty=True) + async def post(self, request, client_id, flow_id, data): + """Handle progressing a login flow request.""" + try: + result = await self._flow_mgr.async_configure(flow_id, data) + except data_entry_flow.UnknownFlow: + return self.json_message('Invalid flow specified', 404) + except vol.Invalid: + return self.json_message('User input malformed', 400) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return self.json(self._prepare_result_json(result)) + + result.pop('data') + result['result'] = self._store_credentials(client_id, result['result']) + + return self.json(result) + + +class GrantTokenView(HomeAssistantView): + """View to grant tokens.""" + + url = '/auth/token' + name = 'api:auth:token' + requires_auth = False + + def __init__(self, retrieve_credentials): + """Initialize the grant token view.""" + self._retrieve_credentials = retrieve_credentials + + @verify_client + async def post(self, request, client_id): + """Grant a token.""" + hass = request.app['hass'] + data = await request.post() + grant_type = data.get('grant_type') + + if grant_type == 'authorization_code': + return await self._async_handle_auth_code( + hass, client_id, data) + + elif grant_type == 'refresh_token': + return await self._async_handle_refresh_token( + hass, client_id, data) + + return self.json({ + 'error': 'unsupported_grant_type', + }, status_code=400) + + async def _async_handle_auth_code(self, hass, client_id, data): + """Handle authorization code request.""" + code = data.get('code') + + if code is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + credentials = self._retrieve_credentials(client_id, code) + + if credentials is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + user = await hass.auth.async_get_or_create_user(credentials) + refresh_token = await hass.auth.async_create_refresh_token(user, + client_id) + access_token = hass.auth.async_create_access_token(refresh_token) + + return self.json({ + 'access_token': access_token.token, + 'token_type': 'Bearer', + 'refresh_token': refresh_token.token, + 'expires_in': + int(refresh_token.access_token_expiration.total_seconds()), + }) + + async def _async_handle_refresh_token(self, hass, client_id, data): + """Handle authorization code request.""" + token = data.get('refresh_token') + + if token is None: + return self.json({ + 'error': 'invalid_request', + }, status_code=400) + + refresh_token = await hass.auth.async_get_refresh_token(token) + + if refresh_token is None or refresh_token.client_id != client_id: + return self.json({ + 'error': 'invalid_grant', + }, status_code=400) + + access_token = hass.auth.async_create_access_token(refresh_token) + + return self.json({ + 'access_token': access_token.token, + 'token_type': 'Bearer', + 'expires_in': + int(refresh_token.access_token_expiration.total_seconds()), + }) + + +class LinkUserView(HomeAssistantView): + """View to link existing users to new credentials.""" + + url = '/auth/link_user' + name = 'api:auth:link_user' + + def __init__(self, retrieve_credentials): + """Initialize the link user view.""" + self._retrieve_credentials = retrieve_credentials + + @RequestDataValidator(vol.Schema({ + 'code': str, + 'client_id': str, + })) + async def post(self, request, data): + """Link a user.""" + hass = request.app['hass'] + user = request['hass_user'] + + credentials = self._retrieve_credentials( + data['client_id'], data['code']) + + if credentials is None: + return self.json_message('Invalid code', status_code=400) + + await hass.auth.async_link_user(user, credentials) + return self.json_message('User linked') + + +@callback +def _create_cred_store(): + """Create a credential store.""" + temp_credentials = {} + + @callback + def store_credentials(client_id, credentials): + """Store credentials and return a code to retrieve it.""" + code = uuid.uuid4().hex + temp_credentials[(client_id, code)] = credentials + return code + + @callback + def retrieve_credentials(client_id, code): + """Retrieve credentials.""" + return temp_credentials.pop((client_id, code), None) + + return store_credentials, retrieve_credentials diff --git a/homeassistant/components/auth/client.py b/homeassistant/components/auth/client.py new file mode 100644 index 00000000000..28d72aefe0f --- /dev/null +++ b/homeassistant/components/auth/client.py @@ -0,0 +1,63 @@ +"""Helpers to resolve client ID/secret.""" +import base64 +from functools import wraps +import hmac + +import aiohttp.hdrs + + +def verify_client(method): + """Decorator to verify client id/secret on requests.""" + @wraps(method) + async def wrapper(view, request, *args, **kwargs): + """Verify client id/secret before doing request.""" + client_id = await _verify_client(request) + + if client_id is None: + return view.json({ + 'error': 'invalid_client', + }, status_code=401) + + return await method( + view, request, *args, client_id=client_id, **kwargs) + + return wrapper + + +async def _verify_client(request): + """Method to verify the client id/secret in consistent time. + + By using a consistent time for looking up client id and comparing the + secret, we prevent attacks by malicious actors trying different client ids + and are able to derive from the time it takes to process the request if + they guessed the client id correctly. + """ + if aiohttp.hdrs.AUTHORIZATION not in request.headers: + return None + + auth_type, auth_value = \ + request.headers.get(aiohttp.hdrs.AUTHORIZATION).split(' ', 1) + + if auth_type != 'Basic': + return None + + decoded = base64.b64decode(auth_value).decode('utf-8') + try: + client_id, client_secret = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return None + + client = await request.app['hass'].auth.async_get_client(client_id) + + if client is None: + # Still do a compare so we run same time as if a client was found. + hmac.compare_digest(client_secret.encode('utf-8'), + client_secret.encode('utf-8')) + return None + + if hmac.compare_digest(client_secret.encode('utf-8'), + client.secret.encode('utf-8')): + return client_id + + return None diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 65c70c37bd2..5558063c5c4 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -32,17 +32,19 @@ def setup_auth(app, trusted_networks, api_password): if (HTTP_HEADER_HA_AUTH in request.headers and hmac.compare_digest( - api_password, request.headers[HTTP_HEADER_HA_AUTH])): + api_password.encode('utf-8'), + request.headers[HTTP_HEADER_HA_AUTH].encode('utf-8'))): # A valid auth header has been set authenticated = True elif (DATA_API_PASSWORD in request.query and - hmac.compare_digest(api_password, - request.query[DATA_API_PASSWORD])): + hmac.compare_digest( + api_password.encode('utf-8'), + request.query[DATA_API_PASSWORD].encode('utf-8'))): authenticated = True elif (hdrs.AUTHORIZATION in request.headers and - validate_authorization_header(api_password, request)): + await async_validate_auth_header(api_password, request)): authenticated = True elif _is_trusted_ip(request, trusted_networks): @@ -70,23 +72,38 @@ def _is_trusted_ip(request, trusted_networks): def validate_password(request, api_password): """Test if password is valid.""" return hmac.compare_digest( - api_password, request.app['hass'].http.api_password) + api_password.encode('utf-8'), + request.app['hass'].http.api_password.encode('utf-8')) -def validate_authorization_header(api_password, request): +async def async_validate_auth_header(api_password, request): """Test an authorization header if valid password.""" if hdrs.AUTHORIZATION not in request.headers: return False - auth_type, auth = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) - if auth_type != 'Basic': + if auth_type == 'Basic': + decoded = base64.b64decode(auth_val).decode('utf-8') + try: + username, password = decoded.split(':', 1) + except ValueError: + # If no ':' in decoded + return False + + if username != 'homeassistant': + return False + + return hmac.compare_digest(api_password.encode('utf-8'), + password.encode('utf-8')) + + if auth_type != 'Bearer': return False - decoded = base64.b64decode(auth).decode('utf-8') - username, password = decoded.split(':', 1) - - if username != 'homeassistant': + hass = request.app['hass'] + access_token = hass.auth.async_get_access_token(auth_val) + if access_token is None: return False - return hmac.compare_digest(api_password, password) + request['hass_user'] = access_token.refresh_token.user + return True diff --git a/homeassistant/config.py b/homeassistant/config.py index 28936ae12e9..2c440485e49 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -12,13 +12,14 @@ from typing import Any, List, Tuple # NOQA import voluptuous as vol from voluptuous.humanize import humanize_error +from homeassistant import auth from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ASSUMED_STATE, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, CONF_UNIT_SYSTEM, CONF_TIME_ZONE, CONF_ELEVATION, CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, TEMP_CELSIUS, __version__, CONF_CUSTOMIZE, CONF_CUSTOMIZE_DOMAIN, CONF_CUSTOMIZE_GLOB, - CONF_WHITELIST_EXTERNAL_DIRS) + CONF_WHITELIST_EXTERNAL_DIRS, CONF_AUTH_PROVIDERS) from homeassistant.core import callback, DOMAIN as CONF_CORE from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import get_component, get_platform @@ -157,6 +158,8 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend({ # pylint: disable=no-value-for-parameter vol.All(cv.ensure_list, [vol.IsDir()]), vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA, + vol.Optional(CONF_AUTH_PROVIDERS): + vol.All(cv.ensure_list, [auth.AUTH_PROVIDER_SCHEMA]) }) @@ -394,6 +397,12 @@ async def async_process_ha_core_config(hass, config): This method is a coroutine. """ config = CORE_CONFIG_SCHEMA(config) + + # Only load auth during startup. + if not hasattr(hass, 'auth'): + hass.auth = await auth.auth_manager_from_config( + hass, config.get(CONF_AUTH_PROVIDERS, [])) + hac = hass.config def set_time_zone(time_zone_str): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c23d53f2735..1350cd7d76a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -260,7 +260,7 @@ class ConfigEntries: """Initialize the entry manager.""" self.hass = hass self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_save_entry) + hass, self._async_create_flow, self._async_finish_flow) self._hass_config = hass_config self._entries = None self._sched_save = None @@ -345,8 +345,8 @@ class ConfigEntries: return await entry.async_unload( self.hass, component=getattr(self.hass.components, component)) - async def _async_save_entry(self, result): - """Add an entry.""" + async def _async_finish_flow(self, result): + """Finish a config flow and add an entry.""" entry = ConfigEntry( version=result['version'], domain=result['handler'], diff --git a/homeassistant/const.py b/homeassistant/const.py index 43380d00a2d..2e96e2f29c0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -30,6 +30,7 @@ CONF_API_KEY = 'api_key' CONF_API_VERSION = 'api_version' CONF_AT = 'at' CONF_AUTHENTICATION = 'authentication' +CONF_AUTH_PROVIDERS = 'auth_providers' CONF_BASE = 'base' CONF_BEFORE = 'before' CONF_BELOW = 'below' diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 8eb18a3a7e7..e9580aba273 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -67,7 +67,7 @@ class FlowManager: return await self._async_handle_step(flow, step, data) async def async_configure(self, flow_id, user_input=None): - """Start or continue a configuration flow.""" + """Continue a configuration flow.""" flow = self._progress.get(flow_id) if flow is None: diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index a8aca2fd2e9..913e90a859d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -7,40 +7,40 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -def _prepare_json(result): - """Convert result for JSON.""" - if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - data = result.copy() - data.pop('result') - data.pop('data') - return data - - elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: - return result - - import voluptuous_serialize - - data = result.copy() - - schema = data['data_schema'] - if schema is None: - data['data_schema'] = [] - else: - data['data_schema'] = voluptuous_serialize.convert(schema) - - return data - - -class FlowManagerIndexView(HomeAssistantView): - """View to create config flows.""" +class _BaseFlowManagerView(HomeAssistantView): + """Foundation for flow manager views.""" def __init__(self, flow_mgr): """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr - async def get(self, request): - """List flows that are in progress.""" - return self.json(self._flow_mgr.async_progress()) + # pylint: disable=no-self-use + def _prepare_result_json(self, result): + """Convert result to JSON.""" + if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + data = result.copy() + data.pop('result') + data.pop('data') + return data + + elif result['type'] != data_entry_flow.RESULT_TYPE_FORM: + return result + + import voluptuous_serialize + + data = result.copy() + + schema = data['data_schema'] + if schema is None: + data['data_schema'] = [] + else: + data['data_schema'] = voluptuous_serialize.convert(schema) + + return data + + +class FlowManagerIndexView(_BaseFlowManagerView): + """View to create config flows.""" @RequestDataValidator(vol.Schema({ vol.Required('handler'): vol.Any(str, list), @@ -59,18 +59,14 @@ class FlowManagerIndexView(HomeAssistantView): except data_entry_flow.UnknownStep: return self.json_message('Handler does not support init', 400) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) -class FlowManagerResourceView(HomeAssistantView): +class FlowManagerResourceView(_BaseFlowManagerView): """View to interact with the flow manager.""" - def __init__(self, flow_mgr): - """Initialize the flow manager resource view.""" - self._flow_mgr = flow_mgr - async def get(self, request, flow_id): """Get the current state of a data_entry_flow.""" try: @@ -78,7 +74,7 @@ class FlowManagerResourceView(HomeAssistantView): except data_entry_flow.UnknownFlow: return self.json_message('Invalid flow specified', 404) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) @@ -92,7 +88,7 @@ class FlowManagerResourceView(HomeAssistantView): except vol.Invalid: return self.json_message('User input malformed', 400) - result = _prepare_json(result) + result = self._prepare_result_json(result) return self.json(result) diff --git a/pylintrc b/pylintrc index 85a44782af1..df839b379b5 100644 --- a/pylintrc +++ b/pylintrc @@ -41,3 +41,7 @@ disable= [EXCEPTIONS] overgeneral-exceptions=Exception,HomeAssistantError + +# For attrs +[typecheck] +ignored-classes=_CountingAttr diff --git a/tests/auth_providers/__init__.py b/tests/auth_providers/__init__.py new file mode 100644 index 00000000000..dd1b58639b1 --- /dev/null +++ b/tests/auth_providers/__init__.py @@ -0,0 +1 @@ +"""Tests for the auth providers.""" diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py new file mode 100644 index 00000000000..92fc2974e27 --- /dev/null +++ b/tests/auth_providers/test_insecure_example.py @@ -0,0 +1,89 @@ +"""Tests for the insecure example auth provider.""" +from unittest.mock import Mock +import uuid + +import pytest + +from homeassistant import auth +from homeassistant.auth_providers import insecure_example + +from tests.common import mock_coro + + +@pytest.fixture +def store(): + """Mock store.""" + return auth.AuthStore(Mock()) + + +@pytest.fixture +def provider(store): + """Mock provider.""" + return insecure_example.ExampleAuthProvider(store, { + 'type': 'insecure_example', + 'users': [ + { + 'username': 'user-test', + 'password': 'password-test', + }, + { + 'username': '🎉', + 'password': '😎', + } + ] + }) + + +async def test_create_new_credential(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'password-test', + }) + assert credentials.is_new is True + + +async def test_match_existing_credentials(store, provider): + """See if we match existing users.""" + existing = auth.Credentials( + id=uuid.uuid4(), + auth_provider_type='insecure_example', + auth_provider_id=None, + data={ + 'username': 'user-test' + }, + is_new=False, + ) + store.credentials_for_provider = Mock(return_value=mock_coro([existing])) + credentials = await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'password-test', + }) + assert credentials is existing + + +async def test_verify_username(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(auth.InvalidUser): + await provider.async_get_or_create_credentials({ + 'username': 'non-existing-user', + 'password': 'password-test', + }) + + +async def test_verify_password(provider): + """Test we raise if incorrect user specified.""" + with pytest.raises(auth.InvalidPassword): + await provider.async_get_or_create_credentials({ + 'username': 'user-test', + 'password': 'incorrect-password', + }) + + +async def test_utf_8_username_password(provider): + """Test that we create a new credential.""" + credentials = await provider.async_get_or_create_credentials({ + 'username': '🎉', + 'password': '😎', + }) + assert credentials.is_new is True diff --git a/tests/common.py b/tests/common.py index 67fd8bab23f..b04abda7c28 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,7 +10,8 @@ import logging import threading from contextlib import contextmanager -from homeassistant import core as ha, loader, data_entry_flow, config_entries +from homeassistant import ( + auth, core as ha, loader, data_entry_flow, config_entries) from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( @@ -113,6 +114,9 @@ def async_test_home_assistant(loop): hass.config_entries = config_entries.ConfigEntries(hass, {}) hass.config_entries._entries = [] hass.config.async_load = Mock() + store = auth.AuthStore(hass) + hass.auth = auth.AuthManager(hass, store, {}) + ensure_auth_manager_loaded(hass.auth) INSTANCES.append(hass) orig_async_add_job = hass.async_add_job @@ -303,6 +307,34 @@ def mock_registry(hass, mock_entries=None): return registry +class MockUser(auth.User): + """Mock a user in Home Assistant.""" + + def __init__(self, id='mock-id', is_owner=True, is_active=True, + name='Mock User'): + """Initialize mock user.""" + super().__init__(id, is_owner, is_active, name) + + def add_to_hass(self, hass): + """Test helper to add entry to hass.""" + return self.add_to_auth_manager(hass.auth) + + def add_to_auth_manager(self, auth_mgr): + """Test helper to add entry to hass.""" + auth_mgr._store.users[self.id] = self + return self + + +@ha.callback +def ensure_auth_manager_loaded(auth_mgr): + """Ensure an auth manager is considered loaded.""" + store = auth_mgr._store + if store.clients is None: + store.clients = {} + if store.users is None: + store.users = {} + + class MockModule(object): """Representation of a fake module.""" diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py new file mode 100644 index 00000000000..3e5a59e8386 --- /dev/null +++ b/tests/components/auth/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the auth component.""" +from aiohttp.helpers import BasicAuth + +from homeassistant import auth +from homeassistant.setup import async_setup_component + +from tests.common import ensure_auth_manager_loaded + + +BASE_CONFIG = [{ + 'name': 'Example', + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] +}] +CLIENT_ID = 'test-id' +CLIENT_SECRET = 'test-secret' +CLIENT_AUTH = BasicAuth(CLIENT_ID, CLIENT_SECRET) + + +async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, + setup_api=False): + """Helper to setup authentication and create a HTTP client.""" + hass.auth = await auth.auth_manager_from_config(hass, provider_configs) + ensure_auth_manager_loaded(hass.auth) + await async_setup_component(hass, 'auth', { + 'http': { + 'api_password': 'bla' + } + }) + client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET) + hass.auth._store.clients[client.id] = client + if setup_api: + await async_setup_component(hass, 'api', {}) + return await aiohttp_client(hass.http.app) diff --git a/tests/components/auth/test_client.py b/tests/components/auth/test_client.py new file mode 100644 index 00000000000..2995a6ac81a --- /dev/null +++ b/tests/components/auth/test_client.py @@ -0,0 +1,70 @@ +"""Tests for the client validator.""" +from aiohttp.helpers import BasicAuth +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.components.auth.client import verify_client +from homeassistant.components.http.view import HomeAssistantView + +from . import async_setup_auth + + +@pytest.fixture +def mock_view(hass): + """Register a view that verifies client id/secret.""" + hass.loop.run_until_complete(async_setup_component(hass, 'http', {})) + + clients = [] + + class ClientView(HomeAssistantView): + url = '/' + name = 'bla' + + @verify_client + async def get(self, request, client_id): + """Handle GET request.""" + clients.append(client_id) + + hass.http.register_view(ClientView) + return clients + + +async def test_verify_client(hass, aiohttp_client, mock_view): + """Test that verify client can extract client auth from a request.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth(client.id, client.secret)) + assert resp.status == 200 + assert mock_view == [client.id] + + +async def test_verify_client_no_auth_header(hass, aiohttp_client, mock_view): + """Test that verify client will decline unknown client id.""" + http_client = await async_setup_auth(hass, aiohttp_client) + + resp = await http_client.get('/') + assert resp.status == 401 + assert mock_view == [] + + +async def test_verify_client_invalid_client_id(hass, aiohttp_client, + mock_view): + """Test that verify client will decline unknown client id.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth('invalid', client.secret)) + assert resp.status == 401 + assert mock_view == [] + + +async def test_verify_client_invalid_client_secret(hass, aiohttp_client, + mock_view): + """Test that verify client will decline incorrect client secret.""" + http_client = await async_setup_auth(hass, aiohttp_client) + client = await hass.auth.async_create_client('Hello') + + resp = await http_client.get('/', auth=BasicAuth(client.id, 'invalid')) + assert resp.status == 401 + assert mock_view == [] diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py new file mode 100644 index 00000000000..5d9bf6b98cc --- /dev/null +++ b/tests/components/auth/test_init.py @@ -0,0 +1,53 @@ +"""Integration tests for the auth component.""" +from . import async_setup_auth, CLIENT_AUTH + + +async def test_login_new_user_and_refresh_token(hass, aiohttp_client): + """Test logging in with new user and refreshing tokens.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + code = step['result'] + + # Exchange code for tokens + resp = await client.post('/auth/token', data={ + 'grant_type': 'authorization_code', + 'code': code + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + # Use refresh token to get more tokens. + resp = await client.post('/auth/token', data={ + 'grant_type': 'refresh_token', + 'refresh_token': tokens['refresh_token'] + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + assert 'refresh_token' not in tokens + assert hass.auth.async_get_access_token(tokens['access_token']) is not None + + # Test using access token to hit API. + resp = await client.get('/api/') + assert resp.status == 401 + + resp = await client.get('/api/', headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + assert resp.status == 200 diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py new file mode 100644 index 00000000000..44695bce202 --- /dev/null +++ b/tests/components/auth/test_init_link_user.py @@ -0,0 +1,150 @@ +"""Tests for the link user flow.""" +from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID + + +async def async_get_code(hass, aiohttp_client): + """Helper for link user tests that returns authorization code.""" + config = [{ + 'name': 'Example', + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }, { + 'name': 'Example', + 'id': '2nd auth', + 'type': 'insecure_example', + 'users': [{ + 'username': '2nd-user', + 'password': '2nd-pass', + 'name': '2nd Name' + }] + }] + client = await async_setup_auth(hass, aiohttp_client, config) + + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + code = step['result'] + + # Exchange code for tokens + resp = await client.post('/auth/token', data={ + 'grant_type': 'authorization_code', + 'code': code + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + tokens = await resp.json() + + access_token = hass.auth.async_get_access_token(tokens['access_token']) + assert access_token is not None + user = access_token.refresh_token.user + assert len(user.credentials) == 1 + + # Now authenticate with the 2nd flow + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', '2nd auth'] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': '2nd-user', + 'password': '2nd-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + return { + 'user': user, + 'code': step['result'], + 'client': client, + 'tokens': tokens, + } + + +async def test_link_user(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': code + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 200 + assert len(info['user'].credentials) == 2 + + +async def test_link_user_invalid_client_id(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': 'invalid', + 'code': code + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 400 + assert len(info['user'].credentials) == 1 + + +async def test_link_user_invalid_code(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + tokens = info['tokens'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': 'invalid' + }, headers={ + 'authorization': 'Bearer {}'.format(tokens['access_token']) + }) + + assert resp.status == 400 + assert len(info['user'].credentials) == 1 + + +async def test_link_user_invalid_auth(hass, aiohttp_client): + """Test linking a user to new credentials.""" + info = await async_get_code(hass, aiohttp_client) + client = info['client'] + code = info['code'] + + # Link user + resp = await client.post('/auth/link_user', json={ + 'client_id': CLIENT_ID, + 'code': code, + }, headers={'authorization': 'Bearer invalid'}) + + assert resp.status == 401 + assert len(info['user'].credentials) == 1 diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_init_login_flow.py new file mode 100644 index 00000000000..96fece6506b --- /dev/null +++ b/tests/components/auth/test_init_login_flow.py @@ -0,0 +1,66 @@ +"""Tests for the login flow.""" +from aiohttp.helpers import BasicAuth + +from . import async_setup_auth, CLIENT_AUTH + + +async def test_fetch_auth_providers(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.get('/auth/providers', auth=CLIENT_AUTH) + assert await resp.json() == [{ + 'name': 'Example', + 'type': 'insecure_example', + 'id': None + }] + + +async def test_fetch_auth_providers_require_valid_client(hass, aiohttp_client): + """Test fetching auth providers.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.get('/auth/providers', + auth=BasicAuth('invalid', 'bla')) + assert resp.status == 401 + + +async def test_cannot_get_flows_in_progress(hass, aiohttp_client): + """Test we cannot get flows in progress.""" + client = await async_setup_auth(hass, aiohttp_client, []) + resp = await client.get('/auth/login_flow') + assert resp.status == 405 + + +async def test_invalid_username_password(hass, aiohttp_client): + """Test we cannot get flows in progress.""" + client = await async_setup_auth(hass, aiohttp_client) + resp = await client.post('/auth/login_flow', json={ + 'handler': ['insecure_example', None] + }, auth=CLIENT_AUTH) + assert resp.status == 200 + step = await resp.json() + + # Incorrect username + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'wrong-user', + 'password': 'test-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + assert step['step_id'] == 'init' + assert step['errors']['base'] == 'invalid_auth' + + # Incorrect password + resp = await client.post( + '/auth/login_flow/{}'.format(step['flow_id']), json={ + 'username': 'test-user', + 'password': 'wrong-pass', + }, auth=CLIENT_AUTH) + + assert resp.status == 200 + step = await resp.json() + + assert step['step_id'] == 'init' + assert step['errors']['base'] == 'invalid_auth' diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index c02e203444f..d5368032a37 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,6 @@ """The tests for the Home Assistant HTTP component.""" +import logging + from homeassistant.setup import async_setup_component import homeassistant.components.http as http @@ -76,14 +78,13 @@ async def test_api_no_base_url(hass): async def test_not_log_password(hass, aiohttp_client, caplog): """Test access with password doesn't get logged.""" - result = await async_setup_component(hass, 'api', { + assert await async_setup_component(hass, 'api', { 'http': { http.CONF_API_PASSWORD: 'some-pass' } }) - assert result - client = await aiohttp_client(hass.http.app) + logging.getLogger('aiohttp.access').setLevel(logging.INFO) resp = await client.get('/api/', params={ 'api_password': 'some-pass' diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 00000000000..4bbf218fd23 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,159 @@ +"""Tests for the Home Assistant auth module.""" +from unittest.mock import Mock + +import pytest + +from homeassistant import auth, data_entry_flow +from tests.common import MockUser, ensure_auth_manager_loaded + + +@pytest.fixture +def mock_hass(): + """Hass mock with minimum amount of data set to make it work with auth.""" + hass = Mock() + hass.config.skip_pip = True + return hass + + +async def test_auth_manager_from_config_validates_config_and_id(mock_hass): + """Test get auth providers.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'users': [], + }, { + 'name': 'Invalid config because no users', + 'type': 'insecure_example', + 'id': 'invalid_config', + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }, { + 'name': 'Wrong because duplicate ID', + 'type': 'insecure_example', + 'id': 'another', + 'users': [], + }]) + + providers = [{ + 'name': provider.name, + 'id': provider.id, + 'type': provider.type, + } for provider in manager.async_auth_providers] + assert providers == [{ + 'name': 'Test Name', + 'type': 'insecure_example', + 'id': None, + }, { + 'name': 'Test Name 2', + 'type': 'insecure_example', + 'id': 'another', + }] + + +async def test_create_new_user(mock_hass): + """Test creating new user.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + assert step['type'] == data_entry_flow.RESULT_TYPE_FORM + + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + credentials = step['result'] + user = await manager.async_get_or_create_user(credentials) + assert user is not None + assert user.is_owner is True + assert user.name == 'Test Name' + + +async def test_login_as_existing_user(mock_hass): + """Test login as existing user.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + 'name': 'Test Name' + }] + }]) + ensure_auth_manager_loaded(manager) + + # Add fake user with credentials for example auth provider. + user = MockUser( + id='mock-user', + is_owner=False, + is_active=False, + name='Paulus', + ).add_to_auth_manager(manager) + user.credentials.append(auth.Credentials( + id='mock-id', + auth_provider_type='insecure_example', + auth_provider_id=None, + data={'username': 'test-user'}, + is_new=False, + )) + + step = await manager.login_flow.async_init(('insecure_example', None)) + assert step['type'] == data_entry_flow.RESULT_TYPE_FORM + + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + assert step['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + credentials = step['result'] + + user = await manager.async_get_or_create_user(credentials) + assert user is not None + assert user.id == 'mock-user' + assert user.is_owner is False + assert user.is_active is False + assert user.name == 'Paulus' + + +async def test_linking_user_to_two_auth_providers(mock_hass): + """Test linking user to two auth providers.""" + manager = await auth.auth_manager_from_config(mock_hass, [{ + 'type': 'insecure_example', + 'users': [{ + 'username': 'test-user', + 'password': 'test-pass', + }] + }, { + 'type': 'insecure_example', + 'id': 'another-provider', + 'users': [{ + 'username': 'another-user', + 'password': 'another-password', + }] + }]) + + step = await manager.login_flow.async_init(('insecure_example', None)) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'test-user', + 'password': 'test-pass', + }) + user = await manager.async_get_or_create_user(step['result']) + assert user is not None + + step = await manager.login_flow.async_init(('insecure_example', + 'another-provider')) + step = await manager.login_flow.async_configure(step['flow_id'], { + 'username': 'another-user', + 'password': 'another-password', + }) + await manager.async_link_user(user, step['result']) + assert len(user.credentials) == 2 From d82693b4606360afaacfa6a995d94121d4b1cc97 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 May 2018 13:35:23 -0400 Subject: [PATCH 560/924] Allow easy extension of websocket API (#14186) * Allow easy extension of websocket API * Lint * Move panel test to frontend * Register websocket commands * Simplify test * Lint --- homeassistant/components/frontend/__init__.py | 24 +- homeassistant/components/websocket_api.py | 231 +++++++++--------- tests/components/conftest.py | 22 ++ tests/components/test_frontend.py | 24 ++ tests/components/test_websocket_api.py | 56 ++--- 5 files changed, 203 insertions(+), 154 deletions(-) create mode 100644 tests/components/conftest.py diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 4a181c00c02..564ba286b96 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -16,8 +16,9 @@ import voluptuous as vol import jinja2 import homeassistant.helpers.config_validation as cv -from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.http.const import KEY_AUTHENTICATED +from homeassistant.components import websocket_api from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback @@ -94,6 +95,10 @@ SERVICE_RELOAD_THEMES = 'reload_themes' SERVICE_SET_THEME_SCHEMA = vol.Schema({ vol.Required(CONF_NAME): cv.string, }) +WS_TYPE_GET_PANELS = 'get_panels' +SCHEMA_GET_PANELS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + vol.Required('type'): WS_TYPE_GET_PANELS, +}) class AbstractPanel: @@ -291,6 +296,8 @@ def add_manifest_json_key(key, val): @asyncio.coroutine def async_setup(hass, config): """Set up the serving of the frontend.""" + hass.components.websocket_api.async_register_command( + WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) hass.http.register_view(ManifestJSONView) conf = config.get(DOMAIN, {}) @@ -597,3 +604,18 @@ def _is_latest(js_option, request): useragent = request.headers.get('User-Agent') return useragent and hass_frontend.version(useragent) + + +def websocket_handle_get_panels(hass, connection, msg): + """Handle get panels command. + + Async friendly. + """ + panels = { + panel: + connection.hass.data[DATA_PANELS][panel].to_response( + connection.hass, connection.request) + for panel in connection.hass.data[DATA_PANELS]} + + connection.to_write.put_nowait(websocket_api.result_message( + msg['id'], panels)) diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 1e23ad19897..84c92631572 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -18,8 +18,8 @@ from voluptuous.humanize import humanize_error from homeassistant.const import ( MATCH_ALL, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, __version__) -from homeassistant.components import frontend from homeassistant.core import callback +from homeassistant.loader import bind_hass from homeassistant.remote import JSONEncoder from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_get_all_descriptions @@ -46,7 +46,6 @@ TYPE_AUTH_REQUIRED = 'auth_required' TYPE_CALL_SERVICE = 'call_service' TYPE_EVENT = 'event' TYPE_GET_CONFIG = 'get_config' -TYPE_GET_PANELS = 'get_panels' TYPE_GET_SERVICES = 'get_services' TYPE_GET_STATES = 'get_states' TYPE_PING = 'ping' @@ -64,62 +63,56 @@ AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('api_password'): str, }) -SUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({ +# Minimal requirements of a message +MINIMAL_MESSAGE_SCHEMA = vol.Schema({ vol.Required('id'): cv.positive_int, + vol.Required('type'): cv.string, +}, extra=vol.ALLOW_EXTRA) +# Base schema to extend by message handlers +BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ + vol.Required('id'): cv.positive_int, +}) + + +SCHEMA_SUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_SUBSCRIBE_EVENTS, vol.Optional('event_type', default=MATCH_ALL): str, }) -UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_UNSUBSCRIBE_EVENTS = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_UNSUBSCRIBE_EVENTS, vol.Required('subscription'): cv.positive_int, }) -CALL_SERVICE_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_CALL_SERVICE = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_CALL_SERVICE, vol.Required('domain'): str, vol.Required('service'): str, vol.Optional('service_data'): dict }) -GET_STATES_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_STATES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_STATES, }) -GET_SERVICES_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_SERVICES = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_SERVICES, }) -GET_CONFIG_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, + +SCHEMA_GET_CONFIG = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_GET_CONFIG, }) -GET_PANELS_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): TYPE_GET_PANELS, -}) -PING_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, +SCHEMA_PING = BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): TYPE_PING, }) -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({ - vol.Required('id'): cv.positive_int, - vol.Required('type'): vol.Any(TYPE_CALL_SERVICE, - TYPE_SUBSCRIBE_EVENTS, - TYPE_UNSUBSCRIBE_EVENTS, - TYPE_GET_STATES, - TYPE_GET_SERVICES, - TYPE_GET_CONFIG, - TYPE_GET_PANELS, - TYPE_PING) -}, extra=vol.ALLOW_EXTRA) # Define the possible errors that occur when connections are cancelled. # Originally, this was just asyncio.CancelledError, but issue #9546 showed @@ -191,9 +184,36 @@ def result_message(iden, result=None): } +@bind_hass +@callback +def async_register_command(hass, command, handler, schema): + """Register a websocket command.""" + handlers = hass.data.get(DOMAIN) + if handlers is None: + handlers = hass.data[DOMAIN] = {} + handlers[command] = (handler, schema) + + async def async_setup(hass, config): """Initialize the websocket API.""" hass.http.register_view(WebsocketAPIView) + + async_register_command(hass, TYPE_SUBSCRIBE_EVENTS, + handle_subscribe_events, SCHEMA_SUBSCRIBE_EVENTS) + async_register_command(hass, TYPE_UNSUBSCRIBE_EVENTS, + handle_unsubscribe_events, + SCHEMA_UNSUBSCRIBE_EVENTS) + async_register_command(hass, TYPE_CALL_SERVICE, + handle_call_service, SCHEMA_CALL_SERVICE) + async_register_command(hass, TYPE_GET_STATES, + handle_get_states, SCHEMA_GET_STATES) + async_register_command(hass, TYPE_GET_SERVICES, + handle_get_services, SCHEMA_GET_SERVICES) + async_register_command(hass, TYPE_GET_CONFIG, + handle_get_config, SCHEMA_GET_CONFIG) + async_register_command(hass, TYPE_PING, + handle_ping, SCHEMA_PING) + return True @@ -316,10 +336,11 @@ class ActiveConnection: msg = await wsock.receive_json() last_id = 0 + handlers = self.hass.data[DOMAIN] while msg: self.debug("Received", msg) - msg = BASE_COMMAND_MESSAGE_SCHEMA(msg) + msg = MINIMAL_MESSAGE_SCHEMA(msg) cur_id = msg['id'] if cur_id <= last_id: @@ -327,9 +348,13 @@ class ActiveConnection: cur_id, ERR_ID_REUSE, 'Identifier values have to increase.')) + elif msg['type'] not in handlers: + # Unknown command + break + else: - handler_name = 'handle_{}'.format(msg['type']) - getattr(self, handler_name)(msg) + handler, schema = handlers[msg['type']] + handler(self.hass, self, schema(msg)) last_id = cur_id msg = await wsock.receive_json() @@ -403,109 +428,89 @@ class ActiveConnection: return wsock - def handle_subscribe_events(self, msg): - """Handle subscribe events command. - Async friendly. - """ - msg = SUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) +def handle_subscribe_events(hass, connection, msg): + """Handle subscribe events command. - async def forward_events(event): - """Forward events to websocket.""" - if event.event_type == EVENT_TIME_CHANGED: - return + Async friendly. + """ + async def forward_events(event): + """Forward events to websocket.""" + if event.event_type == EVENT_TIME_CHANGED: + return - self.send_message_outside(event_message(msg['id'], event)) + connection.send_message_outside(event_message(msg['id'], event)) - self.event_listeners[msg['id']] = self.hass.bus.async_listen( - msg['event_type'], forward_events) + connection.event_listeners[msg['id']] = hass.bus.async_listen( + msg['event_type'], forward_events) - self.to_write.put_nowait(result_message(msg['id'])) + connection.to_write.put_nowait(result_message(msg['id'])) - def handle_unsubscribe_events(self, msg): - """Handle unsubscribe events command. - Async friendly. - """ - msg = UNSUBSCRIBE_EVENTS_MESSAGE_SCHEMA(msg) +def handle_unsubscribe_events(hass, connection, msg): + """Handle unsubscribe events command. - subscription = msg['subscription'] + Async friendly. + """ + subscription = msg['subscription'] - if subscription in self.event_listeners: - self.event_listeners.pop(subscription)() - self.to_write.put_nowait(result_message(msg['id'])) - else: - self.to_write.put_nowait(error_message( - msg['id'], ERR_NOT_FOUND, - 'Subscription not found.')) + if subscription in connection.event_listeners: + connection.event_listeners.pop(subscription)() + connection.to_write.put_nowait(result_message(msg['id'])) + else: + connection.to_write.put_nowait(error_message( + msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) - def handle_call_service(self, msg): - """Handle call service command. - Async friendly. - """ - msg = CALL_SERVICE_MESSAGE_SCHEMA(msg) +def handle_call_service(hass, connection, msg): + """Handle call service command. - async def call_service_helper(msg): - """Call a service and fire complete message.""" - await self.hass.services.async_call( - msg['domain'], msg['service'], msg.get('service_data'), True) - self.send_message_outside(result_message(msg['id'])) + Async friendly. + """ + async def call_service_helper(msg): + """Call a service and fire complete message.""" + await hass.services.async_call( + msg['domain'], msg['service'], msg.get('service_data'), True) + connection.send_message_outside(result_message(msg['id'])) - self.hass.async_add_job(call_service_helper(msg)) + hass.async_add_job(call_service_helper(msg)) - def handle_get_states(self, msg): - """Handle get states command. - Async friendly. - """ - msg = GET_STATES_MESSAGE_SCHEMA(msg) +def handle_get_states(hass, connection, msg): + """Handle get states command. - self.to_write.put_nowait(result_message( - msg['id'], self.hass.states.async_all())) + Async friendly. + """ + connection.to_write.put_nowait(result_message( + msg['id'], hass.states.async_all())) - def handle_get_services(self, msg): - """Handle get services command. - Async friendly. - """ - msg = GET_SERVICES_MESSAGE_SCHEMA(msg) +def handle_get_services(hass, connection, msg): + """Handle get services command. - async def get_services_helper(msg): - """Get available services and fire complete message.""" - descriptions = await async_get_all_descriptions(self.hass) - self.send_message_outside(result_message(msg['id'], descriptions)) + Async friendly. + """ + async def get_services_helper(msg): + """Get available services and fire complete message.""" + descriptions = await async_get_all_descriptions(hass) + connection.send_message_outside( + result_message(msg['id'], descriptions)) - self.hass.async_add_job(get_services_helper(msg)) + hass.async_add_job(get_services_helper(msg)) - def handle_get_config(self, msg): - """Handle get config command. - Async friendly. - """ - msg = GET_CONFIG_MESSAGE_SCHEMA(msg) +def handle_get_config(hass, connection, msg): + """Handle get config command. - self.to_write.put_nowait(result_message( - msg['id'], self.hass.config.as_dict())) + Async friendly. + """ + connection.to_write.put_nowait(result_message( + msg['id'], hass.config.as_dict())) - def handle_get_panels(self, msg): - """Handle get panels command. - Async friendly. - """ - msg = GET_PANELS_MESSAGE_SCHEMA(msg) - panels = { - panel: - self.hass.data[frontend.DATA_PANELS][panel].to_response( - self.hass, self.request) - for panel in self.hass.data[frontend.DATA_PANELS]} +def handle_ping(hass, connection, msg): + """Handle ping command. - self.to_write.put_nowait(result_message( - msg['id'], panels)) - - def handle_ping(self, msg): - """Handle ping command. - - Async friendly. - """ - self.to_write.put_nowait(pong_message(msg['id'])) + Async friendly. + """ + connection.to_write.put_nowait(pong_message(msg['id'])) diff --git a/tests/components/conftest.py b/tests/components/conftest.py new file mode 100644 index 00000000000..53caeb80783 --- /dev/null +++ b/tests/components/conftest.py @@ -0,0 +1,22 @@ +"""Fixtures for component testing.""" +import pytest + +from homeassistant.setup import async_setup_component + + +@pytest.fixture +def hass_ws_client(aiohttp_client): + """Websocket client fixture connected to websocket server.""" + async def create_client(hass): + """Create a websocket client.""" + wapi = hass.components.websocket_api + assert await async_setup_component(hass, 'websocket_api') + + client = await aiohttp_client(hass.http.app) + websocket = await client.ws_connect(wapi.URL) + auth_ok = await websocket.receive_json() + assert auth_ok['type'] == wapi.TYPE_AUTH_OK + + return websocket + + return create_client diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index c742e215738..973544495d7 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -9,6 +9,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.frontend import ( DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL, CONF_EXTRA_HTML_URL_ES5, DATA_PANELS) +from homeassistant.components import websocket_api as wapi @pytest.fixture @@ -189,3 +190,26 @@ def test_panel_without_path(hass): 'test_component', 'nonexistant_file') yield from async_setup_component(hass, 'frontend', {}) assert 'test_component' not in hass.data[DATA_PANELS] + + +async def test_get_panels(hass, hass_ws_client): + """Test get_panels command.""" + await async_setup_component(hass, 'frontend') + await hass.components.frontend.async_register_built_in_panel( + 'map', 'Map', 'mdi:account-location') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'get_panels', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == wapi.TYPE_RESULT + assert msg['success'] + assert msg['result']['map']['component_name'] == 'map' + assert msg['result']['map']['url_path'] == 'map' + assert msg['result']['map']['icon'] == 'mdi:account-location' + assert msg['result']['map']['title'] == 'Map' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 4deccf65209..0a130e507d4 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -7,7 +7,7 @@ from async_timeout import timeout import pytest from homeassistant.core import callback -from homeassistant.components import websocket_api as wapi, frontend +from homeassistant.components import websocket_api as wapi from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -16,20 +16,9 @@ API_PASSWORD = 'test1234' @pytest.fixture -def websocket_client(loop, hass, aiohttp_client): - """Websocket client fixture connected to websocket server.""" - assert loop.run_until_complete( - async_setup_component(hass, 'websocket_api')) - - client = loop.run_until_complete(aiohttp_client(hass.http.app)) - ws = loop.run_until_complete(client.ws_connect(wapi.URL)) - auth_ok = loop.run_until_complete(ws.receive_json()) - assert auth_ok['type'] == wapi.TYPE_AUTH_OK - - yield ws - - if not ws.closed: - loop.run_until_complete(ws.close()) +def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return hass.loop.run_until_complete(hass_ws_client(hass)) @pytest.fixture @@ -289,31 +278,6 @@ def test_get_config(hass, websocket_client): assert msg['result'] == hass.config.as_dict() -@asyncio.coroutine -def test_get_panels(hass, websocket_client): - """Test get_panels command.""" - yield from hass.components.frontend.async_register_built_in_panel( - 'map', 'Map', 'mdi:account-location') - hass.data[frontend.DATA_JS_VERSION] = 'es5' - yield from websocket_client.send_json({ - 'id': 5, - 'type': wapi.TYPE_GET_PANELS, - }) - - msg = yield from websocket_client.receive_json() - assert msg['id'] == 5 - assert msg['type'] == wapi.TYPE_RESULT - assert msg['success'] - assert msg['result'] == {'map': { - 'component_name': 'map', - 'url_path': 'map', - 'config': None, - 'url': None, - 'icon': 'mdi:account-location', - 'title': 'Map', - }} - - @asyncio.coroutine def test_ping(websocket_client): """Test get_panels command.""" @@ -337,3 +301,15 @@ def test_pending_msg_overflow(hass, mock_low_queue, websocket_client): }) msg = yield from websocket_client.receive() assert msg.type == WSMsgType.close + + +@asyncio.coroutine +def test_unknown_command(websocket_client): + """Test get_panels command.""" + yield from websocket_client.send_json({ + 'id': 5, + 'type': 'unknown_command', + }) + + msg = yield from websocket_client.receive() + assert msg.type == WSMsgType.close From e78497789b5ec7f87dc1d956e2a873ed7c9f84f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B6rrle?= Date: Tue, 1 May 2018 20:13:35 +0200 Subject: [PATCH 561/924] Change the divisor for total consumption output (#14215) According to my observations, the "switch_energy" value displayed by Pyfritzhome is the sum of Wh over the last week since measurement. As a result, the correct divisor for representing output as kWh would be 1000 instead of 10000. --- homeassistant/components/switch/fritzbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/fritzbox.py b/homeassistant/components/switch/fritzbox.py index c8313b0dfef..65a1aa6aabc 100755 --- a/homeassistant/components/switch/fritzbox.py +++ b/homeassistant/components/switch/fritzbox.py @@ -87,7 +87,7 @@ class FritzboxSwitch(SwitchDevice): if self._device.has_powermeter: attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format( - (self._device.energy or 0.0) / 100000) + (self._device.energy or 0.0) / 1000) attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = \ ATTR_TOTAL_CONSUMPTION_UNIT_VALUE if self._device.has_temperature_sensor: From b0cccbfd9f2fc4e7699303e672bd41b98be3123a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 1 May 2018 20:14:28 +0200 Subject: [PATCH 562/924] Upgrade mypy to 0.590 (#14207) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 38b716406fd..6d5f68615be 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.580 +mypy==0.590 pydocstyle==1.1.1 pylint==1.8.3 pytest-aiohttp==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5835392c4a..0605b3d2e24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ coveralls==1.2.0 flake8-docstrings==1.0.3 flake8==3.5 mock-open==1.3.1 -mypy==0.580 +mypy==0.590 pydocstyle==1.1.1 pylint==1.8.3 pytest-aiohttp==0.3.0 From 9bc8f6649b1c7dd8da23cafced0a8502a6d11864 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 1 May 2018 20:32:44 +0200 Subject: [PATCH 563/924] Template Sensor add device_class support (#14034) * Template Sensor Device Class Support * Lint * Add tests --- homeassistant/components/sensor/template.py | 19 ++++++++--- tests/components/sensor/test_template.py | 37 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 1cd43262513..65f49998dbf 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -6,16 +6,18 @@ https://home-assistant.io/components/sensor.template/ """ import asyncio import logging +from typing import Optional import voluptuous as vol from homeassistant.core import callback -from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA +from homeassistant.components.sensor import ENTITY_ID_FORMAT, \ + PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, ATTR_ENTITY_ID, CONF_SENSORS, EVENT_HOMEASSISTANT_START, CONF_FRIENDLY_NAME_TEMPLATE, - MATCH_ALL) + MATCH_ALL, CONF_DEVICE_CLASS) from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -30,6 +32,7 @@ SENSOR_SCHEMA = vol.Schema({ vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) @@ -52,6 +55,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) + device_class = device_config.get(CONF_DEVICE_CLASS) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) @@ -86,7 +90,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): state_template, icon_template, entity_picture_template, - entity_ids) + entity_ids, + device_class) ) if not sensors: _LOGGER.error("No sensors added") @@ -101,7 +106,7 @@ class SensorTemplate(Entity): def __init__(self, hass, device_id, friendly_name, friendly_name_template, unit_of_measurement, state_template, icon_template, - entity_picture_template, entity_ids): + entity_picture_template, entity_ids, device_class): """Initialize the sensor.""" self.hass = hass self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, device_id, @@ -116,6 +121,7 @@ class SensorTemplate(Entity): self._icon = None self._entity_picture = None self._entities = entity_ids + self._device_class = device_class @asyncio.coroutine def async_added_to_hass(self): @@ -151,6 +157,11 @@ class SensorTemplate(Entity): """Return the icon to use in the frontend, if any.""" return self._icon + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + @property def entity_picture(self): """Return the entity_picture to use in the frontend, if any.""" diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index b05fc90bfe4..f8d912f24dd 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -267,3 +267,40 @@ class TestTemplateSensor: self.hass.block_till_done() assert self.hass.states.all() == [] + + def test_setup_invalid_device_class(self): + """"Test setup with invalid device_class.""" + with assert_setup_component(0): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test': { + 'value_template': '{{ foo }}', + 'device_class': 'foobarnotreal', + }, + }, + } + }) + + def test_setup_valid_device_class(self): + """"Test setup with valid device_class.""" + with assert_setup_component(1): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'template', + 'sensors': { + 'test1': { + 'value_template': '{{ foo }}', + 'device_class': 'temperature', + }, + 'test2': {'value_template': '{{ foo }}'}, + } + } + }) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test1') + assert state.attributes['device_class'] == 'temperature' + state = self.hass.states.get('sensor.test2') + assert 'device_class' not in state.attributes From b00f771541f9982cbbe9e684f2ed8c9068ac428e Mon Sep 17 00:00:00 2001 From: Ruben Date: Tue, 1 May 2018 20:40:48 +0200 Subject: [PATCH 564/924] Add more parameters for DSMR sensor (#13967) * Add more parameters for DSMR component * Add suiting icon for power failure * Add suiting icon for swells & sags * Fix tab indentation -> spaces * Fix too long lines (PEP8) --- homeassistant/components/sensor/dsmr.py | 93 +++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/dsmr.py b/homeassistant/components/sensor/dsmr.py index cea29d437ae..d7982f1c9db 100644 --- a/homeassistant/components/sensor/dsmr.py +++ b/homeassistant/components/sensor/dsmr.py @@ -31,6 +31,8 @@ DOMAIN = 'dsmr' ICON_GAS = 'mdi:fire' ICON_POWER = 'mdi:flash' +ICON_POWER_FAILURE = 'mdi:flash-off' +ICON_SWELL_SAG = 'mdi:pulse' # Smart meter sends telegram every 10 seconds MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) @@ -61,13 +63,86 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # Define list of name,obis mappings to generate entities obis_mapping = [ - ['Power Consumption', obis_ref.CURRENT_ELECTRICITY_USAGE], - ['Power Production', obis_ref.CURRENT_ELECTRICITY_DELIVERY], - ['Power Tariff', obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ['Power Consumption (low)', obis_ref.ELECTRICITY_USED_TARIFF_1], - ['Power Consumption (normal)', obis_ref.ELECTRICITY_USED_TARIFF_2], - ['Power Production (low)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], - ['Power Production (normal)', obis_ref.ELECTRICITY_DELIVERED_TARIFF_2], + [ + 'Power Consumption', + obis_ref.CURRENT_ELECTRICITY_USAGE + ], + [ + 'Power Production', + obis_ref.CURRENT_ELECTRICITY_DELIVERY + ], + [ + 'Power Tariff', + obis_ref.ELECTRICITY_ACTIVE_TARIFF + ], + [ + 'Power Consumption (low)', + obis_ref.ELECTRICITY_USED_TARIFF_1 + ], + [ + 'Power Consumption (normal)', + obis_ref.ELECTRICITY_USED_TARIFF_2 + ], + [ + 'Power Production (low)', + obis_ref.ELECTRICITY_DELIVERED_TARIFF_1 + ], + [ + 'Power Production (normal)', + obis_ref.ELECTRICITY_DELIVERED_TARIFF_2 + ], + [ + 'Power Consumption Phase L1', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE + ], + [ + 'Power Consumption Phase L2', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE + ], + [ + 'Power Consumption Phase L3', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE + ], + [ + 'Power Production Phase L1', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE + ], + [ + 'Power Production Phase L2', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE + ], + [ + 'Power Production Phase L3', + obis_ref.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE + ], + [ + 'Long Power Failure Count', + obis_ref.LONG_POWER_FAILURE_COUNT + ], + [ + 'Voltage Sags Phase L1', + obis_ref.VOLTAGE_SAG_L1_COUNT + ], + [ + 'Voltage Sags Phase L2', + obis_ref.VOLTAGE_SAG_L2_COUNT + ], + [ + 'Voltage Sags Phase L3', + obis_ref.VOLTAGE_SAG_L3_COUNT + ], + [ + 'Voltage Swells Phase L1', + obis_ref.VOLTAGE_SWELL_L1_COUNT + ], + [ + 'Voltage Swells Phase L2', + obis_ref.VOLTAGE_SWELL_L2_COUNT + ], + [ + 'Voltage Swells Phase L3', + obis_ref.VOLTAGE_SWELL_L3_COUNT + ], ] # Generate device entities @@ -174,6 +249,10 @@ class DSMREntity(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" + if 'Sags' in self._name or 'Swells' in self.name: + return ICON_SWELL_SAG + if 'Failure' in self._name: + return ICON_POWER_FAILURE if 'Power' in self._name: return ICON_POWER elif 'Gas' in self._name: From bf53cbe08d4908ab37063f35bdcb97cb682823b0 Mon Sep 17 00:00:00 2001 From: blackwind Date: Tue, 1 May 2018 12:41:36 -0600 Subject: [PATCH 565/924] Support setting explicit mute value for Panasonic Viera TV (#13954) * Use module's methods instead of API calls * Use module's methods instead of API calls for media commands --- .../components/media_player/panasonic_viera.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 39e5f81b71d..4a25fa1bf67 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -138,20 +138,20 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def turn_off(self): """Turn off media player.""" if self._state != STATE_OFF: - self.send_key('NRC_POWER-ONOFF') + self._remote.turn_off() self._state = STATE_OFF def volume_up(self): """Volume up the media player.""" - self.send_key('NRC_VOLUP-ONOFF') + self._remote.volume_up() def volume_down(self): """Volume down media player.""" - self.send_key('NRC_VOLDOWN-ONOFF') + self._remote.volume_down() def mute_volume(self, mute): """Send mute command.""" - self.send_key('NRC_MUTE-ONOFF') + self._remote.set_mute(mute) def set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -172,20 +172,20 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def media_play(self): """Send play command.""" self._playing = True - self.send_key('NRC_PLAY-ONOFF') + self._remote.media_play() def media_pause(self): """Send media pause command to media player.""" self._playing = False - self.send_key('NRC_PAUSE-ONOFF') + self._remote.media_pause() def media_next_track(self): """Send next track command.""" - self.send_key('NRC_FF-ONOFF') + self._remote.media_next_track() def media_previous_track(self): """Send the previous track command.""" - self.send_key('NRC_REW-ONOFF') + self._remote.media_previous_track() def play_media(self, media_type, media_id, **kwargs): """Play media.""" From 38560cda1c1be7e800f2956903eab4a1f7ce8072 Mon Sep 17 00:00:00 2001 From: NovapaX Date: Tue, 1 May 2018 20:49:33 +0200 Subject: [PATCH 566/924] Allow to set a desired update interval for camera_proxy_stream view (#13350) * allow to set a desired update interval for camera_proxy_stream view * lint * refactor into a seperate method. Keep the handle_async_mjpeg_stream method to be overridden by platforms so they can keep proxying the direct streams from the camera * change descriptions * consolidate * lint * travis * async/await and force min stream interval for fallback stream. * guard clause. Let the method raise error on interval. * is is not = * what to except when you're excepting * raise ValueError, remove unnecessary 500 response --- homeassistant/components/camera/__init__.py | 53 +++++++++++++++------ 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5321ec3d860..1fa89bc2241 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -53,6 +53,9 @@ ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}' TOKEN_CHANGE_INTERVAL = timedelta(minutes=5) _RND = SystemRandom() +FALLBACK_STREAM_INTERVAL = 1 # seconds +MIN_STREAM_INTERVAL = 0.5 # seconds + CAMERA_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -252,19 +255,21 @@ class Camera(Entity): """ return self.hass.async_add_job(self.camera_image) - @asyncio.coroutine - def handle_async_mjpeg_stream(self, request): + async def handle_async_still_stream(self, request, interval): """Generate an HTTP MJPEG stream from camera images. This method must be run in the event loop. """ - response = web.StreamResponse() + if interval < MIN_STREAM_INTERVAL: + raise ValueError("Stream interval must be be > {}" + .format(MIN_STREAM_INTERVAL)) + response = web.StreamResponse() response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--frameboundary') - yield from response.prepare(request) + await response.prepare(request) - async def write(img_bytes): + async def write_to_mjpeg_stream(img_bytes): """Write image to stream.""" await response.write(bytes( '--frameboundary\r\n' @@ -277,21 +282,21 @@ class Camera(Entity): try: while True: - img_bytes = yield from self.async_camera_image() + img_bytes = await self.async_camera_image() if not img_bytes: break if img_bytes and img_bytes != last_image: - yield from write(img_bytes) + await write_to_mjpeg_stream(img_bytes) # Chrome seems to always ignore first picture, # print it twice. if last_image is None: - yield from write(img_bytes) + await write_to_mjpeg_stream(img_bytes) last_image = img_bytes - yield from asyncio.sleep(.5) + await asyncio.sleep(interval) except asyncio.CancelledError: _LOGGER.debug("Stream closed by frontend.") @@ -299,7 +304,17 @@ class Camera(Entity): finally: if response is not None: - yield from response.write_eof() + await response.write_eof() + + async def handle_async_mjpeg_stream(self, request): + """Serve an HTTP MJPEG stream from the camera. + + This method can be overridden by camera plaforms to proxy + a direct stream from the camera. + This method must be run in the event loop. + """ + await self.handle_async_still_stream(request, + FALLBACK_STREAM_INTERVAL) @property def state(self): @@ -411,7 +426,17 @@ class CameraMjpegStream(CameraView): url = '/api/camera_proxy_stream/{entity_id}' name = 'api:camera:stream' - @asyncio.coroutine - def handle(self, request, camera): - """Serve camera image.""" - yield from camera.handle_async_mjpeg_stream(request) + async def handle(self, request, camera): + """Serve camera stream, possibly with interval.""" + interval = request.query.get('interval') + if interval is None: + await camera.handle_async_mjpeg_stream(request) + return + + try: + # Compose camera stream from stills + interval = float(request.query.get('interval')) + await camera.handle_async_still_stream(request, interval) + return + except ValueError: + return web.Response(status=400) From 5d96751168dc2b9a9dc43ed338d35045a113fa75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 1 May 2018 20:54:06 +0200 Subject: [PATCH 567/924] panasonic_viera: Provide unique_id from SSDP UDN, if available (#13541) --- .../components/media_player/panasonic_viera.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/panasonic_viera.py b/homeassistant/components/media_player/panasonic_viera.py index 4a25fa1bf67..db60de922d9 100644 --- a/homeassistant/components/media_player/panasonic_viera.py +++ b/homeassistant/components/media_player/panasonic_viera.py @@ -56,8 +56,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = discovery_info.get('name') host = discovery_info.get('host') port = discovery_info.get('port') + udn = discovery_info.get('udn') + if udn and udn.startswith('uuid:'): + uuid = udn[len('uuid:'):] + else: + uuid = None remote = RemoteControl(host, port) - add_devices([PanasonicVieraTVDevice(mac, name, remote)]) + add_devices([PanasonicVieraTVDevice(mac, name, remote, uuid)]) return True host = config.get(CONF_HOST) @@ -70,19 +75,25 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class PanasonicVieraTVDevice(MediaPlayerDevice): """Representation of a Panasonic Viera TV.""" - def __init__(self, mac, name, remote): + def __init__(self, mac, name, remote, uuid=None): """Initialize the Panasonic device.""" import wakeonlan # Save a reference to the imported class self._wol = wakeonlan self._mac = mac self._name = name + self._uuid = uuid self._muted = False self._playing = True self._state = STATE_UNKNOWN self._remote = remote self._volume = 0 + @property + def unique_id(self) -> str: + """Return the unique ID of this Viera TV.""" + return self._uuid + def update(self): """Retrieve the latest data.""" try: From 83d300fd11e95aaf802e062d14ca0d67b10bb566 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 1 May 2018 14:57:30 -0400 Subject: [PATCH 568/924] Custom component loading cleanup (#14211) * Clean up custom component loading * Fix some tests * Fix some stuff * Make imports work again * Fix tests * Remove debug print * Lint --- homeassistant/bootstrap.py | 8 +- .../components/automation/__init__.py | 16 +- .../components/binary_sensor/bloomsky.py | 3 +- .../components/binary_sensor/netatmo.py | 3 +- .../components/binary_sensor/wemo.py | 7 +- homeassistant/components/camera/bloomsky.py | 3 +- homeassistant/components/camera/netatmo.py | 3 +- homeassistant/components/climate/netatmo.py | 3 +- .../components/device_sun_light_trigger.py | 25 ++- .../components/device_tracker/__init__.py | 7 +- .../components/image_processing/__init__.py | 5 +- homeassistant/components/light/wemo.py | 3 +- homeassistant/components/microsoft_face.py | 3 +- homeassistant/components/mqtt_eventstream.py | 7 +- homeassistant/components/mysensors.py | 7 +- homeassistant/components/scene/__init__.py | 14 +- homeassistant/components/sensor/bloomsky.py | 3 +- homeassistant/components/sensor/netatmo.py | 3 +- homeassistant/components/zwave/__init__.py | 2 +- homeassistant/config.py | 9 +- homeassistant/helpers/config_validation.py | 14 -- homeassistant/helpers/service.py | 8 +- homeassistant/helpers/template.py | 6 +- homeassistant/helpers/translation.py | 6 +- homeassistant/loader.py | 201 +++++++----------- homeassistant/scripts/check_config.py | 28 +-- homeassistant/setup.py | 8 +- tests/common.py | 6 +- .../climate/test_generic_thermostat.py | 2 +- .../components/config/test_config_entries.py | 12 +- .../components/device_tracker/test_asuswrt.py | 2 +- tests/components/device_tracker/test_init.py | 10 +- tests/components/light/test_init.py | 6 +- tests/components/scene/test_init.py | 2 +- tests/components/switch/test_flux.py | 32 +-- tests/components/switch/test_init.py | 6 +- .../test_device_sun_light_trigger.py | 4 +- tests/helpers/test_config_validation.py | 20 -- tests/helpers/test_discovery.py | 8 +- tests/helpers/test_entity_component.py | 30 +-- tests/helpers/test_entity_platform.py | 12 +- tests/helpers/test_service.py | 6 +- tests/helpers/test_translation.py | 6 +- tests/scripts/test_check_config.py | 3 - tests/test_config.py | 26 +-- tests/test_config_entries.py | 21 +- tests/test_loader.py | 31 +-- tests/test_requirements.py | 6 +- tests/test_setup.py | 49 +++-- .../custom_components/test_standalone.py | 2 +- 50 files changed, 315 insertions(+), 392 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 0abe5a7811e..826cc563e82 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -12,8 +12,7 @@ from typing import Any, Optional, Dict import voluptuous as vol from homeassistant import ( - core, config as conf_util, config_entries, loader, - components as core_components) + core, config as conf_util, config_entries, components as core_components) from homeassistant.components import persistent_notification from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.setup import async_setup_component @@ -103,15 +102,12 @@ async def async_from_config_dict(config: Dict[str, Any], _LOGGER.warning("Skipping pip installation of required modules. " "This may cause issues") - if not loader.PREPARED: - await hass.async_add_job(loader.prepare, hass) - # Make a copy because we are mutating it. config = OrderedDict(config) # Merge packages conf_util.merge_packages_config( - config, core_config.get(conf_util.CONF_PACKAGES, {})) + hass, config, core_config.get(conf_util.CONF_PACKAGES, {})) # Ensure we have no None values after merge for key, value in config.items(): diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 8c490754f40..2f510fd33d6 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/automation/ """ import asyncio from functools import partial +import importlib import logging import voluptuous as vol @@ -22,7 +23,6 @@ from homeassistant.helpers import extract_domain_configs, script, condition from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state -from homeassistant.loader import get_platform from homeassistant.util.dt import utcnow import homeassistant.helpers.config_validation as cv @@ -58,12 +58,14 @@ _LOGGER = logging.getLogger(__name__) def _platform_validator(config): """Validate it is a valid platform.""" - platform = get_platform(DOMAIN, config[CONF_PLATFORM]) + try: + platform = importlib.import_module( + 'homeassistant.components.automation.{}'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None - if not hasattr(platform, 'TRIGGER_SCHEMA'): - return config - - return getattr(platform, 'TRIGGER_SCHEMA')(config) + return platform.TRIGGER_SCHEMA(config) _TRIGGER_SCHEMA = vol.All( @@ -71,7 +73,7 @@ _TRIGGER_SCHEMA = vol.All( [ vol.All( vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + vol.Required(CONF_PLATFORM): str }, extra=vol.ALLOW_EXTRA), _platform_validator ), diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index 53f148fe97f..3080cc65532 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,7 +30,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather binary sensors.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky # Default needed in case of discovery sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 7997e4e60db..fd0e30ccebc 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -13,7 +13,6 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.components.netatmo import CameraData -from homeassistant.loader import get_component from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv @@ -61,7 +60,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the access to Netatmo binary sensor.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo home = config.get(CONF_HOME) timeout = config.get(CONF_TIMEOUT) if timeout is None: diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index cc1f602d871..30a7e291401 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -7,7 +7,6 @@ https://home-assistant.io/components/binary_sensor.wemo/ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.loader import get_component DEPENDENCIES = ['wemo'] @@ -25,18 +24,18 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): device = discovery.device_from_description(location, mac) if device: - add_devices_callback([WemoBinarySensor(device)]) + add_devices_callback([WemoBinarySensor(hass, device)]) class WemoBinarySensor(BinarySensorDevice): """Representation a WeMo binary sensor.""" - def __init__(self, device): + def __init__(self, hass, device): """Initialize the WeMo sensor.""" self.wemo = device self._state = None - wemo = get_component('wemo') + wemo = hass.components.wemo wemo.SUBSCRIPTION_REGISTRY.register(self.wemo) wemo.SUBSCRIPTION_REGISTRY.on(self.wemo, None, self._update_callback) diff --git a/homeassistant/components/camera/bloomsky.py b/homeassistant/components/camera/bloomsky.py index c3b4775b593..ef70692215d 100644 --- a/homeassistant/components/camera/bloomsky.py +++ b/homeassistant/components/camera/bloomsky.py @@ -9,7 +9,6 @@ import logging import requests from homeassistant.components.camera import Camera -from homeassistant.loader import get_component DEPENDENCIES = ['bloomsky'] @@ -17,7 +16,7 @@ DEPENDENCIES = ['bloomsky'] # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to BloomSky cameras.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky for device in bloomsky.BLOOMSKY.devices.values(): add_devices([BloomSkyCamera(bloomsky.BLOOMSKY, device)]) diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 48f2710ce2e..bf2dfe39bd8 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -12,7 +12,6 @@ import voluptuous as vol from homeassistant.const import CONF_VERIFY_SSL from homeassistant.components.netatmo import CameraData from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) -from homeassistant.loader import get_component from homeassistant.helpers import config_validation as cv DEPENDENCIES = ['netatmo'] @@ -33,7 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up access to Netatmo cameras.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) import lnetatmo diff --git a/homeassistant/components/climate/netatmo.py b/homeassistant/components/climate/netatmo.py index 5d54b39e773..49452662fc4 100644 --- a/homeassistant/components/climate/netatmo.py +++ b/homeassistant/components/climate/netatmo.py @@ -13,7 +13,6 @@ from homeassistant.components.climate import ( STATE_HEAT, STATE_IDLE, ClimateDevice, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_AWAY_MODE) from homeassistant.util import Throttle -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['netatmo'] @@ -42,7 +41,7 @@ SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the NetAtmo Thermostat.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo device = config.get(CONF_RELAY) import lnetatmo diff --git a/homeassistant/components/device_sun_light_trigger.py b/homeassistant/components/device_sun_light_trigger.py index a1297c5c118..641ade7308b 100644 --- a/homeassistant/components/device_sun_light_trigger.py +++ b/homeassistant/components/device_sun_light_trigger.py @@ -16,7 +16,6 @@ from homeassistant.const import STATE_HOME, STATE_NOT_HOME from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change) from homeassistant.helpers.sun import is_up, get_astral_event_next -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv DOMAIN = 'device_sun_light_trigger' @@ -48,9 +47,9 @@ CONFIG_SCHEMA = vol.Schema({ def async_setup(hass, config): """Set up the triggers to control lights based on device presence.""" logger = logging.getLogger(__name__) - device_tracker = get_component('device_tracker') - group = get_component('group') - light = get_component('light') + device_tracker = hass.components.device_tracker + group = hass.components.group + light = hass.components.light conf = config[DOMAIN] disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) @@ -58,14 +57,14 @@ def async_setup(hass, config): device_group = conf.get( CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids( - hass, device_group, device_tracker.DOMAIN) + device_group, device_tracker.DOMAIN) if not device_entity_ids: logger.error("No devices found to track") return False # Get the light IDs from the specified group - light_ids = group.get_entity_ids(hass, light_group, light.DOMAIN) + light_ids = group.get_entity_ids(light_group, light.DOMAIN) if not light_ids: logger.error("No lights found to turn on") @@ -85,9 +84,9 @@ def async_setup(hass, config): def async_turn_on_before_sunset(light_id): """Turn on lights.""" - if not device_tracker.is_on(hass) or light.is_on(hass, light_id): + if not device_tracker.is_on() or light.is_on(light_id): return - light.async_turn_on(hass, light_id, + light.async_turn_on(light_id, transition=LIGHT_TRANSITION_TIME.seconds, profile=light_profile) @@ -129,7 +128,7 @@ def async_setup(hass, config): @callback def check_light_on_dev_state_change(entity, old_state, new_state): """Handle tracked device state changes.""" - lights_are_on = group.is_on(hass, light_group) + lights_are_on = group.is_on(light_group) light_needed = not (lights_are_on or is_up(hass)) # These variables are needed for the elif check @@ -139,7 +138,7 @@ def async_setup(hass, config): # Do we need lights? if light_needed: logger.info("Home coming event for %s. Turning lights on", entity) - light.async_turn_on(hass, light_ids, profile=light_profile) + light.async_turn_on(light_ids, profile=light_profile) # Are we in the time span were we would turn on the lights # if someone would be home? @@ -152,7 +151,7 @@ def async_setup(hass, config): # when the fading in started and turn it on if so for index, light_id in enumerate(light_ids): if now > start_point + index * LIGHT_TRANSITION_TIME: - light.async_turn_on(hass, light_id) + light.async_turn_on(light_id) else: # If this light didn't happen to be turned on yet so @@ -169,12 +168,12 @@ def async_setup(hass, config): @callback def turn_off_lights_when_all_leave(entity, old_state, new_state): """Handle device group state change.""" - if not group.is_on(hass, light_group): + if not group.is_on(light_group): return logger.info( "Everyone has left but there are lights on. Turning them off") - light.async_turn_off(hass, light_ids) + light.async_turn_off(light_ids) async_track_state_change( hass, device_group, turn_off_lights_when_all_leave, diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 2f068481953..e1dd52a28ea 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -24,7 +24,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import async_get_last_state from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType import homeassistant.helpers.config_validation as cv -from homeassistant.loader import get_component import homeassistant.util as util from homeassistant.util.async_ import run_coroutine_threadsafe import homeassistant.util.dt as dt_util @@ -322,7 +321,7 @@ class DeviceTracker(object): # During init, we ignore the group if self.group and self.track_new: self.group.async_set_group( - self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, add=[device.entity_id]) self.hass.bus.async_fire(EVENT_NEW_DEVICE, { @@ -357,9 +356,9 @@ class DeviceTracker(object): entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - self.group = get_component('group') + self.group = self.hass.components.group self.group.async_set_group( - self.hass, util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, + util.slugify(GROUP_NAME_ALL_DEVICES), visible=False, name=GROUP_NAME_ALL_DEVICES, entity_ids=entity_ids) @callback diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 061fd5d7074..de195ce0165 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -17,7 +17,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.loader import get_component _LOGGER = logging.getLogger(__name__) @@ -121,12 +120,12 @@ class ImageProcessingEntity(Entity): This method is a coroutine. """ - camera = get_component('camera') + camera = self.hass.components.camera image = None try: image = yield from camera.async_get_image( - self.hass, self.camera_entity, timeout=self.timeout) + self.camera_entity, timeout=self.timeout) except HomeAssistantError as err: _LOGGER.error("Error on receive image from entity: %s", err) diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index d0575105235..fcf3d2f7a7d 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -12,7 +12,6 @@ import homeassistant.util as util from homeassistant.components.light import ( Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR, SUPPORT_TRANSITION) -from homeassistant.loader import get_component import homeassistant.util.color as color_util DEPENDENCIES = ['wemo'] @@ -151,7 +150,7 @@ class WemoDimmer(Light): @asyncio.coroutine def async_added_to_hass(self): """Register update callback.""" - wemo = get_component('wemo') + wemo = self.hass.components.wemo # The register method uses a threading condition, so call via executor. # and yield from to wait until the task is done. yield from self.hass.async_add_job( diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 5a0bf2af1c4..e99d8d4a5f6 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -18,7 +18,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -231,7 +230,7 @@ def async_setup(hass, config): p_id = face.store[g_id].get(service.data[ATTR_PERSON]) camera_entity = service.data[ATTR_CAMERA_ENTITY] - camera = get_component('camera') + camera = hass.components.camera try: image = yield from camera.async_get_image(hass, camera_entity) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index 6f6cb312f2b..aa670578172 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -10,7 +10,6 @@ import json import voluptuous as vol from homeassistant.core import callback -import homeassistant.loader as loader from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( @@ -42,7 +41,7 @@ CONFIG_SCHEMA = vol.Schema({ @asyncio.coroutine def async_setup(hass, config): """Set up the MQTT eventstream component.""" - mqtt = loader.get_component('mqtt') + mqtt = hass.components.mqtt conf = config.get(DOMAIN, {}) pub_topic = conf.get(CONF_PUBLISH_TOPIC) sub_topic = conf.get(CONF_SUBSCRIBE_TOPIC) @@ -82,7 +81,7 @@ def async_setup(hass, config): event_info = {'event_type': event.event_type, 'event_data': event.data} msg = json.dumps(event_info, cls=JSONEncoder) - mqtt.async_publish(hass, pub_topic, msg) + mqtt.async_publish(pub_topic, msg) # Only listen for local events if you are going to publish them. if pub_topic: @@ -115,7 +114,7 @@ def async_setup(hass, config): # Only subscribe if you specified a topic. if sub_topic: - yield from mqtt.async_subscribe(hass, sub_topic, _event_receiver) + yield from mqtt.async_subscribe(sub_topic, _event_receiver) hass.states.async_set('{domain}.initialized'.format(domain=DOMAIN), True) return True diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 17c9129a31d..9b394457973 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -24,7 +24,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component from homeassistant.setup import setup_component REQUIREMENTS = ['pymysensors==0.11.1'] @@ -294,16 +293,16 @@ def setup(hass, config): if device == MQTT_COMPONENT: if not setup_component(hass, MQTT_COMPONENT, config): return - mqtt = get_component(MQTT_COMPONENT) + mqtt = hass.components.mqtt retain = config[DOMAIN].get(CONF_RETAIN) def pub_callback(topic, payload, qos, retain): """Call MQTT publish function.""" - mqtt.publish(hass, topic, payload, qos, retain) + mqtt.publish(topic, payload, qos, retain) def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(hass, topic, sub_cb, qos) + mqtt.subscribe(topic, sub_cb, qos) gateway = mysensors.MQTTGateway( pub_callback, sub_callback, event_callback=None, persistence=persistence, diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 2394d538f2f..7b76836555c 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/scene/ """ import asyncio +import importlib import logging import voluptuous as vol @@ -16,7 +17,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN -from homeassistant.loader import get_platform DOMAIN = 'scene' STATE = 'scening' @@ -34,20 +34,24 @@ def _hass_domain_validator(config): def _platform_validator(config): """Validate it is a valid platform.""" - p_name = config[CONF_PLATFORM] - platform = get_platform(DOMAIN, p_name) + try: + platform = importlib.import_module( + 'homeassistant.components.scene.{}'.format( + config[CONF_PLATFORM])) + except ImportError: + raise vol.Invalid('Invalid platform specified') from None if not hasattr(platform, 'PLATFORM_SCHEMA'): return config - return getattr(platform, 'PLATFORM_SCHEMA')(config) + return platform.PLATFORM_SCHEMA(config) PLATFORM_SCHEMA = vol.Schema( vol.All( _hass_domain_validator, vol.Schema({ - vol.Required(CONF_PLATFORM): cv.platform_validator(DOMAIN) + vol.Required(CONF_PLATFORM): str }, extra=vol.ALLOW_EXTRA), _platform_validator ), extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index ce44abdb087..b460498c901 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import (TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS) from homeassistant.helpers.entity import Entity -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +44,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available BloomSky weather sensors.""" - bloomsky = get_component('bloomsky') + bloomsky = hass.components.bloomsky # Default needed in case of discovery sensors = config.get(CONF_MONITORED_CONDITIONS, SENSOR_TYPES) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 4dddaf45aa4..4aeba082e55 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -64,7 +63,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the available Netatmo weather sensors.""" - netatmo = get_component('netatmo') + netatmo = hass.components.netatmo data = NetAtmoData(netatmo.NETATMO_AUTH, config.get(CONF_STATION, None)) dev = [] diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 02d2b574592..d56b4bc91b4 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -788,7 +788,7 @@ class ZWaveDeviceEntityValues(): if polling_intensity: self.primary.enable_poll(polling_intensity) - platform = get_platform(component, DOMAIN) + platform = get_platform(self._hass, component, DOMAIN) device = platform.get_device( node=self._node, values=self, node_config=node_config, hass=self._hass) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2c440485e49..d69704a7032 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,7 +548,8 @@ def _identify_config_schema(module): return '', schema -def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): +def merge_packages_config(hass, config, packages, + _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" # pylint: disable=too-many-nested-blocks PACKAGES_CONFIG_SCHEMA(packages) @@ -556,7 +557,7 @@ def merge_packages_config(config, packages, _log_pkg_error=_log_pkg_error): for comp_name, comp_conf in pack_conf.items(): if comp_name == CONF_CORE: continue - component = get_component(comp_name) + component = get_component(hass, comp_name) if component is None: _log_pkg_error(pack_name, comp_name, config, "does not exist") @@ -625,7 +626,7 @@ def async_process_component_config(hass, config, domain): This method must be run in the event loop. """ - component = get_component(domain) + component = get_component(hass, domain) if hasattr(component, 'CONFIG_SCHEMA'): try: @@ -651,7 +652,7 @@ def async_process_component_config(hass, config, domain): platforms.append(p_validated) continue - platform = get_platform(domain, p_name) + platform = get_platform(hass, domain, p_name) if platform is None: continue diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4b7c58f6e66..8177999cc94 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -12,7 +12,6 @@ from typing import Any, Union, TypeVar, Callable, Sequence, Dict import voluptuous as vol -from homeassistant.loader import get_platform from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, @@ -283,19 +282,6 @@ def match_all(value): return value -def platform_validator(domain): - """Validate if platform exists for given domain.""" - def validator(value): - """Test if platform exists.""" - if value is None: - raise vol.Invalid('platform cannot be None') - if get_platform(domain, str(value)): - return value - raise vol.Invalid( - 'platform {} does not exist for {}'.format(value, domain)) - return validator - - def positive_timedelta(value: timedelta) -> timedelta: """Validate timedelta is positive.""" if value < timedelta(0): diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 3595b258f12..9114a4db941 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -92,7 +92,7 @@ def extract_entity_ids(hass, service_call, expand_group=True): if not (service_call.data and ATTR_ENTITY_ID in service_call.data): return [] - group = get_component('group') + group = hass.components.group # Entity ID attr can be a list or a string service_ent_id = service_call.data[ATTR_ENTITY_ID] @@ -100,10 +100,10 @@ def extract_entity_ids(hass, service_call, expand_group=True): if expand_group: if isinstance(service_ent_id, str): - return group.expand_entity_ids(hass, [service_ent_id]) + return group.expand_entity_ids([service_ent_id]) return [ent_id for ent_id in - group.expand_entity_ids(hass, service_ent_id)] + group.expand_entity_ids(service_ent_id)] else: @@ -128,7 +128,7 @@ async def async_get_all_descriptions(hass): import homeassistant.components as components component_path = path.dirname(components.__file__) else: - component_path = path.dirname(get_component(domain).__file__) + component_path = path.dirname(get_component(hass, domain).__file__) return path.join(component_path, 'services.yaml') def load_services_files(yaml_files): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3a24de6b39c..f523726c388 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import State, valid_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper -from homeassistant.loader import bind_hass, get_component +from homeassistant.loader import bind_hass from homeassistant.util import convert from homeassistant.util import dt as dt_util from homeassistant.util import location as loc_util @@ -349,10 +349,10 @@ class TemplateMethods(object): else: gr_entity_id = str(entities) - group = get_component('group') + group = self._hass.components.group states = [self._hass.states.get(entity_id) for entity_id - in group.expand_entity_ids(self._hass, [gr_entity_id])] + in group.expand_entity_ids([gr_entity_id])] return _wrap_state(loc_helper.closest(latitude, longitude, states)) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 26cb34ede8c..f1335f73346 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -30,14 +30,14 @@ def flatten(data): return recursive_flatten('', data) -def component_translation_file(component, language): +def component_translation_file(hass, component, language): """Return the translation json file location for a component.""" if '.' in component: name = component.split('.', 1)[1] else: name = component - module = get_component(component) + module = get_component(hass, component) component_path = path.dirname(module.__file__) # If loading translations for the package root, (__init__.py), the @@ -97,7 +97,7 @@ async def async_get_component_resources(hass, language): missing_files = {} for component in missing_components: missing_files[component] = component_translation_file( - component, language) + hass, component, language) # Load missing files if missing_files: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index a3ce2a13f56..322870952f2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -6,15 +6,13 @@ documentation as possible to keep it understandable. Components can be accessed via hass.components.switch from your code. If you want to retrieve a platform that is part of a component, you should -call get_component('switch.your_platform'). In both cases the config directory -is checked to see if it contains a user provided version. If not available it -will check the built-in components and platforms. +call get_component(hass, 'switch.your_platform'). In both cases the config +directory is checked to see if it contains a user provided version. If not +available it will check the built-in components and platforms. """ import functools as ft import importlib import logging -import os -import pkgutil import sys from types import ModuleType @@ -42,135 +40,94 @@ _COMPONENT_CACHE = {} # type: Dict[str, ModuleType] _LOGGER = logging.getLogger(__name__) -def prepare(hass: 'HomeAssistant'): - """Prepare the loading of components. - - This method needs to run in an executor. - """ - global PREPARED # pylint: disable=global-statement - - # Load the built-in components - import homeassistant.components as components - - AVAILABLE_COMPONENTS.clear() - - AVAILABLE_COMPONENTS.extend( - item[1] for item in - pkgutil.iter_modules(components.__path__, 'homeassistant.components.')) - - # Look for available custom components - custom_path = hass.config.path("custom_components") - - if os.path.isdir(custom_path): - # Ensure we can load custom components using Pythons import - sys.path.insert(0, hass.config.config_dir) - - # We cannot use the same approach as for built-in components because - # custom components might only contain a platform for a component. - # ie custom_components/switch/some_platform.py. Using pkgutil would - # not give us the switch component (and neither should it). - - # Assumption: the custom_components dir only contains directories or - # python components. If this assumption is not true, HA won't break, - # just might output more errors. - for fil in os.listdir(custom_path): - if fil == '__pycache__': - continue - elif os.path.isdir(os.path.join(custom_path, fil)): - AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) - else: - # For files we will strip out .py extension - AVAILABLE_COMPONENTS.append( - 'custom_components.{}'.format(fil[0:-3])) - - PREPARED = True +DATA_KEY = 'components' +PATH_CUSTOM_COMPONENTS = 'custom_components' +PACKAGE_COMPONENTS = 'homeassistant.components' -def set_component(comp_name: str, component: ModuleType) -> None: +def set_component(hass, comp_name: str, component: ModuleType) -> None: """Set a component in the cache. Async friendly. """ - _check_prepared() - - _COMPONENT_CACHE[comp_name] = component + cache = hass.data.get(DATA_KEY) + if cache is None: + cache = hass.data[DATA_KEY] = {} + cache[comp_name] = component -def get_platform(domain: str, platform: str) -> Optional[ModuleType]: +def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: """Try to load specified platform. Async friendly. """ - return get_component(PLATFORM_FORMAT.format(domain, platform)) + return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(comp_name) -> Optional[ModuleType]: - """Try to load specified component. +def get_component(hass, comp_or_platform): + """Load a module from either custom component or built-in.""" + try: + return hass.data[DATA_KEY][comp_or_platform] + except KeyError: + pass - Looks in config dir first, then built-in components. - Only returns it if also found to be valid. - - Async friendly. - """ - if comp_name in _COMPONENT_CACHE: - return _COMPONENT_CACHE[comp_name] - - _check_prepared() - - # If we ie. try to load custom_components.switch.wemo but the parent - # custom_components.switch does not exist, importing it will trigger - # an exception because it will try to import the parent. - # Because of this behavior, we will approach loading sub components - # with caution: only load it if we can verify that the parent exists. - # We do not want to silent the ImportErrors as they provide valuable - # information to track down when debugging Home Assistant. - - # First check custom, then built-in - potential_paths = ['custom_components.{}'.format(comp_name), - 'homeassistant.components.{}'.format(comp_name)] - - for path in potential_paths: - # Validate here that root component exists - # If path contains a '.' we are specifying a sub-component - # Using rsplit we get the parent component from sub-component - root_comp = path.rsplit(".", 1)[0] if '.' in comp_name else path - - if root_comp not in AVAILABLE_COMPONENTS: - continue + # Try custom component + module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), + comp_or_platform) + if module is None: try: - module = importlib.import_module(path) + module = importlib.import_module( + '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) + except ImportError: + module = None - # In Python 3 you can import files from directories that do not - # contain the file __init__.py. A directory is a valid module if - # it contains a file with the .py extension. In this case Python - # will succeed in importing the directory as a module and call it - # a namespace. We do not care about namespaces. - # This prevents that when only - # custom_components/switch/some_platform.py exists, - # the import custom_components.switch would succeed. - if module.__spec__.origin == 'namespace': - continue + cache = hass.data.get(DATA_KEY) + if cache is None: + cache = hass.data[DATA_KEY] = {} + cache[comp_or_platform] = module - _LOGGER.info("Loaded %s from %s", comp_name, path) + return module - _COMPONENT_CACHE[comp_name] = module - - return module - - except ImportError as err: - # This error happens if for example custom_components/switch - # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): - _LOGGER.exception( - ("Error loading %s. Make sure all " - "dependencies are installed"), path) - - _LOGGER.error("Unable to find component %s", comp_name) +def _find_spec(path, name): + for finder in sys.meta_path: + try: + spec = finder.find_spec(name, path=path) + if spec is not None: + return spec + except AttributeError: + # Not all finders have the find_spec method + pass return None +def _load_module(path, name): + """Load a module based on a folder and a name.""" + spec = _find_spec([path], name) + + # Special handling if loading platforms and the folder is a namespace + # (namespace is a folder without __init__.py) + if spec is None and '.' in name: + parent_spec = _find_spec([path], name.split('.')[0]) + if (parent_spec is None or + parent_spec.submodule_search_locations is None): + return None + spec = _find_spec(parent_spec.submodule_search_locations, name) + + # Not found + if spec is None: + return None + + # This is a namespace + if spec.loader is None: + return None + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + class Components: """Helper to load components.""" @@ -180,7 +137,7 @@ class Components: def __getattr__(self, comp_name): """Fetch a component.""" - component = get_component(comp_name) + component = get_component(self._hass, comp_name) if component is None: raise ImportError('Unable to load {}'.format(comp_name)) wrapped = ModuleWrapper(self._hass, component) @@ -230,7 +187,7 @@ def bind_hass(func): return func -def load_order_component(comp_name: str) -> OrderedSet: +def load_order_component(hass, comp_name: str) -> OrderedSet: """Return an OrderedSet of components in the correct order of loading. Raises HomeAssistantError if a circular dependency is detected. @@ -238,16 +195,16 @@ def load_order_component(comp_name: str) -> OrderedSet: Async friendly. """ - return _load_order_component(comp_name, OrderedSet(), set()) + return _load_order_component(hass, comp_name, OrderedSet(), set()) -def _load_order_component(comp_name: str, load_order: OrderedSet, +def _load_order_component(hass, comp_name: str, load_order: OrderedSet, loading: Set) -> OrderedSet: """Recursive function to get load order of components. Async friendly. """ - component = get_component(comp_name) + component = get_component(hass, comp_name) # If None it does not exist, error already thrown by get_component. if component is None: @@ -266,7 +223,8 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, comp_name, dependency) return OrderedSet() - dep_load_order = _load_order_component(dependency, load_order, loading) + dep_load_order = _load_order_component( + hass, dependency, load_order, loading) # length == 0 means error loading dependency or children if not dep_load_order: @@ -280,14 +238,3 @@ def _load_order_component(comp_name: str, load_order: OrderedSet, loading.remove(comp_name) return load_order - - -def _check_prepared() -> None: - """Issue a warning if loader.prepare() has never been called. - - Async friendly. - """ - if not PREPARED: - _LOGGER.warning(( - "You did not call loader.prepare() yet. " - "Certain functionality might not be working")) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 4375d973a0b..3a1ffa82d47 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -16,8 +16,8 @@ from homeassistant import bootstrap, core, loader from homeassistant.config import ( get_default_config_dir, CONF_CORE, CORE_CONFIG_SCHEMA, CONF_PACKAGES, merge_packages_config, _format_config_error, - find_config_file, load_yaml_config_file, get_component, - extract_domain_configs, config_per_platform, get_platform) + find_config_file, load_yaml_config_file, + extract_domain_configs, config_per_platform) import homeassistant.util.yaml as yaml from homeassistant.exceptions import HomeAssistantError @@ -201,18 +201,10 @@ def check(config_dir, secrets=False): yaml.yaml.SafeLoader.add_constructor('!secret', yaml._secret_yaml) try: - class HassConfig(): - """Hass object with config.""" - - def __init__(self, conf_dir): - """Init the config_dir.""" - self.config = core.Config() - self.config.config_dir = conf_dir - - loader.prepare(HassConfig(config_dir)) - - res['components'] = check_ha_config_file(config_dir) + hass = core.HomeAssistant() + hass.config.config_dir = config_dir + res['components'] = check_ha_config_file(hass) res['secret_cache'] = OrderedDict(yaml.__SECRET_CACHE) for err in res['components'].errors: @@ -222,6 +214,7 @@ def check(config_dir, secrets=False): res['except'].setdefault(domain, []).append(err.config) except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("BURB") print(color('red', 'Fatal error while loading config:'), str(err)) res['except'].setdefault(ERROR_STR, []).append(str(err)) finally: @@ -290,8 +283,9 @@ class HomeAssistantConfig(OrderedDict): return self -def check_ha_config_file(config_dir): +def check_ha_config_file(hass): """Check if Home Assistant configuration file is valid.""" + config_dir = hass.config.config_dir result = HomeAssistantConfig() def _pack_error(package, component, config, message): @@ -330,7 +324,7 @@ def check_ha_config_file(config_dir): # Merge packages merge_packages_config( - config, core_config.get(CONF_PACKAGES, {}), _pack_error) + hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error) del core_config[CONF_PACKAGES] # Ensure we have no None values after merge @@ -343,7 +337,7 @@ def check_ha_config_file(config_dir): # Process and validate config for domain in components: - component = get_component(domain) + component = loader.get_component(hass, domain) if not component: result.add_error("Component not found: {}".format(domain)) continue @@ -375,7 +369,7 @@ def check_ha_config_file(config_dir): platforms.append(p_validated) continue - platform = get_platform(domain, p_name) + platform = loader.get_platform(hass, domain, p_name) if platform is None: result.add_error( diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 169a160af65..f26aa9b61f1 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -98,14 +98,14 @@ async def _async_setup_component(hass: core.HomeAssistant, _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) - component = loader.get_component(domain) + component = loader.get_component(hass, domain) if not component: log_error("Component not found.", False) return False # Validate no circular dependencies - components = loader.load_order_component(domain) + components = loader.load_order_component(hass, domain) # OrderedSet is empty if component or dependencies could not be resolved if not components: @@ -159,7 +159,7 @@ async def _async_setup_component(hass: core.HomeAssistant, elif result is not True: log_error("Component did not return boolean if setup was successful. " "Disabling component.") - loader.set_component(domain, None) + loader.set_component(hass, domain, None) return False for entry in hass.config_entries.async_entries(domain): @@ -193,7 +193,7 @@ async def async_prepare_setup_platform(hass: core.HomeAssistant, config, platform_path, msg) async_notify_setup_error(hass, platform_path) - platform = loader.get_platform(domain, platform_name) + platform = loader.get_platform(hass, domain, platform_name) # Not found if platform is None: diff --git a/tests/common.py b/tests/common.py index b04abda7c28..f53d1c2be2b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -10,8 +10,7 @@ import logging import threading from contextlib import contextmanager -from homeassistant import ( - auth, core as ha, loader, data_entry_flow, config_entries) +from homeassistant import auth, core as ha, data_entry_flow, config_entries from homeassistant.setup import setup_component, async_setup_component from homeassistant.config import async_process_component_config from homeassistant.helpers import ( @@ -138,9 +137,6 @@ def async_test_home_assistant(loop): hass.config.units = METRIC_SYSTEM hass.config.skip_pip = True - if 'custom_components.test' not in loader.AVAILABLE_COMPONENTS: - yield from loop.run_in_executor(None, loader.prepare, hass) - hass.state = ha.CoreState.running # Mock async_start diff --git a/tests/components/climate/test_generic_thermostat.py b/tests/components/climate/test_generic_thermostat.py index bd0b764c6fe..7bc0b0a18e7 100644 --- a/tests/components/climate/test_generic_thermostat.py +++ b/tests/components/climate/test_generic_thermostat.py @@ -116,7 +116,7 @@ class TestGenericThermostatHeaterSwitching(unittest.TestCase): def test_heater_switch(self): """Test heater switching test switch.""" - platform = loader.get_component('switch.test') + platform = loader.get_component(self.hass, 'switch.test') platform.init() self.switch_1 = platform.DEVICES[1] assert setup_component(self.hass, switch.DOMAIN, {'switch': { diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index f53be8818a3..84d15578e13 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -17,10 +17,10 @@ from homeassistant.loader import set_component from tests.common import MockConfigEntry, MockModule, mock_coro_func -@pytest.fixture(scope='session', autouse=True) -def mock_test_component(): +@pytest.fixture(autouse=True) +def mock_test_component(hass): """Ensure a component called 'test' exists.""" - set_component('test', MockModule('test')) + set_component(hass, 'test', MockModule('test')) @pytest.fixture @@ -172,7 +172,8 @@ def test_abort(hass, client): def test_create_account(hass, client): """Test a flow that creates an account.""" set_component( - 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(FlowHandler): VERSION = 1 @@ -204,7 +205,8 @@ def test_create_account(hass, client): def test_two_step_flow(hass, client): """Test we can finish a two step flow.""" set_component( - 'test', MockModule('test', async_setup_entry=mock_coro_func(True))) + hass, 'test', + MockModule('test', async_setup_entry=mock_coro_func(True))) class TestFlow(FlowHandler): VERSION = 1 diff --git a/tests/components/device_tracker/test_asuswrt.py b/tests/components/device_tracker/test_asuswrt.py index d2ae8965668..0cbece6d1b0 100644 --- a/tests/components/device_tracker/test_asuswrt.py +++ b/tests/components/device_tracker/test_asuswrt.py @@ -3,9 +3,9 @@ import os from datetime import timedelta import unittest from unittest import mock +import socket import voluptuous as vol -from future.backports import socket from homeassistant.setup import setup_component from homeassistant.components import device_tracker diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 912bd315ecd..0b17b4e0ac8 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -189,7 +189,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): def test_update_stale(self): """Test stalled update.""" - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('DEV1') @@ -251,7 +251,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): hide_if_away=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() with assert_setup_component(1, device_tracker.DOMAIN): @@ -270,7 +270,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): hide_if_away=True) device_tracker.update_config(self.yaml_devices, dev_id, device) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() with assert_setup_component(1, device_tracker.DOMAIN): @@ -431,7 +431,7 @@ class TestComponentsDeviceTracker(unittest.TestCase): 'zone': zone_info }) - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(self.hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('dev1') @@ -547,7 +547,7 @@ def test_bad_platform(hass): async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): """Test the adding of unknown devices to configuration file.""" - scanner = get_component('device_tracker.test').SCANNER + scanner = get_component(hass, 'device_tracker.test').SCANNER scanner.reset() scanner.come_home('DEV1') diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 4e8fad261bd..634e3774b8a 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -118,7 +118,7 @@ class TestLight(unittest.TestCase): def test_services(self): """Test the provided services.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( @@ -267,7 +267,7 @@ class TestLight(unittest.TestCase): def test_broken_light_profiles(self): """Test light profiles.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) @@ -282,7 +282,7 @@ class TestLight(unittest.TestCase): def test_light_profiles(self): """Test light profiles.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() user_light_file = self.hass.config.path(light.LIGHT_PROFILES_FILE) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index 25ea818c774..a832e249832 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -16,7 +16,7 @@ class TestScene(unittest.TestCase): def setUp(self): # pylint: disable=invalid-name """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - test_light = loader.get_component('light.test') + test_light = loader.get_component(self.hass, 'light.test') test_light.init() self.assertTrue(setup_component(self.hass, light.DOMAIN, { diff --git a/tests/components/switch/test_flux.py b/tests/components/switch/test_flux.py index c42061db958..61e665f265c 100644 --- a/tests/components/switch/test_flux.py +++ b/tests/components/switch/test_flux.py @@ -71,7 +71,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_when_switch_is_off(self): """Test the flux switch when it is off.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -113,7 +113,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_before_sunrise(self): """Test the flux switch before sunrise.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -160,7 +160,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_sunrise_before_sunset(self): """Test the flux switch after sunrise and before sunset.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -207,7 +207,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_sunset_before_stop(self): """Test the flux switch after sunset and before stop.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -255,7 +255,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_after_stop_before_sunrise(self): """Test the flux switch after stop and before sunrise.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -302,7 +302,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_start_stop_times(self): """Test the flux with custom start and stop times.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -353,7 +353,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -405,7 +405,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -456,7 +456,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -507,7 +507,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -558,7 +558,7 @@ class TestSwitchFlux(unittest.TestCase): This test has the stop_time on the next day (after midnight). """ - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -606,7 +606,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_colortemps(self): """Test the flux with custom start and stop colortemps.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -656,7 +656,7 @@ class TestSwitchFlux(unittest.TestCase): # pylint: disable=invalid-name def test_flux_with_custom_brightness(self): """Test the flux with custom start and stop colortemps.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -704,7 +704,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_multiple_lights(self): """Test the flux switch with multiple light entities.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -773,7 +773,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_mired(self): """Test the flux switch´s mode mired.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, @@ -818,7 +818,7 @@ class TestSwitchFlux(unittest.TestCase): def test_flux_with_rgb(self): """Test the flux switch´s mode rgb.""" - platform = loader.get_component('light.test') + platform = loader.get_component(self.hass, 'light.test') platform.init() self.assertTrue( setup_component(self.hass, light.DOMAIN, diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index 090e3c74bf1..d679aa2c827 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -17,7 +17,7 @@ class TestSwitch(unittest.TestCase): def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() - platform = loader.get_component('switch.test') + platform = loader.get_component(self.hass, 'switch.test') platform.init() # Switch 1 is ON, switch 2 is OFF self.switch_1, self.switch_2, self.switch_3 = \ @@ -79,10 +79,10 @@ class TestSwitch(unittest.TestCase): def test_setup_two_platforms(self): """Test with bad configuration.""" # Test if switch component returns 0 switches - test_platform = loader.get_component('switch.test') + test_platform = loader.get_component(self.hass, 'switch.test') test_platform.init(True) - loader.set_component('switch.test2', test_platform) + loader.set_component(self.hass, 'switch.test2', test_platform) test_platform.init(False) self.assertTrue(setup_component( diff --git a/tests/components/test_device_sun_light_trigger.py b/tests/components/test_device_sun_light_trigger.py index 3c73e85c4e5..a8b8a201217 100644 --- a/tests/components/test_device_sun_light_trigger.py +++ b/tests/components/test_device_sun_light_trigger.py @@ -22,12 +22,12 @@ class TestDeviceSunLightTrigger(unittest.TestCase): self.hass = get_test_home_assistant() self.scanner = loader.get_component( - 'device_tracker.test').get_scanner(None, None) + self.hass, 'device_tracker.test').get_scanner(None, None) self.scanner.reset() self.scanner.come_home('DEV1') - loader.get_component('light.test').init() + loader.get_component(self.hass, 'light.test').init() with patch( 'homeassistant.components.device_tracker.load_yaml_config_file', diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 90be56bbc7c..aff0acf9e3a 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -10,8 +10,6 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv -from tests.common import get_test_home_assistant - def test_boolean(): """Test boolean validation.""" @@ -256,24 +254,6 @@ def test_event_schema(): cv.EVENT_SCHEMA(value) -def test_platform_validator(): - """Test platform validation.""" - hass = None - - try: - hass = get_test_home_assistant() - - schema = vol.Schema(cv.platform_validator('light')) - - with pytest.raises(vol.MultipleInvalid): - schema('platform_that_does_not_exist') - - schema('hue') - finally: - if hass is not None: - hass.stop() - - def test_icon(): """Test icon validation.""" schema = vol.Schema(cv.icon) diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index b345400ba17..c7b39954d85 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -129,11 +129,11 @@ class TestHelpersDiscovery: platform_calls.append('disc' if discovery_info else 'component') loader.set_component( - 'test_component', + self.hass, 'test_component', MockModule('test_component', setup=component_setup)) loader.set_component( - 'switch.test_circular', + self.hass, 'switch.test_circular', MockPlatform(setup_platform, dependencies=['test_component'])) @@ -177,11 +177,11 @@ class TestHelpersDiscovery: return True loader.set_component( - 'test_component1', + self.hass, 'test_component1', MockModule('test_component1', setup=component1_setup)) loader.set_component( - 'test_component2', + self.hass, 'test_component2', MockModule('test_component2', setup=component2_setup)) @callback diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0bc6a7601dc..504f31cc987 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -75,9 +75,9 @@ class TestHelpersEntityComponent(unittest.TestCase): component_setup = Mock(return_value=True) platform_setup = Mock(return_value=None) loader.set_component( - 'test_component', + self.hass, 'test_component', MockModule('test_component', setup=component_setup)) - loader.set_component('test_domain.mod2', + loader.set_component(self.hass, 'test_domain.mod2', MockPlatform(platform_setup, ['test_component'])) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -100,8 +100,10 @@ class TestHelpersEntityComponent(unittest.TestCase): platform1_setup = Mock(side_effect=Exception('Broken')) platform2_setup = Mock(return_value=None) - loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) - loader.set_component('test_domain.mod2', MockPlatform(platform2_setup)) + loader.set_component(self.hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) + loader.set_component(self.hass, 'test_domain.mod2', + MockPlatform(platform2_setup)) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -145,7 +147,7 @@ class TestHelpersEntityComponent(unittest.TestCase): """Test the platform setup.""" add_devices([MockEntity(should_poll=True)]) - loader.set_component('test_domain.platform', + loader.set_component(self.hass, 'test_domain.platform', MockPlatform(platform_setup)) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -172,7 +174,7 @@ class TestHelpersEntityComponent(unittest.TestCase): platform = MockPlatform(platform_setup) - loader.set_component('test_domain.platform', platform) + loader.set_component(self.hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -220,7 +222,8 @@ def test_platform_not_ready(hass): """Test that we retry when platform not ready.""" platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) - loader.set_component('test_domain.mod1', MockPlatform(platform1_setup)) + loader.set_component(hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -316,10 +319,11 @@ def test_setup_dependencies_platform(hass): We're explictely testing that we process dependencies even if a component with the same name has already been loaded. """ - loader.set_component('test_component', MockModule('test_component')) - loader.set_component('test_component2', MockModule('test_component2')) + loader.set_component(hass, 'test_component', MockModule('test_component')) + loader.set_component(hass, 'test_component2', + MockModule('test_component2')) loader.set_component( - 'test_domain.test_component', + hass, 'test_domain.test_component', MockPlatform(dependencies=['test_component', 'test_component2'])) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -341,7 +345,7 @@ async def test_setup_entry(hass): """Test setup entry calls async_setup_entry on platform.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -366,7 +370,7 @@ async def test_setup_entry_fails_duplicate(hass): """Test we don't allow setting up a config entry twice.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -382,7 +386,7 @@ async def test_unload_entry_resets_platform(hass): """Test unloading an entry removes all entities.""" mock_setup_entry = Mock(return_value=mock_coro(True)) loader.set_component( - 'test_domain.entry_domain', + hass, 'test_domain.entry_domain', MockPlatform(async_setup_entry=mock_setup_entry)) component = EntityComponent(_LOGGER, DOMAIN, hass) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 2018cb27541..4e09f9576f2 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -147,7 +147,7 @@ class TestHelpersEntityPlatform(unittest.TestCase): platform = MockPlatform(platform_setup) platform.SCAN_INTERVAL = timedelta(seconds=30) - loader.set_component('test_domain.platform', platform) + loader.set_component(self.hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -184,7 +184,7 @@ def test_platform_warn_slow_setup(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -218,7 +218,7 @@ def test_platform_error_slow_setup(hass, caplog): platform = MockPlatform(async_setup_platform=setup_platform) component = EntityComponent(_LOGGER, DOMAIN, hass) - loader.set_component('test_domain.test_platform', platform) + loader.set_component(hass, 'test_domain.test_platform', platform) yield from component.async_setup({ DOMAIN: { 'platform': 'test_platform', @@ -260,7 +260,7 @@ def test_parallel_updates_async_platform(hass): platform.async_setup_platform = mock_update - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} @@ -288,7 +288,7 @@ def test_parallel_updates_async_platform_with_constant(hass): platform.async_setup_platform = mock_update platform.PARALLEL_UPDATES = 1 - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} @@ -309,7 +309,7 @@ def test_parallel_updates_sync_platform(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform(setup_platform=lambda *args: None) - loader.set_component('test_domain.platform', platform) + loader.set_component(hass, 'test_domain.platform', platform) component = EntityComponent(_LOGGER, DOMAIN, hass) component._platforms = {} diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a987f5130f1..79054726c03 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -138,7 +138,7 @@ class TestServiceHelpers(unittest.TestCase): self.hass.states.set('light.Ceiling', STATE_OFF) self.hass.states.set('light.Kitchen', STATE_OFF) - loader.get_component('group').Group.create_group( + loader.get_component(self.hass, 'group').Group.create_group( self.hass, 'test', ['light.Ceiling', 'light.Kitchen']) call = ha.ServiceCall('light', 'turn_on', @@ -160,7 +160,7 @@ class TestServiceHelpers(unittest.TestCase): @asyncio.coroutine def test_async_get_all_descriptions(hass): """Test async_get_all_descriptions.""" - group = loader.get_component('group') + group = loader.get_component(hass, 'group') group_config = {group.DOMAIN: {}} yield from async_setup_component(hass, group.DOMAIN, group_config) descriptions = yield from service.async_get_all_descriptions(hass) @@ -170,7 +170,7 @@ def test_async_get_all_descriptions(hass): assert 'description' in descriptions['group']['reload'] assert 'fields' in descriptions['group']['reload'] - logger = loader.get_component('logger') + logger = loader.get_component(hass, 'logger') logger_config = {logger.DOMAIN: {}} yield from async_setup_component(hass, logger.DOMAIN, logger_config) descriptions = yield from service.async_get_all_descriptions(hass) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index c72efca8c29..99c6f7dddf1 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -50,15 +50,15 @@ async def test_component_translation_file(hass): }) assert path.normpath(translation.component_translation_file( - 'switch.test', 'en')) == path.normpath(hass.config.path( + hass, 'switch.test', 'en')) == path.normpath(hass.config.path( 'custom_components', 'switch', '.translations', 'test.en.json')) assert path.normpath(translation.component_translation_file( - 'test_standalone', 'en')) == path.normpath(hass.config.path( + hass, 'test_standalone', 'en')) == path.normpath(hass.config.path( 'custom_components', '.translations', 'test_standalone.en.json')) assert path.normpath(translation.component_translation_file( - 'test_package', 'en')) == path.normpath(hass.config.path( + hass, 'test_package', 'en')) == path.normpath(hass.config.path( 'custom_components', 'test_package', '.translations', 'en.json')) diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 28a3f2ebdc8..8dfc5db90e0 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -7,7 +7,6 @@ from unittest.mock import patch import homeassistant.scripts.check_config as check_config from homeassistant.config import YAML_CONFIG_FILE -from homeassistant.loader import set_component from tests.common import patch_yaml_files, get_test_config_dir _LOGGER = logging.getLogger(__name__) @@ -106,7 +105,6 @@ class TestCheckConfig(unittest.TestCase): def test_component_platform_not_found(self, isfile_patch): """Test errors if component or platform not found.""" # Make sure they don't exist - set_component('beer', None) files = { YAML_CONFIG_FILE: BASE_CONFIG + 'beer:', } @@ -119,7 +117,6 @@ class TestCheckConfig(unittest.TestCase): assert res['secrets'] == {} assert len(res['yaml_files']) == 1 - set_component('light.beer', None) files = { YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: beer', } diff --git a/tests/test_config.py b/tests/test_config.py index 652b931366a..4b1115c3814 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -568,7 +568,7 @@ def merge_log_err(hass): yield logerr -def test_merge(merge_log_err): +def test_merge(merge_log_err, hass): """Test if we can merge packages.""" packages = { 'pack_dict': {'input_boolean': {'ib1': None}}, @@ -582,7 +582,7 @@ def test_merge(merge_log_err): 'input_boolean': {'ib2': None}, 'light': {'platform': 'test'} } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert len(config) == 5 @@ -592,7 +592,7 @@ def test_merge(merge_log_err): assert config['wake_on_lan'] is None -def test_merge_try_falsy(merge_log_err): +def test_merge_try_falsy(merge_log_err, hass): """Ensure we dont add falsy items like empty OrderedDict() to list.""" packages = { 'pack_falsy_to_lst': {'automation': OrderedDict()}, @@ -603,7 +603,7 @@ def test_merge_try_falsy(merge_log_err): 'automation': {'do': 'something'}, 'light': {'some': 'light'}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert len(config) == 3 @@ -611,7 +611,7 @@ def test_merge_try_falsy(merge_log_err): assert len(config['light']) == 1 -def test_merge_new(merge_log_err): +def test_merge_new(merge_log_err, hass): """Test adding new components to outer scope.""" packages = { 'pack_1': {'light': [{'platform': 'one'}]}, @@ -624,7 +624,7 @@ def test_merge_new(merge_log_err): config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 0 assert 'api' in config @@ -633,7 +633,7 @@ def test_merge_new(merge_log_err): assert len(config['panel_custom']) == 1 -def test_merge_type_mismatch(merge_log_err): +def test_merge_type_mismatch(merge_log_err, hass): """Test if we have a type mismatch for packages.""" packages = { 'pack_1': {'input_boolean': [{'ib1': None}]}, @@ -646,7 +646,7 @@ def test_merge_type_mismatch(merge_log_err): 'input_select': [{'ib2': None}], 'light': [{'platform': 'two'}] } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 2 assert len(config) == 4 @@ -654,7 +654,7 @@ def test_merge_type_mismatch(merge_log_err): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err): +def test_merge_once_only(merge_log_err, hass): """Test if we have a merge for a comp that may occur only once.""" packages = { 'pack_2': { @@ -666,7 +666,7 @@ def test_merge_once_only(merge_log_err): config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, 'mqtt': {}, 'api': {} } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 assert len(config) == 3 @@ -682,13 +682,13 @@ def test_merge_id_schema(hass): 'qwikswitch': 'dict', } for name, expected_type in types.items(): - module = config_util.get_component(name) + module = config_util.get_component(hass, name) typ, _ = config_util._identify_config_schema(module) assert typ == expected_type, "{} expected {}, got {}".format( name, expected_type, typ) -def test_merge_duplicate_keys(merge_log_err): +def test_merge_duplicate_keys(merge_log_err, hass): """Test if keys in dicts are duplicates.""" packages = { 'pack_1': {'input_select': {'ib1': None}}, @@ -697,7 +697,7 @@ def test_merge_duplicate_keys(merge_log_err): config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, 'input_select': {'ib1': None}, } - config_util.merge_packages_config(config, packages) + config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 assert len(config) == 2 diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b46909d7732..1518706db55 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -27,7 +27,7 @@ def test_call_setup_entry(hass): mock_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'comp', + hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) result = yield from async_setup_component(hass, 'comp', {}) @@ -36,12 +36,12 @@ def test_call_setup_entry(hass): @asyncio.coroutine -def test_remove_entry(manager): +def test_remove_entry(hass, manager): """Test that we can remove an entry.""" mock_unload_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'test', + hass, 'test', MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) @@ -63,7 +63,7 @@ def test_remove_entry(manager): @asyncio.coroutine -def test_remove_entry_raises(manager): +def test_remove_entry_raises(hass, manager): """Test if a component raises while removing entry.""" @asyncio.coroutine def mock_unload_entry(hass, entry): @@ -71,7 +71,7 @@ def test_remove_entry_raises(manager): raise Exception("BROKEN") loader.set_component( - 'test', + hass, 'test', MockModule('comp', async_unload_entry=mock_unload_entry)) MockConfigEntry(domain='test', entry_id='test1').add_to_manager(manager) @@ -96,7 +96,7 @@ def test_add_entry_calls_setup_entry(hass, manager): mock_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'comp', + hass, 'comp', MockModule('comp', async_setup_entry=mock_setup_entry)) class TestFlow(data_entry_flow.FlowHandler): @@ -151,6 +151,8 @@ def test_domains_gets_uniques(manager): @asyncio.coroutine def test_saving_and_loading(hass): """Test that we're saving and loading correctly.""" + loader.set_component(hass, 'test', MockModule('test')) + class TestFlow(data_entry_flow.FlowHandler): VERSION = 5 @@ -217,12 +219,12 @@ async def test_forward_entry_sets_up_component(hass): mock_original_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'original', + hass, 'original', MockModule('original', async_setup_entry=mock_original_setup_entry)) mock_forwarded_setup_entry = MagicMock(return_value=mock_coro(True)) loader.set_component( - 'forwarded', + hass, 'forwarded', MockModule('forwarded', async_setup_entry=mock_forwarded_setup_entry)) await hass.config_entries.async_forward_entry_setup(entry, 'forwarded') @@ -236,7 +238,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): mock_setup = MagicMock(return_value=mock_coro(False)) mock_setup_entry = MagicMock() - loader.set_component('forwarded', MockModule( + hass, loader.set_component(hass, 'forwarded', MockModule( 'forwarded', async_setup=mock_setup, async_setup_entry=mock_setup_entry, @@ -249,6 +251,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(hass): async def test_discovery_notification(hass): """Test that we create/dismiss a notification when source is discovery.""" + loader.set_component(hass, 'test', MockModule('test')) await async_setup_component(hass, 'persistent_notification', {}) class TestFlow(data_entry_flow.FlowHandler): diff --git a/tests/test_loader.py b/tests/test_loader.py index 7fc33df57bb..646526e94ea 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -27,37 +27,40 @@ class TestLoader(unittest.TestCase): def test_set_component(self): """Test if set_component works.""" - loader.set_component('switch.test_set', http) + comp = object() + loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(http, loader.get_component('switch.test_set')) + self.assertEqual(comp, + loader.get_component(self.hass, 'switch.test_set')) def test_get_component(self): """Test if get_component works.""" - self.assertEqual(http, loader.get_component('http')) - - self.assertIsNotNone(loader.get_component('switch.test')) + self.assertEqual(http, loader.get_component(self.hass, 'http')) + self.assertIsNotNone(loader.get_component(self.hass, 'light.hue')) def test_load_order_component(self): """Test if we can get the proper load order of components.""" - loader.set_component('mod1', MockModule('mod1')) - loader.set_component('mod2', MockModule('mod2', ['mod1'])) - loader.set_component('mod3', MockModule('mod3', ['mod2'])) + loader.set_component(self.hass, 'mod1', MockModule('mod1')) + loader.set_component(self.hass, 'mod2', MockModule('mod2', ['mod1'])) + loader.set_component(self.hass, 'mod3', MockModule('mod3', ['mod2'])) self.assertEqual( - ['mod1', 'mod2', 'mod3'], loader.load_order_component('mod3')) + ['mod1', 'mod2', 'mod3'], + loader.load_order_component(self.hass, 'mod3')) # Create circular dependency - loader.set_component('mod1', MockModule('mod1', ['mod3'])) + loader.set_component(self.hass, 'mod1', MockModule('mod1', ['mod3'])) - self.assertEqual([], loader.load_order_component('mod3')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod3')) # Depend on non-existing component - loader.set_component('mod1', MockModule('mod1', ['nonexisting'])) + loader.set_component(self.hass, 'mod1', + MockModule('mod1', ['nonexisting'])) - self.assertEqual([], loader.load_order_component('mod1')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod1')) # Try to get load order for non-existing component - self.assertEqual([], loader.load_order_component('mod1')) + self.assertEqual([], loader.load_order_component(self.hass, 'mod1')) def test_component_loader(hass): diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 5f09e0bd83e..8ae0f6c11de 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -35,7 +35,8 @@ class TestRequirements: mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) + self.hass, 'comp', + MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components assert mock_install.call_args == mock.call( @@ -53,7 +54,8 @@ class TestRequirements: mock_dirname.return_value = 'ha_package_path' self.hass.config.skip_pip = False loader.set_component( - 'comp', MockModule('comp', requirements=['package==0.0.1'])) + self.hass, 'comp', + MockModule('comp', requirements=['package==0.0.1'])) assert setup.setup_component(self.hass, 'comp') assert 'comp' in self.hass.config.components assert mock_install.call_args == mock.call( diff --git a/tests/test_setup.py b/tests/test_setup.py index 6a94310793c..6f0c282e016 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -49,6 +49,7 @@ class TestSetup: } }, required=True) loader.set_component( + self.hass, 'comp_conf', MockModule('comp_conf', config_schema=config_schema)) with assert_setup_component(0): @@ -93,10 +94,12 @@ class TestSetup: 'hello': str, }) loader.set_component( + self.hass, 'platform_conf', MockModule('platform_conf', platform_schema=platform_schema)) loader.set_component( + self.hass, 'platform_conf.whatever', MockPlatform('whatever')) with assert_setup_component(0): @@ -179,7 +182,8 @@ class TestSetup: """Test we do not setup a component twice.""" mock_setup = mock.MagicMock(return_value=True) - loader.set_component('comp', MockModule('comp', setup=mock_setup)) + loader.set_component( + self.hass, 'comp', MockModule('comp', setup=mock_setup)) assert setup.setup_component(self.hass, 'comp') assert mock_setup.called @@ -195,6 +199,7 @@ class TestSetup: """Component setup should fail if requirement can't install.""" self.hass.config.skip_pip = False loader.set_component( + self.hass, 'comp', MockModule('comp', requirements=['package==0.0.1'])) assert not setup.setup_component(self.hass, 'comp') @@ -210,6 +215,7 @@ class TestSetup: result.append(1) loader.set_component( + self.hass, 'comp', MockModule('comp', async_setup=async_setup)) def setup_component(): @@ -227,20 +233,23 @@ class TestSetup: def test_component_not_setup_missing_dependencies(self): """Test we do not setup a component if not all dependencies loaded.""" deps = ['non_existing'] - loader.set_component('comp', MockModule('comp', dependencies=deps)) + loader.set_component( + self.hass, 'comp', MockModule('comp', dependencies=deps)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) - loader.set_component('non_existing', MockModule('non_existing')) + loader.set_component( + self.hass, 'non_existing', MockModule('non_existing')) assert setup.setup_component(self.hass, 'comp', {}) def test_component_failing_setup(self): """Test component that fails setup.""" loader.set_component( - 'comp', MockModule('comp', setup=lambda hass, config: False)) + self.hass, 'comp', + MockModule('comp', setup=lambda hass, config: False)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components @@ -251,7 +260,8 @@ class TestSetup: """Setup that raises exception.""" raise Exception('fail!') - loader.set_component('comp', MockModule('comp', setup=exception_setup)) + loader.set_component( + self.hass, 'comp', MockModule('comp', setup=exception_setup)) assert not setup.setup_component(self.hass, 'comp', {}) assert 'comp' not in self.hass.config.components @@ -264,11 +274,12 @@ class TestSetup: return True raise Exception('Config not passed in: {}'.format(config)) - loader.set_component('comp_a', - MockModule('comp_a', setup=config_check_setup)) + loader.set_component( + self.hass, 'comp_a', + MockModule('comp_a', setup=config_check_setup)) - loader.set_component('switch.platform_a', MockPlatform('comp_b', - ['comp_a'])) + loader.set_component( + self.hass, 'switch.platform_a', MockPlatform('comp_b', ['comp_a'])) setup.setup_component(self.hass, 'switch', { 'comp_a': { @@ -289,6 +300,7 @@ class TestSetup: mock_setup = mock.MagicMock(spec_set=True) loader.set_component( + self.hass, 'switch.platform_a', MockPlatform(platform_schema=platform_schema, setup_platform=mock_setup)) @@ -330,29 +342,34 @@ class TestSetup: def test_disable_component_if_invalid_return(self): """Test disabling component if invalid return.""" loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: None)) assert not setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is None + assert loader.get_component(self.hass, 'disabled_component') is None assert 'disabled_component' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: False)) assert not setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is not None + assert loader.get_component( + self.hass, 'disabled_component') is not None assert 'disabled_component' not in self.hass.config.components self.hass.data.pop(setup.DATA_SETUP) loader.set_component( + self.hass, 'disabled_component', MockModule('disabled_component', setup=lambda hass, config: True)) assert setup.setup_component(self.hass, 'disabled_component') - assert loader.get_component('disabled_component') is not None + assert loader.get_component( + self.hass, 'disabled_component') is not None assert 'disabled_component' in self.hass.config.components def test_all_work_done_before_start(self): @@ -373,14 +390,17 @@ class TestSetup: return True loader.set_component( + self.hass, 'test_component1', MockModule('test_component1', setup=component1_setup)) loader.set_component( + self.hass, 'test_component2', MockModule('test_component2', setup=component_track_setup)) loader.set_component( + self.hass, 'test_component3', MockModule('test_component3', setup=component_track_setup)) @@ -409,7 +429,8 @@ def test_component_cannot_depend_config(hass): @asyncio.coroutine def test_component_warn_slow_setup(hass): """Warn we log when a component setup takes a long time.""" - loader.set_component('test_component1', MockModule('test_component1')) + loader.set_component( + hass, 'test_component1', MockModule('test_component1')) with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ as mock_call: result = yield from setup.async_setup_component( @@ -430,7 +451,7 @@ def test_component_warn_slow_setup(hass): def test_platform_no_warn_slow(hass): """Do not warn for long entity setup time.""" loader.set_component( - 'test_component1', + hass, 'test_component1', MockModule('test_component1', platform_schema=PLATFORM_SCHEMA)) with mock.patch.object(hass.loop, 'call_later', mock.MagicMock()) \ as mock_call: diff --git a/tests/testing_config/custom_components/test_standalone.py b/tests/testing_config/custom_components/test_standalone.py index f0d4ba7982b..de3a360a4da 100644 --- a/tests/testing_config/custom_components/test_standalone.py +++ b/tests/testing_config/custom_components/test_standalone.py @@ -2,6 +2,6 @@ DOMAIN = 'test_standalone' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True From 2f0fc0934f0eae56b9a833e8e449043bb79d5d40 Mon Sep 17 00:00:00 2001 From: corneyl Date: Tue, 1 May 2018 21:06:41 +0200 Subject: [PATCH 569/924] Buienradar improvements: continuous sensors and unique ID's (#13249) * Force update continuous sensors when new measurement available. * Added unique ID's based on coordinates, sensor type and client name. * Fixed over-indentation (hound review) * Revert "Added unique ID's based on coordinates, sensor type and client name." This reverts commit 3345e67a155c7953afc42c1b1b676616a7a77e56. * Fix lint errors. * Re-added unique ID's based on location. * Removed wrong error logging. * Removed creating UUID from unique id * Lint --- homeassistant/components/sensor/buienradar.py | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 5d74f038eaa..6eb67f7cbd8 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -161,7 +161,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: - dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'))) + dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'), + coordinates)) async_add_devices(dev) data = BrData(hass, coordinates, timeframe, dev) @@ -172,9 +173,9 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BrSensor(Entity): """Representation of an Buienradar sensor.""" - def __init__(self, sensor_type, client_name): + def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.buienradar import (PRECIPITATION_FORECAST) + from buienradar.buienradar import (PRECIPITATION_FORECAST, CONDITION) self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -185,10 +186,22 @@ class BrSensor(Entity): self._attribution = None self._measured = None self._stationname = None + self._unique_id = self.uid(coordinates) + + # All continuous sensors should be forced to be updated + self._force_update = self.type != SYMBOL and \ + not self.type.startswith(CONDITION) if self.type.startswith(PRECIPITATION_FORECAST): self._timeframe = None + def uid(self, coordinates): + """Generate a unique id using coordinates and sensor type.""" + # The combination of the location, name an sensor type is unique + return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], + coordinates[CONF_LONGITUDE], + self.type) + def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor @@ -198,6 +211,11 @@ class BrSensor(Entity): PRECIPITATION_FORECAST, STATIONNAME, TIMEFRAME) + # Check if we have a new measurement, + # otherwise we do not have to update the sensor + if self._measured == data.get(MEASURED): + return False + self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) @@ -246,18 +264,12 @@ class BrSensor(Entity): return False else: try: - new_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + self._state = data.get(FORECAST)[fcday].get(self.type[:-3]) + return True except IndexError: _LOGGER.warning("No forecast for fcday=%s...", fcday) return False - if new_state != self._state: - self._state = new_state - return True - return False - - return False - if self.type == SYMBOL or self.type.startswith(CONDITION): # update weather symbol & status text condition = data.get(CONDITION, None) @@ -286,27 +298,26 @@ class BrSensor(Entity): if self.type.startswith(PRECIPITATION_FORECAST): # update nested precipitation forecast sensors nested = data.get(PRECIPITATION_FORECAST) - new_state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) self._timeframe = nested.get(TIMEFRAME) # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True - return False + self._state = nested.get(self.type[len(PRECIPITATION_FORECAST)+1:]) + return True # update all other sensors - new_state = data.get(self.type) # pylint: disable=protected-access - if new_state != self._state: - self._state = new_state - return True - return False + self._state = data.get(self.type) + return True @property def attribution(self): """Return the attribution.""" return self._attribution + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + @property def name(self): """Return the name of the sensor.""" @@ -360,6 +371,11 @@ class BrSensor(Entity): """Return possible sensor specific icon.""" return SENSOR_TYPES[self.type][2] + @property + def force_update(self): + """Return true for continuous sensors, false for discrete sensors.""" + return self._force_update + class BrData(object): """Get the latest data and updates the states.""" From 8d5c3a2b91c8e074638168511c4f0371d452292e Mon Sep 17 00:00:00 2001 From: escoand Date: Tue, 1 May 2018 21:20:38 +0200 Subject: [PATCH 570/924] add volumio discovery (#14220) * add volumio discovery * add missing library * Update volumio.py --- homeassistant/components/discovery.py | 1 + .../components/media_player/volumio.py | 29 ++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 69d0f4796ff..07eb5aaab82 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -79,6 +79,7 @@ SERVICE_HANDLERS = { 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), + 'volumio': ('media_player', 'volumio'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/media_player/volumio.py b/homeassistant/components/media_player/volumio.py index 0a940c0aa9d..11ab1615617 100644 --- a/homeassistant/components/media_player/volumio.py +++ b/homeassistant/components/media_player/volumio.py @@ -8,6 +8,7 @@ Volumio rest API: https://volumio.github.io/docs/API/REST_API.html """ from datetime import timedelta import logging +import socket import asyncio import aiohttp @@ -31,6 +32,8 @@ DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Volumio' DEFAULT_PORT = 3000 +DATA_VOLUMIO = 'volumio' + TIMEOUT = 10 SUPPORT_VOLUMIO = SUPPORT_PAUSE | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ @@ -50,11 +53,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Volumio platform.""" - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME) + if DATA_VOLUMIO not in hass.data: + hass.data[DATA_VOLUMIO] = dict() - async_add_devices([Volumio(name, host, port, hass)]) + # This is a manual configuration? + if discovery_info is None: + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + else: + name = "{} ({})".format(DEFAULT_NAME, discovery_info.get('hostname')) + host = discovery_info.get('host') + port = discovery_info.get('port') + + # Only add a device once, so discovered devices do not override manual + # config. + ip_addr = socket.gethostbyname(host) + if ip_addr in hass.data[DATA_VOLUMIO]: + return + + entity = Volumio(name, host, port, hass) + + hass.data[DATA_VOLUMIO][ip_addr] = entity + async_add_devices([entity]) class Volumio(MediaPlayerDevice): From 7a054719129bcc8f0fa3dc9861f2b6f56a3ee981 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 1 May 2018 13:36:43 -0600 Subject: [PATCH 571/924] Converts RainMachine to hub model (part 2) (#14225) * Converts RainMachine to hub model (part 2) * Small style adjustments for consistency * Moving MAC calculation to one-time call in component * Removing unneeded attribute * Bumping Travis * Lint --- homeassistant/components/rainmachine.py | 26 ++++++++-- .../components/switch/rainmachine.py | 47 ++++++++----------- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py index 4c8b8a1114f..99cec53c2ed 100644 --- a/homeassistant/components/rainmachine.py +++ b/homeassistant/components/rainmachine.py @@ -8,11 +8,10 @@ import logging from datetime import timedelta import voluptuous as vol -from requests.exceptions import ConnectTimeout -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL) + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES) REQUIREMENTS = ['regenmaschine==0.4.1'] @@ -24,6 +23,8 @@ DOMAIN = 'rainmachine' NOTIFICATION_ID = 'rainmachine_notification' NOTIFICATION_TITLE = 'RainMachine Component Setup' +CONF_ZONE_RUN_TIME = 'zone_run_time' + DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' DEFAULT_PORT = 8080 DEFAULT_SSL = True @@ -31,6 +32,11 @@ DEFAULT_SSL = True MIN_SCAN_TIME = timedelta(seconds=1) MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) +SWITCH_SCHEMA = vol.Schema({ + vol.Optional(CONF_ZONE_RUN_TIME): + cv.positive_int +}) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema({ @@ -38,6 +44,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, }) }, extra=vol.ALLOW_EXTRA) @@ -47,6 +54,7 @@ def setup(hass, config): """Set up the RainMachine component.""" from regenmaschine import Authenticator, Client from regenmaschine.exceptions import HTTPError + from requests.exceptions import ConnectTimeout conf = config[DOMAIN] ip_address = conf[CONF_IP_ADDRESS] @@ -54,11 +62,14 @@ def setup(hass, config): port = conf[CONF_PORT] ssl = conf[CONF_SSL] + _LOGGER.debug('Setting up RainMachine client') + try: auth = Authenticator.create_local( ip_address, password, port=port, https=ssl) client = Client(auth) - hass.data[DATA_RAINMACHINE] = client + mac = client.provision.wifi()['macAddress'] + hass.data[DATA_RAINMACHINE] = (client, mac) except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: _LOGGER.error('An error occurred: %s', str(exc_info)) hass.components.persistent_notification.create( @@ -68,4 +79,11 @@ def setup(hass, config): title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) return False + + _LOGGER.debug('Setting up switch platform') + switch_config = conf.get(CONF_SWITCHES, {}) + discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) + + _LOGGER.debug('Setup complete') + return True diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index cdada7ce274..8306b323330 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -2,40 +2,33 @@ from logging import getLogger -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv from homeassistant.components.rainmachine import ( - DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, + MIN_SCAN_TIME_FORCED) +from homeassistant.components.switch import SwitchDevice +from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.util import Throttle -_LOGGER = getLogger(__name__) DEPENDENCIES = ['rainmachine'] +_LOGGER = getLogger(__name__) + ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' -CONF_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_ZONE_RUN_SECONDS = 60 * 10 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): - cv.positive_int -}) +DEFAULT_ZONE_RUN = 60 * 10 def setup_platform(hass, config, add_devices, discovery_info=None): """Set this component up under its platform.""" - client = hass.data.get(DATA_RAINMACHINE) - device_name = client.provision.device_name()['name'] - device_mac = client.provision.wifi()['macAddress'] + if discovery_info is None: + return - _LOGGER.debug('Config received: %s', config) + _LOGGER.debug('Config received: %s', discovery_info) - zone_run_time = config[CONF_ZONE_RUN_TIME] + zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) + + client, device_mac = hass.data.get(DATA_RAINMACHINE) entities = [] for program in client.programs.all().get('programs', {}): @@ -44,7 +37,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.debug('Adding program: %s', program) entities.append( - RainMachineProgram(client, device_name, device_mac, program)) + RainMachineProgram(client, device_mac, program)) for zone in client.zones.all().get('zones', {}): if not zone.get('active'): @@ -52,7 +45,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.debug('Adding zone: %s', zone) entities.append( - RainMachineZone(client, device_name, device_mac, zone, + RainMachineZone(client, device_mac, zone, zone_run_time)) add_devices(entities, True) @@ -61,18 +54,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RainMachineEntity(SwitchDevice): """A class to represent a generic RainMachine entity.""" - def __init__(self, client, device_name, device_mac, entity_json): + def __init__(self, client, device_mac, entity_json): """Initialize a generic RainMachine entity.""" self._api_type = 'remote' if client.auth.using_remote_api else 'local' self._client = client self._entity_json = entity_json self.device_mac = device_mac - self.device_name = device_name self._attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, - ATTR_DEVICE_CLASS: self.device_name + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION } @property @@ -156,10 +147,10 @@ class RainMachineProgram(RainMachineEntity): class RainMachineZone(RainMachineEntity): """A RainMachine zone.""" - def __init__(self, client, device_name, device_mac, zone_json, + def __init__(self, client, device_mac, zone_json, zone_run_time): """Initialize a RainMachine zone.""" - super().__init__(client, device_name, device_mac, zone_json) + super().__init__(client, device_mac, zone_json) self._run_time = zone_run_time self._attrs.update({ ATTR_CYCLES: self._entity_json.get('noOfCycles'), From e4655a7e63ec95502b2ac78cce4bc931a75328ba Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 1 May 2018 21:38:08 +0200 Subject: [PATCH 572/924] Add MQTT Sensor device_class (#14033) * Add MQTT Sensor device_class * Add test --- homeassistant/components/sensor/mqtt.py | 15 ++++++++--- tests/components/sensor/test_mqtt.py | 36 ++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index d7d66a3a145..997fd312a6a 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -15,9 +15,10 @@ from homeassistant.core import callback from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, MqttAvailability) +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, - CONF_UNIT_OF_MEASUREMENT, CONF_ICON) + CONF_UNIT_OF_MEASUREMENT, CONF_ICON, CONF_DEVICE_CLASS) from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -39,6 +40,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -66,6 +68,7 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, config.get(CONF_FORCE_UPDATE), config.get(CONF_EXPIRE_AFTER), config.get(CONF_ICON), + config.get(CONF_DEVICE_CLASS), value_template, config.get(CONF_JSON_ATTRS), config.get(CONF_UNIQUE_ID), @@ -79,8 +82,8 @@ class MqttSensor(MqttAvailability, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, name, state_topic, qos, unit_of_measurement, - force_update, expire_after, icon, value_template, - json_attributes, unique_id: Optional[str], + force_update, expire_after, icon, device_class: Optional[str], + value_template, json_attributes, unique_id: Optional[str], availability_topic, payload_available, payload_not_available): """Initialize the sensor.""" @@ -95,6 +98,7 @@ class MqttSensor(MqttAvailability, Entity): self._template = value_template self._expire_after = expire_after self._icon = icon + self._device_class = device_class self._expiration_trigger = None self._json_attributes = set(json_attributes) self._unique_id = unique_id @@ -190,3 +194,8 @@ class MqttSensor(MqttAvailability, Entity): def icon(self): """Return the icon.""" return self._icon + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 88e74e11008..2583f52b3d2 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -10,7 +10,8 @@ import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util -from tests.common import mock_mqtt_component, fire_mqtt_message +from tests.common import mock_mqtt_component, fire_mqtt_message, \ + assert_setup_component from tests.common import get_test_home_assistant, mock_component @@ -350,3 +351,36 @@ class TestSensorMQTT(unittest.TestCase): self.hass.block_till_done() assert len(self.hass.states.all()) == 1 + + def test_invalid_device_class(self): + """Test device_class option with invalid value.""" + with assert_setup_component(0): + assert setup_component(self.hass, 'sensor', { + 'sensor': { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'foobarnotreal' + } + }) + + def test_valid_device_class(self): + """Test device_class option with valid values.""" + assert setup_component(self.hass, 'sensor', { + 'sensor': [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'temperature' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + }] + }) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test_1') + assert state.attributes['device_class'] == 'temperature' + state = self.hass.states.get('sensor.test_2') + assert 'device_class' not in state.attributes From c2d00be91ea4acb90ba944723fe42fe6a6b8a218 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Tue, 1 May 2018 14:38:45 -0500 Subject: [PATCH 573/924] Allow independent control of white level on flux_led component (#13985) * Allow independent control of white level on flux_led component. Also preserve brightness on color change. * Limit white value support to RGBW mode. * Requested changes. * Correct liniting issues * Formatting --- homeassistant/components/light/flux_led.py | 39 +++++++++++++++------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 6ffdcc0bb4a..6c7f2e98e37 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -12,9 +12,9 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL from homeassistant.components.light import ( - ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, EFFECT_COLORLOOP, - EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, - SUPPORT_COLOR, Light, PLATFORM_SCHEMA) + ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_EFFECT, ATTR_WHITE_VALUE, + EFFECT_COLORLOOP, EFFECT_RANDOM, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, PLATFORM_SCHEMA) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -191,8 +191,16 @@ class FluxLight(Light): @property def supported_features(self): """Flag supported features.""" + if self._mode is MODE_RGBW: + return SUPPORT_FLUX_LED | SUPPORT_WHITE_VALUE + return SUPPORT_FLUX_LED + @property + def white_value(self): + """Return the white value of this light between 0..255.""" + return self._bulb.getRgbw()[3] + @property def effect_list(self): """Return the list of supported effects.""" @@ -212,24 +220,31 @@ class FluxLight(Light): brightness = kwargs.get(ATTR_BRIGHTNESS) effect = kwargs.get(ATTR_EFFECT) + white = kwargs.get(ATTR_WHITE_VALUE) - if rgb is not None and brightness is not None: - self._bulb.setRgb(*tuple(rgb), brightness=brightness) - elif rgb is not None: - self._bulb.setRgb(*tuple(rgb)) + # color change only + if rgb is not None: + self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) + + # brightness change only elif brightness is not None: - if self._mode == MODE_RGBW: - self._bulb.setWarmWhite255(brightness) - elif self._mode == MODE_RGB: - (red, green, blue) = self._bulb.getRgb() - self._bulb.setRgb(red, green, blue, brightness=brightness) + (red, green, blue) = self._bulb.getRgb() + self._bulb.setRgb(red, green, blue, brightness=brightness) + + # random color effect elif effect == EFFECT_RANDOM: self._bulb.setRgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + + # effect selection elif effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + # white change only + elif white is not None: + self._bulb.setWarmWhite255(white) + def turn_off(self, **kwargs): """Turn the specified or all lights off.""" self._bulb.turnOff() From 6453ea4e6181924cddf72ea58eb0c72e942b0772 Mon Sep 17 00:00:00 2001 From: Mohamad Tarbin Date: Tue, 1 May 2018 16:27:20 -0400 Subject: [PATCH 574/924] Add Social Blade Sensor (#14060) * Adding Dominion Energy Sensor * Update : remove white spacves and set the update time to be daily * Update : update spacing as per hound suggestions, Move imports * Update : Fix Travis CI build errors * Update Documentations on method levels * Update Documentations on method levels * Update Documentations on method levels * Add Exception Handeling if login failed, add PLATFORM_SCHEMA * Add Exception Handeling if login failed, add PLATFORM_SCHEMA * Add Exception Handeling if login failed, add PLATFORM_SCHEMA * Update dominionenergy.py * Adding Selenium to requirements_all.txt * Checking the username/password while setup * Checking the username/password while setup * removing extra white space * Update : Adding the Platform only if credentials works * Update : Add PlatformNotReady exception * Update : Add PlatformNotReady exception * Update .coveragerc * Remove change * Adding USCIS component * Adding Line after the class DOC * Update : Extract USCIS logic code to Component * Update : Extract USCIS logic code to Component * Adding CURRENT_STATUS * Change Error handling, remove date from attributes * Update the Version for USCIS * Add Social Blade Sensor * Update class documentation * Update coverage and requirements_all * Update : houndci error with intent * Update : Add coverage * Update uscis.py * Add comments * Add comments * Delete dominionenergy.py * Update requirements_all.txt * Update .coveragerc * Update .coveragerc * Update .coveragerc * Update : update after code review * Fix remaining issues --- .coveragerc | 1 + .../components/sensor/socialblade.py | 90 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 94 insertions(+) create mode 100644 homeassistant/components/sensor/socialblade.py diff --git a/.coveragerc b/.coveragerc index 1852d7d7365..94722666c05 100644 --- a/.coveragerc +++ b/.coveragerc @@ -661,6 +661,7 @@ omit = homeassistant/components/sensor/sma.py homeassistant/components/sensor/snmp.py homeassistant/components/sensor/sochain.py + homeassistant/components/sensor/socialblade.py homeassistant/components/sensor/sonarr.py homeassistant/components/sensor/speedtest.py homeassistant/components/sensor/spotcrime.py diff --git a/homeassistant/components/sensor/socialblade.py b/homeassistant/components/sensor/socialblade.py new file mode 100644 index 00000000000..1e0084e1404 --- /dev/null +++ b/homeassistant/components/sensor/socialblade.py @@ -0,0 +1,90 @@ +""" +Support for Social Blade. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.socialblade/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['socialbladeclient==0.2'] + +CHANNEL_ID = 'channel_id' + +DEFAULT_NAME = "Social Blade" + +MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) + +SUBSCRIBERS = 'subscribers' + +TOTAL_VIEWS = 'total_views' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CHANNEL_ID): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Social Blade sensor.""" + social_blade = SocialBladeSensor( + config[CHANNEL_ID], config[CONF_NAME]) + + social_blade.update() + if social_blade.valid_channel_id is False: + return + + add_devices([social_blade]) + + +class SocialBladeSensor(Entity): + """Representation of a Social Blade Sensor.""" + + def __init__(self, case, name): + """Initialize the Social Blade sensor.""" + self._state = None + self.channel_id = case + self._attributes = None + self.valid_channel_id = None + self._name = name + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._attributes: + return self._attributes + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the latest data from Social Blade.""" + import socialbladeclient + try: + data = socialbladeclient.get_data(self.channel_id) + self._attributes = {TOTAL_VIEWS: data[TOTAL_VIEWS]} + self._state = data[SUBSCRIBERS] + self.valid_channel_id = True + + except (ValueError, IndexError): + _LOGGER.error("Unable to find valid channel ID") + self.valid_channel_id = False + self._attributes = None diff --git a/requirements_all.txt b/requirements_all.txt index 93bf26f5239..a609ee5f7f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1203,6 +1203,9 @@ smappy==0.2.15 # homeassistant.components.media_player.snapcast snapcast==2.0.8 +# homeassistant.components.sensor.socialblade +socialbladeclient==0.2 + # homeassistant.components.climate.honeywell somecomfort==0.5.2 From e968b1a0f4e63ea862068f960d3777088fee7639 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 2 May 2018 14:15:30 +0100 Subject: [PATCH 575/924] UPnP code cleanup (#14235) * missing async calls * lint * cleanup --- homeassistant/components/sensor/upnp.py | 10 ++++++++-- homeassistant/components/upnp.py | 12 ++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/upnp.py b/homeassistant/components/sensor/upnp.py index e0c57ca9ac6..07b63553fcb 100644 --- a/homeassistant/components/sensor/upnp.py +++ b/homeassistant/components/sensor/upnp.py @@ -11,6 +11,8 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) +DEPENDENCIES = ['upnp'] + BYTES_RECEIVED = 1 BYTES_SENT = 2 PACKETS_RECEIVED = 3 @@ -25,12 +27,16 @@ SENSOR_TYPES = { } -async def async_setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the IGD sensors.""" + if discovery_info is None: + return + device = hass.data[DATA_UPNP] service = device.find_first_service(CIC_SERVICE) unit = discovery_info['unit'] - add_devices([ + async_add_devices([ IGDSensor(service, t, unit if SENSOR_TYPES[t][1] else '#') for t in SENSOR_TYPES], True) diff --git a/homeassistant/components/upnp.py b/homeassistant/components/upnp.py index 26a59746aea..8aeb93fed25 100644 --- a/homeassistant/components/upnp.py +++ b/homeassistant/components/upnp.py @@ -50,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_ENABLE_PORT_MAPPING, default=True): cv.boolean, vol.Optional(CONF_UNITS, default="MBytes"): vol.In(UNITS), - vol.Optional(CONF_LOCAL_IP): ip_address, + vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), vol.Optional(CONF_PORTS): vol.Schema({vol.Any(CONF_HASS, cv.positive_int): cv.positive_int}) }), @@ -62,9 +62,7 @@ async def async_setup(hass, config): config = config[DOMAIN] host = config.get(CONF_LOCAL_IP) - if host is not None: - host = str(host) - else: + if host is None: host = get_local_ip() if host == '127.0.0.1': @@ -90,10 +88,8 @@ async def async_setup(hass, config): service = device.find_first_service(IP_SERVICE) if _service['serviceType'] == CIC_SERVICE: unit = config.get(CONF_UNITS) - discovery.load_platform(hass, 'sensor', - DOMAIN, - {'unit': unit}, - config) + hass.async_add_job(discovery.async_load_platform( + hass, 'sensor', DOMAIN, {'unit': unit}, config)) except UpnpSoapError as error: _LOGGER.error(error) return False From 8b13658d3b39c1deadd5d6a88c102b8a856d80f3 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Wed, 2 May 2018 15:21:50 +0200 Subject: [PATCH 576/924] Improve config schema of the blackbird component (#14007) * Import moved, return values removed and redundant log message removed * Improve config schema of the blackbird component * Tests updated * Handle updated * Schema fixed --- .../components/media_player/blackbird.py | 56 +++++++++---------- .../components/media_player/test_blackbird.py | 43 ++++++-------- 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/media_player/blackbird.py b/homeassistant/components/media_player/blackbird.py index 37b3c0ff819..1c976f5eecd 100644 --- a/homeassistant/components/media_player/blackbird.py +++ b/homeassistant/components/media_player/blackbird.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/media_player.blackbird """ import logging +import socket import voluptuous as vol @@ -50,71 +51,68 @@ ZONE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) # Valid source ids: 1-8 SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=8)) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_TYPE): vol.In(['serial', 'socket']), - vol.Optional(CONF_PORT): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), - vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), -}) +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + PLATFORM_SCHEMA.extend({ + vol.Exclusive(CONF_PORT, CONF_TYPE): cv.string, + vol.Exclusive(CONF_HOST, CONF_TYPE): cv.string, + vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), + vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), + })) # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Monoprice Blackbird 4k 8x8 HDBaseT Matrix platform.""" + if DATA_BLACKBIRD not in hass.data: + hass.data[DATA_BLACKBIRD] = {} + port = config.get(CONF_PORT) host = config.get(CONF_HOST) - device_type = config.get(CONF_TYPE) - import socket from pyblackbird import get_blackbird from serial import SerialException - if device_type == 'serial': - if port is None: - _LOGGER.error("No port configured") - return + connection = None + if port is not None: try: blackbird = get_blackbird(port) + connection = port except SerialException: _LOGGER.error("Error connecting to the Blackbird controller") return - elif device_type == 'socket': + if host is not None: try: - if host is None: - _LOGGER.error("No host configured") - return blackbird = get_blackbird(host, False) + connection = host except socket.timeout: _LOGGER.error("Error connecting to the Blackbird controller") return - else: - _LOGGER.error("Incorrect device type specified") - return - sources = {source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items()} - hass.data[DATA_BLACKBIRD] = [] + devices = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - hass.data[DATA_BLACKBIRD].append(BlackbirdZone( - blackbird, sources, zone_id, extra[CONF_NAME])) + unique_id = "{}-{}".format(connection, zone_id) + device = BlackbirdZone(blackbird, sources, zone_id, extra[CONF_NAME]) + hass.data[DATA_BLACKBIRD][unique_id] = device + devices.append(device) - add_devices(hass.data[DATA_BLACKBIRD], True) + add_devices(devices, True) def service_handle(service): """Handle for services.""" entity_ids = service.data.get(ATTR_ENTITY_ID) source = service.data.get(ATTR_SOURCE) if entity_ids: - devices = [device for device in hass.data[DATA_BLACKBIRD] + devices = [device for device in hass.data[DATA_BLACKBIRD].values() if device.entity_id in entity_ids] else: - devices = hass.data[DATA_BLACKBIRD] + devices = hass.data[DATA_BLACKBIRD].values() for device in devices: if service.service == SERVICE_SETALLZONES: @@ -146,14 +144,13 @@ class BlackbirdZone(MediaPlayerDevice): """Retrieve latest state.""" state = self._blackbird.zone_status(self._zone_id) if not state: - return False + return self._state = STATE_ON if state.power else STATE_OFF idx = state.av if idx in self._source_id_name: self._source = self._source_id_name[idx] else: self._source = None - return True @property def name(self): @@ -187,7 +184,6 @@ class BlackbirdZone(MediaPlayerDevice): def set_all_zones(self, source): """Set all zones to one source.""" - _LOGGER.debug("Setting all zones") if source not in self._source_name_id: return idx = self._source_name_id[source] diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index 86bfdfb52c4..eea6295b79e 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -59,7 +59,6 @@ class TestBlackbirdSchema(unittest.TestCase): """Test valid schema.""" valid_schema = { 'platform': 'blackbird', - 'type': 'serial', 'port': '/dev/ttyUSB0', 'zones': {1: {'name': 'a'}, 2: {'name': 'a'}, @@ -87,8 +86,7 @@ class TestBlackbirdSchema(unittest.TestCase): """Test valid schema.""" valid_schema = { 'platform': 'blackbird', - 'type': 'socket', - 'port': '192.168.1.50', + 'host': '192.168.1.50', 'zones': {1: {'name': 'a'}, 2: {'name': 'a'}, 3: {'name': 'a'}, @@ -109,10 +107,18 @@ class TestBlackbirdSchema(unittest.TestCase): schemas = ( {}, # Empty None, # None - # Missing type + # Port and host used concurrently + { + 'platform': 'blackbird', + 'port': '/dev/ttyUSB0', + 'host': '192.168.1.50', + 'name': 'Name', + 'zones': {1: {'name': 'a'}}, + 'sources': {1: {'name': 'b'}}, + }, + # Port or host missing { 'platform': 'blackbird', - 'port': 'aaa', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, @@ -120,8 +126,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Invalid zone number { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {11: {'name': 'a'}}, 'sources': {1: {'name': 'b'}}, @@ -129,8 +134,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Invalid source number { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {9: {'name': 'b'}}, @@ -138,8 +142,7 @@ class TestBlackbirdSchema(unittest.TestCase): # Zone missing name { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {}}, 'sources': {1: {'name': 'b'}}, @@ -147,21 +150,11 @@ class TestBlackbirdSchema(unittest.TestCase): # Source missing name { 'platform': 'blackbird', - 'type': 'serial', - 'port': 'aaa', + 'port': '/dev/ttyUSB0', 'name': 'Name', 'zones': {1: {'name': 'a'}}, 'sources': {1: {}}, }, - # Invalid type - { - 'platform': 'blackbird', - 'type': 'aaa', - 'port': 'aaa', - 'name': 'Name', - 'zones': {1: {'name': 'a'}}, - 'sources': {1: {'name': 'b'}}, - }, ) for value in schemas: with self.assertRaises(vol.MultipleInvalid): @@ -181,7 +174,6 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): new=lambda *a: self.blackbird): setup_platform(self.hass, { 'platform': 'blackbird', - 'type': 'serial', 'port': '/dev/ttyUSB0', 'zones': {3: {'name': 'Zone name'}}, 'sources': {1: {'name': 'one'}, @@ -189,7 +181,7 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): 2: {'name': 'two'}}, }, lambda *args, **kwargs: None, {}) self.hass.block_till_done() - self.media_player = self.hass.data[DATA_BLACKBIRD][0] + self.media_player = self.hass.data[DATA_BLACKBIRD]['/dev/ttyUSB0-3'] self.media_player.hass = self.hass self.media_player.entity_id = 'media_player.zone_3' @@ -203,7 +195,8 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): self.assertTrue(self.hass.services.has_service(DOMAIN, SERVICE_SETALLZONES)) self.assertEqual(len(self.hass.data[DATA_BLACKBIRD]), 1) - self.assertEqual(self.hass.data[DATA_BLACKBIRD][0].name, 'Zone name') + self.assertEqual(self.hass.data[DATA_BLACKBIRD]['/dev/ttyUSB0-3'].name, + 'Zone name') def test_setallzones_service_call_with_entity_id(self): """Test set all zone source service call with entity id.""" From bf056b6f019199a15ad0b78bf1e5439c60861d61 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 2 May 2018 15:25:08 +0200 Subject: [PATCH 577/924] Fix Hue color state for missing xy (#14230) --- homeassistant/components/light/hue.py | 2 +- tests/components/light/test_hue.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 9f662718514..837a6f82510 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -245,7 +245,7 @@ class HueLight(Light): mode = self._color_mode source = self.light.action if self.is_group else self.light.state - if mode in ('xy', 'hs'): + if mode in ('xy', 'hs') and 'xy' in source: return color.color_xy_to_hs(*source['xy']) return None diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 8f5b52ea6de..a1e3867f9c3 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -650,6 +650,19 @@ def test_hs_color(): assert light.hs_color is None + light = hue_light.HueLight( + light=Mock(state={ + 'colormode': 'hs', + 'hue': 1234, + 'sat': 123, + }), + request_bridge_update=None, + bridge=Mock(), + is_group=False, + ) + + assert light.hs_color is None + light = hue_light.HueLight( light=Mock(state={ 'colormode': 'xy', From ce98dfe3959bda7c80542d14e6713b613bd15661 Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Wed, 2 May 2018 15:38:24 +0200 Subject: [PATCH 578/924] Add support for tracking devices on Netgear access points (#13331) * Netgear: add support for tracking devices on access points * Netgear: add SSL support and autodetection --- .../components/device_tracker/netgear.py | 103 ++++++++++++++---- requirements_all.txt | 2 +- 2 files changed, 84 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/device_tracker/netgear.py b/homeassistant/components/device_tracker/netgear.py index 25d5d38b2a7..0e48e3072b2 100644 --- a/homeassistant/components/device_tracker/netgear.py +++ b/homeassistant/components/device_tracker/netgear.py @@ -12,21 +12,27 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT) + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT, CONF_SSL, + CONF_DEVICES, CONF_EXCLUDE) -REQUIREMENTS = ['pynetgear==0.3.3'] +REQUIREMENTS = ['pynetgear==0.4.0'] _LOGGER = logging.getLogger(__name__) -DEFAULT_HOST = 'routerlogin.net' -DEFAULT_USER = 'admin' -DEFAULT_PORT = 5000 +CONF_APS = 'accesspoints' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=DEFAULT_USER): cv.string, + vol.Optional(CONF_HOST, default=''): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_USERNAME, default=''): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port + vol.Optional(CONF_PORT, default=None): vol.Any(None, cv.port), + vol.Optional(CONF_DEVICES, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_EXCLUDE, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_APS, default=[]): + vol.All(cv.ensure_list, [cv.string]), }) @@ -34,11 +40,16 @@ def get_scanner(hass, config): """Validate the configuration and returns a Netgear scanner.""" info = config[DOMAIN] host = info.get(CONF_HOST) + ssl = info.get(CONF_SSL) username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) port = info.get(CONF_PORT) + devices = info.get(CONF_DEVICES) + excluded_devices = info.get(CONF_EXCLUDE) + accesspoints = info.get(CONF_APS) - scanner = NetgearDeviceScanner(host, username, password, port) + scanner = NetgearDeviceScanner(host, ssl, username, password, port, + devices, excluded_devices, accesspoints) return scanner if scanner.success_init else None @@ -46,16 +57,21 @@ def get_scanner(hass, config): class NetgearDeviceScanner(DeviceScanner): """Queries a Netgear wireless router using the SOAP-API.""" - def __init__(self, host, username, password, port): + def __init__(self, host, ssl, username, password, port, devices, + excluded_devices, accesspoints): """Initialize the scanner.""" import pynetgear + self.tracked_devices = devices + self.excluded_devices = excluded_devices + self.tracked_accesspoints = accesspoints + self.last_results = [] - self._api = pynetgear.Netgear(password, host, username, port) + self._api = pynetgear.Netgear(password, host, username, port, ssl) _LOGGER.info("Logging in") - results = self._api.get_attached_devices() + results = self.get_attached_devices() self.success_init = results is not None @@ -68,15 +84,50 @@ class NetgearDeviceScanner(DeviceScanner): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return (device.mac for device in self.last_results) + devices = [] + + for dev in self.last_results: + tracked = (not self.tracked_devices or + dev.mac in self.tracked_devices or + dev.name in self.tracked_devices) + tracked = tracked and (not self.excluded_devices or not( + dev.mac in self.excluded_devices or + dev.name in self.excluded_devices)) + if tracked: + devices.append(dev.mac) + if (self.tracked_accesspoints and + dev.conn_ap_mac in self.tracked_accesspoints): + devices.append(dev.mac + "_" + dev.conn_ap_mac) + + return devices def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - try: - return next(result.name for result in self.last_results - if result.mac == device) - except StopIteration: - return None + """Return the name of the given device or the MAC if we don't know.""" + parts = device.split("_") + mac = parts[0] + ap_mac = None + if len(parts) > 1: + ap_mac = parts[1] + + name = None + for dev in self.last_results: + if dev.mac == mac: + name = dev.name + break + + if not name or name == "--": + name = mac + + if ap_mac: + ap_name = "Router" + for dev in self.last_results: + if dev.mac == ap_mac: + ap_name = dev.name + break + + return name + " on " + ap_name + + return name def _update_info(self): """Retrieve latest information from the Netgear router. @@ -88,9 +139,21 @@ class NetgearDeviceScanner(DeviceScanner): _LOGGER.info("Scanning") - results = self._api.get_attached_devices() + results = self.get_attached_devices() if results is None: _LOGGER.warning("Error scanning devices") self.last_results = results or [] + + def get_attached_devices(self): + """ + List attached devices with pynetgear. + + The v2 method takes more time and is more heavy on the router + so we only use it if we need connected AP info. + """ + if self.tracked_accesspoints: + return self._api.get_attached_devices_2() + + return self._api.get_attached_devices() diff --git a/requirements_all.txt b/requirements_all.txt index a609ee5f7f2..16f6001d576 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -869,7 +869,7 @@ pymysensors==0.11.1 pynello==1.5.1 # homeassistant.components.device_tracker.netgear -pynetgear==0.3.3 +pynetgear==0.4.0 # homeassistant.components.switch.netio pynetio==0.1.6 From 14c7fa888225e48280ffa7b1e77253467ef8f8e6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 2 May 2018 20:23:07 +0200 Subject: [PATCH 579/924] WUnderground unique ids (#13311) * WUnderground unique_id * Remove async_generate_entity_id * Lint * Address comment --- .../components/sensor/wunderground.py | 48 +++++++++---------- tests/components/sensor/test_wunderground.py | 24 +++++++++- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/wunderground.py b/homeassistant/components/sensor/wunderground.py index bbee167d4b0..7f2df4bcda9 100644 --- a/homeassistant/components/sensor/wunderground.py +++ b/homeassistant/components/sensor/wunderground.py @@ -15,13 +15,13 @@ import voluptuous as vol from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import sensor -from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, TEMP_FAHRENHEIT, TEMP_CELSIUS, LENGTH_INCHES, LENGTH_KILOMETERS, - LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION, CONF_ENTITY_NAMESPACE) + LENGTH_MILES, LENGTH_FEET, ATTR_ATTRIBUTION) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -618,8 +618,6 @@ LANG_CODES = [ 'CY', 'SN', 'JI', 'YI', ] -DEFAULT_ENTITY_NAMESPACE = 'pws' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_PWS_ID): cv.string, @@ -629,31 +627,30 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Inclusive(CONF_LONGITUDE, 'coordinates', 'Latitude and longitude must exist together'): cv.longitude, vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_ENTITY_NAMESPACE, - default=DEFAULT_ENTITY_NAMESPACE): cv.string, + vol.All(cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)]) }) -# Stores a list of entity ids we added in order to support multiple stations -# at once. -ADDED_ENTITY_IDS_KEY = 'wunderground_added_entity_ids' - async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None): """Set up the WUnderground sensor.""" - hass.data.setdefault(ADDED_ENTITY_IDS_KEY, set()) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - namespace = config.get(CONF_ENTITY_NAMESPACE) + pws_id = config.get(CONF_PWS_ID) rest = WUndergroundData( - hass, config.get(CONF_API_KEY), config.get(CONF_PWS_ID), + hass, config.get(CONF_API_KEY), pws_id, config.get(CONF_LANG), latitude, longitude) + + if pws_id is None: + unique_id_base = "@{:06f},{:06f}".format(longitude, latitude) + else: + # Manually specified weather station, use that for unique_id + unique_id_base = pws_id sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: - sensors.append(WUndergroundSensor(hass, rest, variable, namespace)) + sensors.append(WUndergroundSensor(hass, rest, variable, + unique_id_base)) await rest.async_update() if not rest.data: @@ -666,7 +663,7 @@ class WUndergroundSensor(Entity): """Implementing the WUnderground sensor.""" def __init__(self, hass: HomeAssistantType, rest, condition, - namespace: str): + unique_id_base: str): """Initialize the sensor.""" self.rest = rest self._condition = condition @@ -678,12 +675,10 @@ class WUndergroundSensor(Entity): self._entity_picture = None self._unit_of_measurement = self._cfg_expand("unit_of_measurement") self.rest.request_feature(SENSOR_TYPES[condition].feature) - current_ids = set(hass.states.async_entity_ids(sensor.DOMAIN)) - current_ids |= hass.data[ADDED_ENTITY_IDS_KEY] - self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, "{} {}".format(namespace, condition), - current_ids=current_ids) - hass.data[ADDED_ENTITY_IDS_KEY].add(self.entity_id) + # This is only the suggested entity id, it might get changed by + # the entity registry later. + self.entity_id = sensor.ENTITY_ID_FORMAT.format('pws_' + condition) + self._unique_id = "{},{}".format(unique_id_base, condition) def _cfg_expand(self, what, default=None): """Parse and return sensor data.""" @@ -763,6 +758,11 @@ class WUndergroundSensor(Entity): self._entity_picture = re.sub(r'^http://', 'https://', url, flags=re.IGNORECASE) + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + class WUndergroundData(object): """Get data from WUnderground.""" diff --git a/tests/components/sensor/test_wunderground.py b/tests/components/sensor/test_wunderground.py index 65526e2d938..3f490b4ab12 100644 --- a/tests/components/sensor/test_wunderground.py +++ b/tests/components/sensor/test_wunderground.py @@ -148,10 +148,11 @@ def test_invalid_data(hass, aioclient_mock): async def test_entity_id_with_multiple_stations(hass, aioclient_mock): """Test not generating duplicate entity ids with multiple stations.""" aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) config = [ VALID_CONFIG, - {**VALID_CONFIG, 'entity_namespace': 'hi'} + {**VALID_CONFIG_PWS, 'entity_namespace': 'hi'} ] await async_setup_component(hass, 'sensor', {'sensor': config}) await hass.async_block_till_done() @@ -160,6 +161,25 @@ async def test_entity_id_with_multiple_stations(hass, aioclient_mock): assert state is not None assert state.state == 'Clear' - state = hass.states.get('sensor.hi_weather') + state = hass.states.get('sensor.hi_pws_weather') assert state is not None assert state.state == 'Clear' + + +async def test_fails_because_of_unique_id(hass, aioclient_mock): + """Test same config twice fails because of unique_id.""" + aioclient_mock.get(URL, text=load_fixture('wunderground-valid.json')) + aioclient_mock.get(PWS_URL, text=load_fixture('wunderground-valid.json')) + + config = [ + VALID_CONFIG, + {**VALID_CONFIG, 'entity_namespace': 'hi'}, + VALID_CONFIG_PWS + ] + await async_setup_component(hass, 'sensor', {'sensor': config}) + await hass.async_block_till_done() + + states = hass.states.async_all() + expected = len(VALID_CONFIG['monitored_conditions']) + \ + len(VALID_CONFIG_PWS['monitored_conditions']) + assert len(states) == expected From b66be59598c3d47413e2eb4d516216f8b52a3e09 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Wed, 2 May 2018 20:37:41 +0200 Subject: [PATCH 580/924] Add PostNL sensor (Dutch Postal Services) (#12366) * Add basic PostNL sensor (WIP) * Update PostNL sensor * Bump version * Small updates to PostNL package based on feedback * Remove unused import * Pass api to sensor * Refactor based on feedback * Update based on feedback * Fix feedback * Clean up * Bugfiix * Bugfix * SCAN_INTERVAL fix * Remove unused import * Refactor for new wrapper implementation * Update postnl package requirement * Change throttle logic * Update package version * Add new line * Minor changes * Change refresh time to 30 minutes * Update requirements_all.txt --- .coveragerc | 1 + homeassistant/components/sensor/postnl.py | 110 ++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 114 insertions(+) create mode 100644 homeassistant/components/sensor/postnl.py diff --git a/.coveragerc b/.coveragerc index 94722666c05..cf7a5a2cd9c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -640,6 +640,7 @@ omit = homeassistant/components/sensor/plex.py homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pollen.py + homeassistant/components/sensor/postnl.py homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py homeassistant/components/sensor/pyload.py diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py new file mode 100644 index 00000000000..c38f58b7916 --- /dev/null +++ b/homeassistant/components/sensor/postnl.py @@ -0,0 +1,110 @@ +""" +Sensor for PostNL packages. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.postnl/ +""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_PASSWORD, CONF_USERNAME) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['postnl_api==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Information provided by PostNL' + +DEFAULT_NAME = 'postnl' + +ICON = 'mdi:package-variant-closed' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the PostNL sensor platform.""" + from postnl_api import PostNL_API, UnauthorizedException + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + name = config.get(CONF_NAME) + + try: + api = PostNL_API(username, password) + + except UnauthorizedException: + _LOGGER.exception("Can't connect to the PostNL webservice") + return + + add_devices([PostNLSensor(api, name)], True) + + +class PostNLSensor(Entity): + """Representation of a PostNL sensor.""" + + def __init__(self, api, name): + """Initialize the PostNL sensor.""" + self._name = name + self._attributes = None + self._state = None + self._api = api + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return 'package(s)' + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def icon(self): + """Icon to use in the frontend.""" + return ICON + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update device state.""" + shipments = self._api.get_relevant_shipments() + status_counts = {} + + for shipment in shipments: + status = shipment['status']['formatted']['short'] + status = self._api.parse_datetime(status, '%d-%m-%Y', '%H:%M') + + name = shipment['settings']['title'] + status_counts[name] = status + + self._attributes = { + ATTR_ATTRIBUTION: ATTRIBUTION, + **status_counts + } + + self._state = len(status_counts) diff --git a/requirements_all.txt b/requirements_all.txt index 16f6001d576..1283011d7ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -632,6 +632,9 @@ pmsensor==0.4 # homeassistant.components.sensor.pocketcasts pocketcasts==0.1 +# homeassistant.components.sensor.postnl +postnl_api==1.0.1 + # homeassistant.components.climate.proliphix proliphix==0.4.1 From 351e8921fa2e39d0711bc28a65b33ddd887de6db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Osb=C3=A4ck?= Date: Wed, 2 May 2018 21:06:09 +0200 Subject: [PATCH 581/924] python_openzwave update config service (#12060) * update python-openzwave to 4.1.0 * add service which updates the configuration files from github * 0.4.3 --- homeassistant/components/zwave/__init__.py | 7 +++++++ homeassistant/components/zwave/const.py | 1 + homeassistant/components/zwave/services.yaml | 3 +++ 3 files changed, 11 insertions(+) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d56b4bc91b4..c2d4a13a934 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -359,6 +359,11 @@ def setup(hass, config): _LOGGER.info("Z-Wave soft_reset have been initialized") network.controller.soft_reset() + def update_config(service): + """Update the config from git.""" + _LOGGER.info("Configuration update has been initialized") + network.controller.update_ozw_config() + def test_network(service): """Test the network by sending commands to all the nodes.""" _LOGGER.info("Z-Wave test_network have been initialized") @@ -616,6 +621,8 @@ def setup(hass, config): hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) + hass.services.register(DOMAIN, const.SERVICE_UPDATE_CONFIG, + update_config) hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 8e1a22047c1..b42b6d0fce7 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -51,6 +51,7 @@ SERVICE_RENAME_VALUE = "rename_value" SERVICE_REFRESH_ENTITY = "refresh_entity" SERVICE_REFRESH_NODE = "refresh_node" SERVICE_RESET_NODE_METERS = "reset_node_meters" +SERVICE_UPDATE_CONFIG = "update_config" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 61855143d59..1762c33237d 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -119,6 +119,9 @@ set_wakeup: value: description: Value of the interval to set. (integer) +update_config: + description: Attempt to update ozw configuration files from git to support newer devices. + start_network: description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. From f72d5683749340ed5c65bb177d0de61f1e914fd8 Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 2 May 2018 23:10:26 +0300 Subject: [PATCH 582/924] Add unique_id to zwave node entity (#14201) * Add unique_id to zwave node entity * Wait 30s before adding zwave node if its unique_id is not ready * Use only node_id in unique_id. Update name, manufacturer, and product attributes on node update. --- homeassistant/components/zwave/__init__.py | 47 +++++++++++++++---- homeassistant/components/zwave/const.py | 1 + homeassistant/components/zwave/node_entity.py | 17 +++++++ tests/components/zwave/test_init.py | 41 ++++++++++++++++ tests/components/zwave/test_node_entity.py | 13 ++++- tests/mock/zwave.py | 4 ++ 6 files changed, 113 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index c2d4a13a934..01b17023c12 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -297,15 +297,46 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" entity = ZWaveNodeEntity(node, network) - name = node_name(node) - generated_id = generate_entity_id(DOMAIN + '.{}', name, []) - node_config = device_config.get(generated_id) - if node_config.get(CONF_IGNORED): - _LOGGER.info( - "Ignoring node entity %s due to device settings", - generated_id) + + def _add_node_to_component(): + name = node_name(node) + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) + node_config = device_config.get(generated_id) + if node_config.get(CONF_IGNORED): + _LOGGER.info( + "Ignoring node entity %s due to device settings", + generated_id) + return + component.add_entities([entity]) + + if entity.unique_id: + _add_node_to_component() return - component.add_entities([entity]) + + async def _check_node_ready(): + """Wait for node to be parsed.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + + if entity.unique_id: + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, waited) + break + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave + # node to be ready. + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, waited) + break + else: + await asyncio.sleep(1, loop=hass.loop) + + hass.async_add_job(_add_node_to_component) + + hass.add_job(_check_node_ready) def network_ready(): """Handle the query of all awake nodes.""" diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index b42b6d0fce7..3e503e4d9a4 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -20,6 +20,7 @@ ATTR_POLL_INTENSITY = "poll_intensity" ATTR_VALUE_INDEX = "value_index" ATTR_VALUE_INSTANCE = "value_instance" NETWORK_READY_WAIT_SECS = 300 +NODE_READY_WAIT_SECS = 30 DISCOVERY_DEVICE = 'device' diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 5a4b1b02504..bcddcb0b800 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -81,6 +81,7 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name + self._unique_id = self._compute_unique_id() self._attributes = {} self.wakeup_interval = None self.location = None @@ -95,6 +96,11 @@ class ZWaveNodeEntity(ZWaveBaseEntity): dispatcher.connect( self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT) + @property + def unique_id(self): + """Unique ID of Z-wave node.""" + return self._unique_id + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: @@ -138,8 +144,14 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.wakeup_interval = None self.battery_level = self.node.get_battery_level() + self._product_name = self.node.product_name + self._manufacturer_name = self.node.manufacturer_name + self._name = node_name(self.node) self._attributes = attributes + if not self._unique_id: + self._unique_id = self._compute_unique_id() + self.maybe_schedule_update() def network_node_event(self, node, value): @@ -229,3 +241,8 @@ class ZWaveNodeEntity(ZWaveBaseEntity): attrs[ATTR_WAKEUP] = self.wakeup_interval return attrs + + def _compute_unique_id(self): + if self._manufacturer_name and self._product_name: + return 'node-{}'.format(self.node_id) + return None diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 004e5e95ca0..faa7357bd8a 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -224,6 +224,47 @@ def test_node_discovery(hass, mock_openzwave): assert hass.states.get('zwave.mock_node').state is 'unknown' +async def test_unparsed_node_discovery(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + await async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=14, manufacturer_name=None) + + sleeps = [] + + def utcnow(): + return datetime.fromtimestamp(len(sleeps)) + + asyncio_sleep = asyncio.sleep + + async def sleep(duration, loop): + if duration > 0: + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('homeassistant.components.zwave.dt_util.utcnow', new=utcnow): + with patch('asyncio.sleep', new=sleep): + with patch.object(zwave, '_LOGGER') as mock_logger: + hass.async_add_job(mock_receivers[0], node) + await hass.async_block_till_done() + + assert len(sleeps) == const.NODE_READY_WAIT_SECS + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + assert mock_logger.warning.mock_calls[0][1][1:] == \ + (14, const.NODE_READY_WAIT_SECS) + assert hass.states.get('zwave.mock_node').state is 'unknown' + + @asyncio.coroutine def test_node_ignored(hass, mock_openzwave): """Test discovery of a node.""" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 299821d3685..f4d9b3ef0e8 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -182,8 +182,6 @@ class TestZWaveNodeEntity(unittest.TestCase): query_stage='Dynamic', is_awake=True, is_ready=False, is_failed=False, is_info_received=True, max_baud_rate=40000, is_zwave_plus=False, capabilities=[], neighbors=[], location=None) - self.node.manufacturer_name = 'Test Manufacturer' - self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) @@ -357,3 +355,14 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_not_polled(self): """Test should_poll property.""" self.assertFalse(self.entity.should_poll) + + def test_unique_id(self): + """Test unique_id.""" + self.assertEqual('node-567', self.entity.unique_id) + + def test_unique_id_missing_data(self): + """Test unique_id.""" + self.node.manufacturer_name = None + entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) + + self.assertIsNone(entity.unique_id) diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 672cc884904..67bfb590c3f 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -119,6 +119,8 @@ class MockNode(MagicMock): product_type='678', command_classes=None, can_wake_up_value=True, + manufacturer_name='Test Manufacturer', + product_name='Test Product', network=None, **kwargs): """Initialize a Z-Wave mock node.""" @@ -128,6 +130,8 @@ class MockNode(MagicMock): self.manufacturer_id = manufacturer_id self.product_id = product_id self.product_type = product_type + self.manufacturer_name = manufacturer_name + self.product_name = product_name self.can_wake_up_value = can_wake_up_value self._command_classes = command_classes or [] if network is not None: From 64b9fbd8d9b654ca6a94d85a53e741f8ad22217b Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Wed, 2 May 2018 16:28:43 -0400 Subject: [PATCH 583/924] Add prereqs for HomeKit Controller (#14172) --- virtualization/Docker/setup_docker_prereqs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index bd70af28dce..302dfba2e1d 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -25,6 +25,8 @@ PACKAGES=( libsodium13 # homeassistant.components.zwave libudev-dev + # homeassistant.components.homekit_controller + libmpc-dev libmpfr-dev libgmp-dev ) # Required debian packages for building dependencies From c851dfa2c7bd77601976102d566d695006499fab Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 2 May 2018 22:29:07 +0100 Subject: [PATCH 584/924] Restores switch state, case the switch is optimistic (#14151) * Add restore_state to optimistic switch * no need to schedule update * test added * lint * new async syntax * lint --- homeassistant/components/switch/mqtt.py | 9 +++++++- tests/components/switch/test_mqtt.py | 30 +++++++++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 15dc6f1d0f4..69f12536c5f 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -16,9 +16,10 @@ from homeassistant.components.mqtt import ( from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, CONF_ICON) + CONF_PAYLOAD_ON, CONF_ICON, STATE_ON) import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -112,6 +113,12 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self.hass, self._state_topic, state_message_received, self._qos) + if self._optimistic: + last_state = await async_get_last_state(self.hass, + self.entity_id) + if last_state: + self._state = last_state.state == STATE_ON + @property def should_poll(self): """Return the polling state.""" diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index f79d0706321..b5e2a0b0395 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -1,12 +1,14 @@ """The tests for the MQTT switch platform.""" import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE,\ ATTR_ASSUMED_STATE +import homeassistant.core as ha import homeassistant.components.switch as switch from tests.common import ( - mock_mqtt_component, fire_mqtt_message, get_test_home_assistant) + mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, mock_coro) class TestSwitchMQTT(unittest.TestCase): @@ -52,19 +54,23 @@ class TestSwitchMQTT(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): """Test the sending MQTT commands in optimistic mode.""" - assert setup_component(self.hass, switch.DOMAIN, { - switch.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'command_topic': 'command-topic', - 'payload_on': 'beer on', - 'payload_off': 'beer off', - 'qos': '2' - } - }) + fake_state = ha.State('switch.test', 'on') + + with patch('homeassistant.components.switch.mqtt.async_get_last_state', + return_value=mock_coro(fake_state)): + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'command-topic', + 'payload_on': 'beer on', + 'payload_off': 'beer off', + 'qos': '2' + } + }) state = self.hass.states.get('switch.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) switch.turn_on(self.hass, 'switch.test') From ef4498ec27583b9d34ddd30ee60f8d2106a90070 Mon Sep 17 00:00:00 2001 From: giangvo Date: Thu, 3 May 2018 07:45:31 +1000 Subject: [PATCH 585/924] Issue/add template fans (#12027) * add template fan * add-template: address PR comments * add-template: remove unused import * add-template: revert async_track_state_change change * add-template: use yield from * Revert "add-template: use yield from" This reverts commit 1e053714a7c75c29367e3d04cf52161ebfaabba1. * add-template: use yield * add-template: remove unused import * add-template: remove async_add_job usages * use components * add-template: use async/await * add-template: fix style * add-template: remove str() * address pr comments * fix style --- homeassistant/components/fan/template.py | 324 +++++++++++++ tests/components/fan/test_template.py | 549 +++++++++++++++++++++++ 2 files changed, 873 insertions(+) create mode 100644 homeassistant/components/fan/template.py create mode 100644 tests/components/fan/test_template.py diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py new file mode 100644 index 00000000000..31b335eb2bc --- /dev/null +++ b/homeassistant/components/fan/template.py @@ -0,0 +1,324 @@ +""" +Support for Template fans. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/fan.template/ +""" +import logging + +import voluptuous as vol + +from homeassistant.core import callback +from homeassistant.const import ( + CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, CONF_ENTITY_ID, + STATE_ON, STATE_OFF, MATCH_ALL, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN) + +from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, + SPEED_HIGH, SUPPORT_SET_SPEED, + SUPPORT_OSCILLATE, FanEntity, + ATTR_SPEED, ATTR_OSCILLATING, + ENTITY_ID_FORMAT) + +from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.script import Script + +_LOGGER = logging.getLogger(__name__) + +CONF_FANS = 'fans' +CONF_SPEED_LIST = 'speeds' +CONF_SPEED_TEMPLATE = 'speed_template' +CONF_OSCILLATING_TEMPLATE = 'oscillating_template' +CONF_ON_ACTION = 'turn_on' +CONF_OFF_ACTION = 'turn_off' +CONF_SET_SPEED_ACTION = 'set_speed' +CONF_SET_OSCILLATING_ACTION = 'set_oscillating' + +_VALID_STATES = [STATE_ON, STATE_OFF] +_VALID_OSC = [True, False] + +FAN_SCHEMA = vol.Schema({ + vol.Optional(CONF_FRIENDLY_NAME): cv.string, + vol.Required(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_SPEED_TEMPLATE): cv.template, + vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + + vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, + + vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + + vol.Optional( + CONF_SPEED_LIST, + default=[SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + ): cv.ensure_list, + + vol.Optional(CONF_ENTITY_ID): cv.entity_ids +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FANS): vol.Schema({cv.slug: FAN_SCHEMA}), +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None +): + """Set up the Template Fans.""" + fans = [] + + for device, device_config in config[CONF_FANS].items(): + friendly_name = device_config.get(CONF_FRIENDLY_NAME, device) + + state_template = device_config[CONF_VALUE_TEMPLATE] + speed_template = device_config.get(CONF_SPEED_TEMPLATE) + oscillating_template = device_config.get( + CONF_OSCILLATING_TEMPLATE + ) + + on_action = device_config[CONF_ON_ACTION] + off_action = device_config[CONF_OFF_ACTION] + set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) + set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) + + speed_list = device_config[CONF_SPEED_LIST] + + entity_ids = set() + manual_entity_ids = device_config.get(CONF_ENTITY_ID) + + for template in (state_template, speed_template, oscillating_template): + if template is None: + continue + template.hass = hass + + if entity_ids == MATCH_ALL or manual_entity_ids is not None: + continue + + template_entity_ids = template.extract_entities() + if template_entity_ids == MATCH_ALL: + entity_ids = MATCH_ALL + else: + entity_ids |= set(template_entity_ids) + + if manual_entity_ids is not None: + entity_ids = manual_entity_ids + elif entity_ids != MATCH_ALL: + entity_ids = list(entity_ids) + + fans.append( + TemplateFan( + hass, device, friendly_name, + state_template, speed_template, oscillating_template, + on_action, off_action, set_speed_action, + set_oscillating_action, speed_list, entity_ids + ) + ) + + async_add_devices(fans) + + +class TemplateFan(FanEntity): + """A template fan component.""" + + def __init__(self, hass, device_id, friendly_name, + state_template, speed_template, oscillating_template, + on_action, off_action, set_speed_action, + set_oscillating_action, speed_list, entity_ids): + """Initialize the fan.""" + self.hass = hass + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, device_id, hass=hass) + self._name = friendly_name + + self._template = state_template + self._speed_template = speed_template + self._oscillating_template = oscillating_template + self._supported_features = 0 + + self._on_script = Script(hass, on_action) + self._off_script = Script(hass, off_action) + + self._set_speed_script = None + if set_speed_action: + self._set_speed_script = Script(hass, set_speed_action) + + self._set_oscillating_script = None + if set_oscillating_action: + self._set_oscillating_script = Script(hass, set_oscillating_action) + + self._state = STATE_OFF + self._speed = None + self._oscillating = None + + self._template.hass = self.hass + if self._speed_template: + self._speed_template.hass = self.hass + self._supported_features |= SUPPORT_SET_SPEED + if self._oscillating_template: + self._oscillating_template.hass = self.hass + self._supported_features |= SUPPORT_OSCILLATE + + self._entities = entity_ids + # List of valid speeds + self._speed_list = speed_list + + @property + def name(self): + """Return the display name of this fan.""" + return self._name + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def speed_list(self: ToggleEntity) -> list: + """Get the list of available speeds.""" + return self._speed_list + + @property + def is_on(self): + """Return true if device is on.""" + return self._state == STATE_ON + + @property + def speed(self): + """Return the current speed.""" + return self._speed + + @property + def oscillating(self): + """Return the oscillation state.""" + return self._oscillating + + @property + def should_poll(self): + """Return the polling state.""" + return False + + # pylint: disable=arguments-differ + async def async_turn_on(self, speed: str = None) -> None: + """Turn on the fan.""" + await self._on_script.async_run() + self._state = STATE_ON + + if speed is not None: + await self.async_set_speed(speed) + + # pylint: disable=arguments-differ + async def async_turn_off(self) -> None: + """Turn off the fan.""" + await self._off_script.async_run() + self._state = STATE_OFF + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if self._set_speed_script is None: + return + + if speed in self._speed_list: + self._speed = speed + await self._set_speed_script.async_run({ATTR_SPEED: speed}) + else: + _LOGGER.error( + 'Received invalid speed: %s. ' + + 'Expected: %s.', + speed, self._speed_list) + + async def async_oscillate(self, oscillating: bool) -> None: + """Set oscillation of the fan.""" + if self._set_oscillating_script is None: + return + + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating} + ) + self._oscillating = oscillating + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def template_fan_state_listener(entity, old_state, new_state): + """Handle target device state changes.""" + self.async_schedule_update_ha_state(True) + + @callback + def template_fan_startup(event): + """Update template on startup.""" + self.hass.helpers.event.async_track_state_change( + self._entities, template_fan_state_listener) + + self.async_schedule_update_ha_state(True) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, template_fan_startup) + + async def async_update(self): + """Update the state from the template.""" + # Update state + try: + state = self._template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + state = None + self._state = None + + # Validate state + if state in _VALID_STATES: + self._state = state + elif state == STATE_UNKNOWN: + self._state = None + else: + _LOGGER.error( + 'Received invalid fan is_on state: %s. ' + + 'Expected: %s.', + state, ', '.join(_VALID_STATES)) + self._state = None + + # Update speed if 'speed_template' is configured + if self._speed_template is not None: + try: + speed = self._speed_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + speed = None + self._state = None + + # Validate speed + if speed in self._speed_list: + self._speed = speed + elif speed == STATE_UNKNOWN: + self._speed = None + else: + _LOGGER.error( + 'Received invalid speed: %s. ' + + 'Expected: %s.', + speed, self._speed_list) + self._speed = None + + # Update oscillating if 'oscillating_template' is configured + if self._oscillating_template is not None: + try: + oscillating = self._oscillating_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None + + # Validate osc + if oscillating == 'True' or oscillating is True: + self._oscillating = True + elif oscillating == 'False' or oscillating is False: + self._oscillating = False + elif oscillating == STATE_UNKNOWN: + self._oscillating = None + else: + _LOGGER.error( + 'Received invalid oscillating: %s. ' + + 'Expected: True/False.', oscillating) + self._oscillating = None diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py new file mode 100644 index 00000000000..719a3f96aed --- /dev/null +++ b/tests/components/fan/test_template.py @@ -0,0 +1,549 @@ +"""The tests for the Template fan platform.""" +import logging + +from homeassistant.core import callback +from homeassistant import setup +import homeassistant.components as components +from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.components.fan import ( + ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + +from tests.common import ( + get_test_home_assistant, assert_setup_component) +_LOGGER = logging.getLogger(__name__) + + +_TEST_FAN = 'fan.test_fan' +# Represent for fan's state +_STATE_INPUT_BOOLEAN = 'input_boolean.state' +# Represent for fan's speed +_SPEED_INPUT_SELECT = 'input_select.speed' +# Represent for fan's oscillating +_OSC_INPUT = 'input_select.osc' + + +class TestTemplateFan: + """Test the Template light.""" + + hass = None + calls = None + # pylint: disable=invalid-name + + def setup_method(self, method): + """Setup.""" + self.hass = get_test_home_assistant() + + self.calls = [] + + @callback + def record_call(service): + """Track function calls..""" + self.calls.append(service) + + self.hass.services.register('test', 'automation', record_call) + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + # Configuration tests # + def test_missing_optional_config(self): + """Test: missing optional template is ok.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_ON, None, None) + + def test_missing_value_template_config(self): + """Test: missing 'value_template' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_turn_on_config(self): + """Test: missing 'turn_on' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_missing_turn_off_config(self): + """Test: missing 'turn_off' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_on': { + 'service': 'script.fan_on' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + def test_invalid_config(self): + """Test: missing 'turn_off' will fail.""" + with assert_setup_component(0, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': "{{ 'on' }}", + 'turn_on': { + 'service': 'script.fan_on' + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + assert self.hass.states.all() == [] + + # End of configuration tests # + + # Template tests # + def test_templates_with_entities(self): + """Test tempalates with values from other entities.""" + value_template = """ + {% if is_state('input_boolean.state', 'True') %} + {{ 'on' }} + {% else %} + {{ 'off' }} + {% endif %} + """ + + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': value_template, + 'speed_template': + "{{ states('input_select.speed') }}", + 'oscillating_template': + "{{ states('input_select.osc') }}", + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_OFF, None, None) + + self.hass.states.set(_STATE_INPUT_BOOLEAN, True) + self.hass.states.set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) + self.hass.states.set(_OSC_INPUT, 'True') + self.hass.block_till_done() + + self._verify(STATE_ON, SPEED_MEDIUM, True) + + def test_templates_with_valid_values(self): + """Test templates with valid values.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': + "{{ 'on' }}", + 'speed_template': + "{{ 'medium' }}", + 'oscillating_template': + "{{ 1 == 1 }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_ON, SPEED_MEDIUM, True) + + def test_templates_invalid_values(self): + """Test templates with invalid values.""" + with assert_setup_component(1, 'fan'): + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': { + 'value_template': + "{{ 'abc' }}", + 'speed_template': + "{{ '0' }}", + 'oscillating_template': + "{{ 'xyz' }}", + + 'turn_on': { + 'service': 'script.fan_on' + }, + 'turn_off': { + 'service': 'script.fan_off' + } + } + } + } + }) + + self.hass.start() + self.hass.block_till_done() + + self._verify(STATE_OFF, None, None) + + # End of template tests # + + # Function tests # + def test_on_off(self): + """Test turn on and turn off.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON + self._verify(STATE_ON, None, None) + + # Turn off fan + components.fan.turn_off(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF + self._verify(STATE_OFF, None, None) + + def test_on_with_speed(self): + """Test turn on with speed.""" + self._register_components() + + # Turn on fan with high speed + components.fan.turn_on(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + def test_set_speed(self): + """Test set valid speed.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to high + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + # Set fan's speed to medium + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM + self._verify(STATE_ON, SPEED_MEDIUM, None) + + def test_set_invalid_speed_from_initial_stage(self): + """Test set invalid speed when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to 'invalid' + components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '' + self._verify(STATE_ON, None, None) + + def test_set_invalid_speed(self): + """Test set invalid speed when fan has valid speed.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to high + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_HIGH) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + # Set fan's speed to 'invalid' + components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH + self._verify(STATE_ON, SPEED_HIGH, None) + + def test_custom_speed_list(self): + """Test set custom speed list.""" + self._register_components(['1', '2', '3']) + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's speed to '1' + components.fan.set_speed(self.hass, _TEST_FAN, '1') + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' + self._verify(STATE_ON, '1', None) + + # Set fan's speed to 'medium' which is invalid + components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) + self.hass.block_till_done() + + # verify that speed is unchanged + assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' + self._verify(STATE_ON, '1', None) + + def test_set_osc(self): + """Test set oscillating.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to True + components.fan.oscillate(self.hass, _TEST_FAN, True) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + # Set fan's osc to False + components.fan.oscillate(self.hass, _TEST_FAN, False) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'False' + self._verify(STATE_ON, None, False) + + def test_set_invalid_osc_from_initial_state(self): + """Test set invalid oscillating when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to 'invalid' + components.fan.oscillate(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == '' + self._verify(STATE_ON, None, None) + + def test_set_invalid_osc(self): + """Test set invalid oscillating when fan has valid osc.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's osc to True + components.fan.oscillate(self.hass, _TEST_FAN, True) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + # Set fan's osc to False + components.fan.oscillate(self.hass, _TEST_FAN, None) + self.hass.block_till_done() + + # verify osc is unchanged + assert self.hass.states.get(_OSC_INPUT).state == 'True' + self._verify(STATE_ON, None, True) + + def _verify(self, expected_state, expected_speed, expected_oscillating): + """Verify fan's state, speed and osc.""" + state = self.hass.states.get(_TEST_FAN) + attributes = state.attributes + assert state.state == expected_state + assert attributes.get(ATTR_SPEED, None) == expected_speed + assert attributes.get(ATTR_OSCILLATING, None) == expected_oscillating + + def _register_components(self, speed_list=None): + """Register basic components for testing.""" + with assert_setup_component(1, 'input_boolean'): + assert setup.setup_component( + self.hass, + 'input_boolean', + {'input_boolean': {'state': None}} + ) + + with assert_setup_component(2, 'input_select'): + assert setup.setup_component(self.hass, 'input_select', { + 'input_select': { + 'speed': { + 'name': 'Speed', + 'options': ['', SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + '1', '2', '3'] + }, + + 'osc': { + 'name': 'oscillating', + 'options': ['', 'True', 'False'] + }, + } + }) + + with assert_setup_component(1, 'fan'): + value_template = """ + {% if is_state('input_boolean.state', 'on') %} + {{ 'on' }} + {% else %} + {{ 'off' }} + {% endif %} + """ + + test_fan_config = { + 'value_template': value_template, + 'speed_template': + "{{ states('input_select.speed') }}", + 'oscillating_template': + "{{ states('input_select.osc') }}", + + 'turn_on': { + 'service': 'input_boolean.turn_on', + 'entity_id': _STATE_INPUT_BOOLEAN + }, + 'turn_off': { + 'service': 'input_boolean.turn_off', + 'entity_id': _STATE_INPUT_BOOLEAN + }, + 'set_speed': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _SPEED_INPUT_SELECT, + 'option': '{{ speed }}' + } + }, + 'set_oscillating': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _OSC_INPUT, + 'option': '{{ oscillating }}' + } + } + } + + if speed_list: + test_fan_config['speeds'] = speed_list + + assert setup.setup_component(self.hass, 'fan', { + 'fan': { + 'platform': 'template', + 'fans': { + 'test_fan': test_fan_config + } + } + }) + + self.hass.start() + self.hass.block_till_done() From c9de2f015b1e438be7a943b29d567987eac7149f Mon Sep 17 00:00:00 2001 From: roiff Date: Fri, 4 May 2018 00:22:43 +0800 Subject: [PATCH 586/924] HomeKit - Climate: power state on/off support (#14082) * add power state support on off * Added check for current operation mode * Extended 'set_heat_cool' * Added tests --- .../components/homekit/type_thermostats.py | 24 ++++++-- .../homekit/test_type_thermostats.py | 61 ++++++++++++++++++- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ce10b96c51c..4faceefe850 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -5,10 +5,10 @@ from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, + STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES @@ -41,6 +41,7 @@ class Thermostat(HomeAccessory): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS + self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False @@ -50,6 +51,8 @@ class Thermostat(HomeAccessory): self.chars = [] features = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_ON_OFF: + self.support_power_state = True if features & SUPPORT_TEMP_RANGE: self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)) @@ -93,6 +96,13 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heat-cool to %d', self.entity_id, value) self.heat_cool_flag_target_state = True hass_value = HC_HOMEKIT_TO_HASS[value] + if self.support_power_state is True: + params = {ATTR_ENTITY_ID: self.entity_id} + if hass_value == STATE_OFF: + self.hass.services.call('climate', 'turn_off', params) + return + else: + self.hass.services.call('climate', 'turn_on', params) self.hass.components.climate.set_operation_mode( operation_mode=hass_value, entity_id=self.entity_id) @@ -178,15 +188,19 @@ class Thermostat(HomeAccessory): # Update target operation mode operation_mode = new_state.attributes.get(ATTR_OPERATION_MODE) - if operation_mode \ - and operation_mode in HC_HASS_TO_HOMEKIT: + if self.support_power_state is True and new_state.state == STATE_OFF: + self.char_target_heat_cool.set_value( + HC_HASS_TO_HOMEKIT[STATE_OFF]) + elif operation_mode and operation_mode in HC_HASS_TO_HOMEKIT: if not self.heat_cool_flag_target_state: self.char_target_heat_cool.set_value( HC_HASS_TO_HOMEKIT[operation_mode]) self.heat_cool_flag_target_state = False # Set current operation mode based on temperatures and target mode - if operation_mode == STATE_HEAT: + if self.support_power_state is True and new_state.state == STATE_OFF: + current_operation_mode = STATE_OFF + elif operation_mode == STATE_HEAT: if isinstance(target_temp, float) and current_temp < target_temp: current_operation_mode = STATE_HEAT else: diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index adc3fb018f8..fe2a7f6cd02 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -7,7 +7,7 @@ from homeassistant.components.climate import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) from homeassistant.const import ( - ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, + ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -261,6 +261,65 @@ class TestHomekitThermostats(unittest.TestCase): 25.0) self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) + def test_power_state(self): + """Test if accessory and HA are updated accordingly.""" + climate = 'climate.test' + + # SUPPORT_ON_OFF = True + self.hass.states.set(climate, STATE_HEAT, + {ATTR_SUPPORTED_FEATURES: 4096, + ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + acc = self.thermostat_cls(self.hass, 'Climate', climate, + 2, config=None) + acc.run() + self.assertTrue(acc.support_power_state) + + self.assertEqual(acc.char_current_heat_cool.value, 1) + self.assertEqual(acc.char_target_heat_cool.value, 1) + + self.hass.states.set(climate, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 0) + + self.hass.states.set(climate, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + self.hass.block_till_done() + self.assertEqual(acc.char_current_heat_cool.value, 0) + self.assertEqual(acc.char_target_heat_cool.value, 0) + + # Set from HomeKit + acc.char_target_heat_cool.client_update_value(1) + self.hass.block_till_done() + self.assertEqual( + self.events[0].data[ATTR_SERVICE], 'turn_on') + self.assertEqual( + self.events[0].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], + climate) + self.assertEqual( + self.events[1].data[ATTR_SERVICE], 'set_operation_mode') + self.assertEqual( + self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], + STATE_HEAT) + self.assertEqual(acc.char_target_heat_cool.value, 1) + + acc.char_target_heat_cool.client_update_value(0) + self.hass.block_till_done() + self.assertEqual( + self.events[2].data[ATTR_SERVICE], 'turn_off') + self.assertEqual( + self.events[2].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], + climate) + self.assertEqual(acc.char_target_heat_cool.value, 0) + def test_thermostat_fahrenheit(self): """Test if accessory and HA are updated accordingly.""" climate = 'climate.test' From e68b52d50d6bb3a1696d168f7df45b8ab639c22f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 May 2018 19:51:36 +0200 Subject: [PATCH 587/924] Demo Sensor - Added device_class support (#14269) --- homeassistant/components/sensor/demo.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index ba7c93203df..5cae1a47c23 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -12,18 +12,21 @@ from homeassistant.helpers.entity import Entity def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo sensors.""" add_devices([ - DemoSensor('Outside Temperature', 15.6, TEMP_CELSIUS, 12), - DemoSensor('Outside Humidity', 54, '%', None), + DemoSensor('Outside Temperature', 15.6, 'temperature', + TEMP_CELSIUS, 12), + DemoSensor('Outside Humidity', 54, 'humidity', '%', None), ]) class DemoSensor(Entity): """Representation of a Demo sensor.""" - def __init__(self, name, state, unit_of_measurement, battery): + def __init__(self, name, state, device_class, + unit_of_measurement, battery): """Initialize the sensor.""" self._name = name self._state = state + self._device_class = device_class self._unit_of_measurement = unit_of_measurement self._battery = battery @@ -32,6 +35,11 @@ class DemoSensor(Entity): """No polling needed for a demo sensor.""" return False + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + @property def name(self): """Return the name of the sensor.""" From 4ecce2598ac81ce9496be1cc42d836575a22737c Mon Sep 17 00:00:00 2001 From: Erik Eriksson <8228319+molobrakos@users.noreply.github.com> Date: Thu, 3 May 2018 19:54:37 +0200 Subject: [PATCH 588/924] Re-enable eliqonline requirement (#14265) --- homeassistant/components/sensor/eliqonline.py | 3 +-- requirements_all.txt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/eliqonline.py b/homeassistant/components/sensor/eliqonline.py index 23c397053c5..6405c707536 100644 --- a/homeassistant/components/sensor/eliqonline.py +++ b/homeassistant/components/sensor/eliqonline.py @@ -14,8 +14,7 @@ from homeassistant.const import (CONF_ACCESS_TOKEN, CONF_NAME, STATE_UNKNOWN) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -# pylint: disable=import-error, no-member -REQUIREMENTS = [] # ['eliqonline==1.0.13'] - package disappeared +REQUIREMENTS = ['eliqonline==1.0.14'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1283011d7ac..99917ef9e35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,6 +276,9 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.sensor.eliqonline +eliqonline==1.0.14 + # homeassistant.components.enocean enocean==0.40 From 58257af28953eff376467f02789ef76884f88d0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 May 2018 16:02:59 -0400 Subject: [PATCH 589/924] Add fetching camera thumbnails over websocket (#14231) * Add fetching camera thumbnails over websocket * Lint --- homeassistant/components/camera/__init__.py | 95 +++++++++++++------ homeassistant/components/frontend/__init__.py | 1 + .../components/image_processing/__init__.py | 2 +- homeassistant/components/microsoft_face.py | 2 +- homeassistant/components/websocket_api.py | 7 ++ tests/components/camera/test_init.py | 58 +++++++---- .../image_processing/test_openalpr_cloud.py | 40 ++++---- tests/components/test_microsoft_face.py | 4 +- 8 files changed, 135 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 1fa89bc2241..c1f92965198 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -6,6 +6,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/camera/ """ import asyncio +import base64 import collections from contextlib import suppress from datetime import timedelta @@ -13,20 +14,20 @@ import logging import hashlib from random import SystemRandom -import aiohttp +import attr from aiohttp import web import async_timeout import voluptuous as vol from homeassistant.core import callback -from homeassistant.const import (ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE) +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED +from homeassistant.components import websocket_api import homeassistant.helpers.config_validation as cv DOMAIN = 'camera' @@ -64,6 +65,20 @@ CAMERA_SERVICE_SNAPSHOT = CAMERA_SERVICE_SCHEMA.extend({ vol.Required(ATTR_FILENAME): cv.template }) +WS_TYPE_CAMERA_THUMBNAIL = 'camera_thumbnail' +SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + 'type': WS_TYPE_CAMERA_THUMBNAIL, + 'entity_id': cv.entity_id +}) + + +@attr.s +class Image: + """Represent an image.""" + + content_type = attr.ib(type=str) + content = attr.ib(type=bytes) + @bind_hass def enable_motion_detection(hass, entity_id=None): @@ -92,43 +107,40 @@ def async_snapshot(hass, filename, entity_id=None): @bind_hass -@asyncio.coroutine -def async_get_image(hass, entity_id, timeout=10): +async def async_get_image(hass, entity_id, timeout=10): """Fetch an image from a camera entity.""" - websession = async_get_clientsession(hass) - state = hass.states.get(entity_id) + component = hass.data.get(DOMAIN) - if state is None: - raise HomeAssistantError( - "No entity '{0}' for grab an image".format(entity_id)) + if component is None: + raise HomeAssistantError('Camera component not setup') - url = "{0}{1}".format( - hass.config.api.base_url, - state.attributes.get(ATTR_ENTITY_PICTURE) - ) + camera = component.get_entity(entity_id) - try: + if camera is None: + raise HomeAssistantError('Camera not found') + + with suppress(asyncio.CancelledError, asyncio.TimeoutError): with async_timeout.timeout(timeout, loop=hass.loop): - response = yield from websession.get(url) + image = await camera.async_camera_image() - if response.status != 200: - raise HomeAssistantError("Error {0} on {1}".format( - response.status, url)) + if image: + return Image(camera.content_type, image) - image = yield from response.read() - return image - - except (asyncio.TimeoutError, aiohttp.ClientError): - raise HomeAssistantError("Can't connect to {0}".format(url)) + raise HomeAssistantError('Unable to get image') @asyncio.coroutine def async_setup(hass, config): """Set up the camera component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) + hass.components.websocket_api.async_register_command( + WS_TYPE_CAMERA_THUMBNAIL, websocket_camera_thumbnail, + SCHEMA_WS_CAMERA_THUMBNAIL + ) yield from component.async_setup(config) @@ -344,20 +356,20 @@ class Camera(Entity): @property def state_attributes(self): """Return the camera state attributes.""" - attr = { + attrs = { 'access_token': self.access_tokens[-1], } if self.model: - attr['model_name'] = self.model + attrs['model_name'] = self.model if self.brand: - attr['brand'] = self.brand + attrs['brand'] = self.brand if self.motion_detection_enabled: - attr['motion_detection'] = self.motion_detection_enabled + attrs['motion_detection'] = self.motion_detection_enabled - return attr + return attrs @callback def async_update_token(self): @@ -440,3 +452,26 @@ class CameraMjpegStream(CameraView): return except ValueError: return web.Response(status=400) + + +@callback +def websocket_camera_thumbnail(hass, connection, msg): + """Handle get camera thumbnail websocket command. + + Async friendly. + """ + async def send_camera_still(): + """Send a camera still.""" + try: + image = await async_get_image(hass, msg['entity_id']) + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': image.content_type, + 'content': base64.b64encode(image.content).decode('utf-8') + } + )) + except HomeAssistantError: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'image_fetch_failed', 'Unable to fetch image')) + + hass.async_add_job(send_camera_still()) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 564ba286b96..58cea0e0c66 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -606,6 +606,7 @@ def _is_latest(js_option, request): return useragent and hass_frontend.version(useragent) +@callback def websocket_handle_get_panels(hass, connection, msg): """Handle get panels command. diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index de195ce0165..f0cb3a66d52 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -132,4 +132,4 @@ class ImageProcessingEntity(Entity): return # process image data - yield from self.async_process_image(image) + yield from self.async_process_image(image.content) diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index e99d8d4a5f6..7c167f93142 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -239,7 +239,7 @@ def async_setup(hass, config): 'post', "persongroups/{0}/persons/{1}/persistedFaces".format( g_id, p_id), - image, + image.content, binary=True ) except HomeAssistantError as err: diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 84c92631572..4989f4f0db2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -429,6 +429,7 @@ class ActiveConnection: return wsock +@callback def handle_subscribe_events(hass, connection, msg): """Handle subscribe events command. @@ -447,6 +448,7 @@ def handle_subscribe_events(hass, connection, msg): connection.to_write.put_nowait(result_message(msg['id'])) +@callback def handle_unsubscribe_events(hass, connection, msg): """Handle unsubscribe events command. @@ -462,6 +464,7 @@ def handle_unsubscribe_events(hass, connection, msg): msg['id'], ERR_NOT_FOUND, 'Subscription not found.')) +@callback def handle_call_service(hass, connection, msg): """Handle call service command. @@ -476,6 +479,7 @@ def handle_call_service(hass, connection, msg): hass.async_add_job(call_service_helper(msg)) +@callback def handle_get_states(hass, connection, msg): """Handle get states command. @@ -485,6 +489,7 @@ def handle_get_states(hass, connection, msg): msg['id'], hass.states.async_all())) +@callback def handle_get_services(hass, connection, msg): """Handle get services command. @@ -499,6 +504,7 @@ def handle_get_services(hass, connection, msg): hass.async_add_job(get_services_helper(msg)) +@callback def handle_get_config(hass, connection, msg): """Handle get config command. @@ -508,6 +514,7 @@ def handle_get_config(hass, connection, msg): msg['id'], hass.config.as_dict())) +@callback def handle_ping(hass, connection, msg): """Handle ping command. diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 465d6276ad5..d0f1425a595 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,18 +1,19 @@ """The tests for the camera component.""" import asyncio +import base64 from unittest.mock import patch, mock_open import pytest from homeassistant.setup import setup_component, async_setup_component from homeassistant.const import ATTR_ENTITY_PICTURE -import homeassistant.components.camera as camera -import homeassistant.components.http as http +from homeassistant.components import camera, http, websocket_api from homeassistant.exceptions import HomeAssistantError from homeassistant.util.async_ import run_coroutine_threadsafe from tests.common import ( - get_test_home_assistant, get_test_instance_port, assert_setup_component) + get_test_home_assistant, get_test_instance_port, assert_setup_component, + mock_coro) @pytest.fixture @@ -90,36 +91,32 @@ class TestGetImage(object): self.hass, 'camera.demo_camera'), self.hass.loop).result() assert mock_camera.called - assert image == b'Test' + assert image.content == b'Test' def test_get_image_without_exists_camera(self): """Try to get image without exists camera.""" - self.hass.states.remove('camera.demo_camera') - - with pytest.raises(HomeAssistantError): + with patch('homeassistant.helpers.entity_component.EntityComponent.' + 'get_entity', return_value=None), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - def test_get_image_with_timeout(self, aioclient_mock): + def test_get_image_with_timeout(self): """Try to get image with timeout.""" - aioclient_mock.get(self.url, exc=asyncio.TimeoutError()) - - with pytest.raises(HomeAssistantError): + with patch('homeassistant.components.camera.Camera.async_camera_image', + side_effect=asyncio.TimeoutError), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 - - def test_get_image_with_bad_http_state(self, aioclient_mock): - """Try to get image with bad http status.""" - aioclient_mock.get(self.url, status=400) - - with pytest.raises(HomeAssistantError): + def test_get_image_fails(self): + """Try to get image with timeout.""" + with patch('homeassistant.components.camera.Camera.async_camera_image', + return_value=mock_coro(None)), \ + pytest.raises(HomeAssistantError): run_coroutine_threadsafe(camera.async_get_image( self.hass, 'camera.demo_camera'), self.hass.loop).result() - assert len(aioclient_mock.mock_calls) == 1 - @asyncio.coroutine def test_snapshot_service(hass, mock_camera): @@ -136,3 +133,24 @@ def test_snapshot_service(hass, mock_camera): assert len(mock_write.mock_calls) == 1 assert mock_write.mock_calls[0][1][0] == b'Test' + + +async def test_webocket_camera_thumbnail(hass, hass_ws_client, mock_camera): + """Test camera_thumbnail websocket command.""" + await async_setup_component(hass, 'camera') + + client = await hass_ws_client(hass) + await client.send_json({ + 'id': 5, + 'type': 'camera_thumbnail', + 'entity_id': 'camera.demo_camera', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['success'] + assert msg['result']['content_type'] == 'image/jpeg' + assert msg['result']['content'] == \ + base64.b64encode(b'Test').decode('utf-8') diff --git a/tests/components/image_processing/test_openalpr_cloud.py b/tests/components/image_processing/test_openalpr_cloud.py index e840bce54f7..50060e08a4b 100644 --- a/tests/components/image_processing/test_openalpr_cloud.py +++ b/tests/components/image_processing/test_openalpr_cloud.py @@ -3,14 +3,13 @@ import asyncio from unittest.mock import patch, PropertyMock from homeassistant.core import callback -from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.setup import setup_component -import homeassistant.components.image_processing as ip +from homeassistant.components import camera, image_processing as ip from homeassistant.components.image_processing.openalpr_cloud import ( OPENALPR_API_URL) from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture) + get_test_home_assistant, assert_setup_component, load_fixture, mock_coro) class TestOpenAlprCloudSetup(object): @@ -131,11 +130,6 @@ class TestOpenAlprCloud(object): new_callable=PropertyMock(return_value=False)): setup_component(self.hass, ip.DOMAIN, config) - state = self.hass.states.get('camera.demo_camera') - self.url = "{0}{1}".format( - self.hass.config.api.base_url, - state.attributes.get(ATTR_ENTITY_PICTURE)) - self.alpr_events = [] @callback @@ -158,18 +152,20 @@ class TestOpenAlprCloud(object): def test_openalpr_process_image(self, aioclient_mock): """Setup and scan a picture and test plates from event.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, text=load_fixture('alpr_cloud.json'), status=200 ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() state = self.hass.states.get('image_processing.test_local') - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 5 assert state.attributes.get('vehicles') == 1 assert state.state == 'H786P0J' @@ -184,28 +180,32 @@ class TestOpenAlprCloud(object): def test_openalpr_process_image_api_error(self, aioclient_mock): """Setup and scan a picture and test api error.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, text="{'error': 'error message'}", status=400 ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 0 def test_openalpr_process_image_api_timeout(self, aioclient_mock): """Setup and scan a picture and test api error.""" - aioclient_mock.get(self.url, content=b'image') aioclient_mock.post( OPENALPR_API_URL, params=self.params, exc=asyncio.TimeoutError() ) - ip.scan(self.hass, entity_id='image_processing.test_local') - self.hass.block_till_done() + with patch('homeassistant.components.camera.async_get_image', + return_value=mock_coro( + camera.Image('image/jpeg', b'image'))): + ip.scan(self.hass, entity_id='image_processing.test_local') + self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 2 + assert len(aioclient_mock.mock_calls) == 1 assert len(self.alpr_events) == 0 diff --git a/tests/components/test_microsoft_face.py b/tests/components/test_microsoft_face.py index 7a047a73f47..370059a0a09 100644 --- a/tests/components/test_microsoft_face.py +++ b/tests/components/test_microsoft_face.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import patch -import homeassistant.components.microsoft_face as mf +from homeassistant.components import camera, microsoft_face as mf from homeassistant.setup import setup_component from tests.common import ( @@ -190,7 +190,7 @@ class TestMicrosoftFaceSetup(object): assert len(aioclient_mock.mock_calls) == 1 @patch('homeassistant.components.camera.async_get_image', - return_value=mock_coro(b'Test')) + return_value=mock_coro(camera.Image('image/jpeg', b'Test'))) def test_service_face(self, camera_mock, aioclient_mock): """Setup component, test person face services.""" aioclient_mock.get( From 15e75b07d83c5779fbbdfa0ba92ba26da3f9823f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 3 May 2018 16:03:26 -0400 Subject: [PATCH 590/924] Allow fetching media player covers via websocket connection (#14233) Lint --- .../components/media_player/__init__.py | 49 ++++++++++++++++++- tests/components/media_player/test_init.py | 37 ++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/components/media_player/test_init.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 20fd3b875c8..20a1a473ba8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/media_player/ """ import asyncio +import base64 from datetime import timedelta import functools as ft import collections @@ -17,6 +18,7 @@ from aiohttp.hdrs import CONTENT_TYPE, CACHE_CONTROL import async_timeout import voluptuous as vol +from homeassistant.core import callback from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.const import ( STATE_OFF, STATE_IDLE, STATE_PLAYING, STATE_UNKNOWN, ATTR_ENTITY_ID, @@ -31,6 +33,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass +from homeassistant.components import websocket_api _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() @@ -361,11 +364,22 @@ def set_shuffle(hass, shuffle, entity_id=None): hass.services.call(DOMAIN, SERVICE_SHUFFLE_SET, data) +WS_TYPE_MEDIA_PLAYER_THUMBNAIL = 'media_player_thumbnail' +SCHEMA_WEBSOCKET_GET_THUMBNAIL = \ + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ + 'type': WS_TYPE_MEDIA_PLAYER_THUMBNAIL, + 'entity_id': cv.entity_id + }) + + async def async_setup(hass, config): """Track states and offer events for media_players.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) + hass.components.websocket_api.async_register_command( + WS_TYPE_MEDIA_PLAYER_THUMBNAIL, websocket_handle_thumbnail, + SCHEMA_WEBSOCKET_GET_THUMBNAIL) hass.http.register_view(MediaPlayerImageView(component)) await component.async_setup(config) @@ -942,3 +956,36 @@ class MediaPlayerImageView(HomeAssistantView): headers = {CACHE_CONTROL: 'max-age=3600'} return web.Response( body=data, content_type=content_type, headers=headers) + + +@callback +def websocket_handle_thumbnail(hass, connection, msg): + """Handle get media player cover command. + + Async friendly. + """ + component = hass.data[DOMAIN] + player = component.get_entity(msg['entity_id']) + + if player is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'entity_not_found', 'Entity not found')) + return + + async def send_image(): + """Send image.""" + data, content_type = await player.async_get_media_image() + + if data is None: + connection.send_message_outside(websocket_api.error_message( + msg['id'], 'thumbnail_fetch_failed', + 'Failed to fetch thumbnail')) + return + + connection.send_message_outside(websocket_api.result_message( + msg['id'], { + 'content_type': content_type, + 'content': base64.b64encode(data).decode('utf-8') + })) + + hass.async_add_job(send_image()) diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py new file mode 100644 index 00000000000..5d632d4de0b --- /dev/null +++ b/tests/components/media_player/test_init.py @@ -0,0 +1,37 @@ +"""Test the base functions of the media player.""" +import base64 +from unittest.mock import patch + +from homeassistant.setup import async_setup_component +from homeassistant.components import websocket_api + +from tests.common import mock_coro + + +async def test_get_panels(hass, hass_ws_client): + """Test get_panels command.""" + await async_setup_component(hass, 'media_player', { + 'media_player': { + 'platform': 'demo' + } + }) + + client = await hass_ws_client(hass) + + with patch('homeassistant.components.media_player.MediaPlayerDevice.' + 'async_get_media_image', return_value=mock_coro( + (b'image', 'image/jpeg'))): + await client.send_json({ + 'id': 5, + 'type': 'media_player_thumbnail', + 'entity_id': 'media_player.bedroom', + }) + + msg = await client.receive_json() + + assert msg['id'] == 5 + assert msg['type'] == websocket_api.TYPE_RESULT + assert msg['success'] + assert msg['result']['content_type'] == 'image/jpeg' + assert msg['result']['content'] == \ + base64.b64encode(b'image').decode('utf-8') From 8cabec7ac114ef8a0a6e20862ab41de05d4da1d7 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 3 May 2018 23:28:03 +0200 Subject: [PATCH 591/924] Fix ZWave light brightness (#14261) * Fix ZWave light brightness * The brightness should always be an integer * Changed to round --- homeassistant/components/light/zwave.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/light/zwave.py b/homeassistant/components/light/zwave.py index 286ce73f1ed..04216780c80 100644 --- a/homeassistant/components/light/zwave.py +++ b/homeassistant/components/light/zwave.py @@ -61,7 +61,7 @@ def get_device(node, values, node_config, **kwargs): def brightness_state(value): """Return the brightness and state.""" if value.data > 0: - return round((value.data / 99) * 255, 0), STATE_ON + return round((value.data / 99) * 255), STATE_ON return 0, STATE_OFF From 9859840b9ce3047752662e3c81098d6b83e3933c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:48:13 +0200 Subject: [PATCH 592/924] Update issue templates --- .github/ISSUE_TEMPLATE/Bug_report.md | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/Bug_report.md diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 00000000000..b2721db553f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,50 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +**Home Assistant release with the issue:** + + + +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** + + +**Component/platform:** + + + +**Description of problem:** + + + +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** +```yaml + +``` + +**Traceback (if applicable):** +``` + +``` + +**Additional information:** From a7a3cff0f1c5f4110dd4ea619c1a6a5ded4251d9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:52:20 +0200 Subject: [PATCH 593/924] Update issue templates --- .github/ISSUE_TEMPLATE/Feature_request.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/Feature_request.md diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 00000000000..3489db2b397 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,7 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). Thanks From 54ccbbcb64faecb84466c7533235c20b1e454224 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:54:55 +0200 Subject: [PATCH 594/924] Update issue templates --- .github/ISSUE_TEMPLATE/Feature_request.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 3489db2b397..69ab7c33a85 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,7 +1,8 @@ --- name: Feature request -about: Suggest an idea for this project +about: Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). + Thanks --- -Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). Thanks + From 5f8f6666e6e7a72c05f0c450518b8196ed537285 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:55:55 +0200 Subject: [PATCH 595/924] Update issue templates --- .github/ISSUE_TEMPLATE/Feature_request.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 69ab7c33a85..e8e12dfa167 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -1,7 +1,7 @@ --- name: Feature request -about: Please use the forum for [feature requests](https://community.home-assistant.io/c/feature-requests). - Thanks +about: Please use the forum (https://community.home-assistant.io/c/feature-requests) for + feature requests. Thanks --- From b49d98407c4c2bd3df132a03e16afa9faa1e2276 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 10:56:35 +0200 Subject: [PATCH 596/924] Remove feature request --- .github/ISSUE_TEMPLATE/Feature_request.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/Feature_request.md diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md deleted file mode 100644 index e8e12dfa167..00000000000 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: Feature request -about: Please use the forum (https://community.home-assistant.io/c/feature-requests) for - feature requests. Thanks - ---- - - From fa0ad7b3171d6abcf0b765558a1f1c824323fd94 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 4 May 2018 12:28:56 +0200 Subject: [PATCH 597/924] Color fixes for Wink lights (#14263) --- homeassistant/components/light/wink.py | 34 +++++++++----------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index fd957f8f11d..04e9c34b0f6 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.wink/ """ import asyncio -import colorsys from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, @@ -54,29 +53,19 @@ class WinkLight(WinkDevice, Light): return int(self.wink.brightness() * 255) return None - @property - def rgb_color(self): - """Define current bulb color in RGB.""" - if not self.wink.supports_hue_saturation(): - return None - else: - hue = self.wink.color_hue() - saturation = self.wink.color_saturation() - value = int(self.wink.brightness() * 255) - if hue is None or saturation is None or value is None: - return None - rgb = colorsys.hsv_to_rgb(hue, saturation, value) - r_value = int(round(rgb[0])) - g_value = int(round(rgb[1])) - b_value = int(round(rgb[2])) - return r_value, g_value, b_value - @property def hs_color(self): """Define current bulb color.""" - if not self.wink.supports_xy_color(): - return None - return color_util.color_xy_to_hs(*self.wink.color_xy()) + if self.wink.supports_xy_color(): + return color_util.color_xy_to_hs(*self.wink.color_xy()) + + if self.wink.supports_hue_saturation(): + hue = self.wink.color_hue() + saturation = self.wink.color_saturation() + if hue is not None and saturation is not None: + return hue*360, saturation*100 + + return None @property def color_temp(self): @@ -104,7 +93,8 @@ class WinkLight(WinkDevice, Light): xy_color = color_util.color_hs_to_xy(*hs_color) state_kwargs['color_xy'] = xy_color if self.wink.supports_hue_saturation(): - state_kwargs['color_hue_saturation'] = hs_color + hs_scaled = hs_color[0]/360, hs_color[1]/100 + state_kwargs['color_hue_saturation'] = hs_scaled if color_temp_mired: state_kwargs['color_kelvin'] = mired_to_kelvin(color_temp_mired) From c80b752d0e3db93b35cc8d203b1ed98e2b5a9c95 Mon Sep 17 00:00:00 2001 From: Boyi C Date: Fri, 4 May 2018 18:29:07 +0800 Subject: [PATCH 598/924] fix check config not working after #14211 (#14259) --- homeassistant/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index d69704a7032..5c432490f6a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -687,7 +687,7 @@ async def async_check_ha_config_file(hass): from homeassistant.scripts.check_config import check_ha_config_file res = await hass.async_add_job( - check_ha_config_file, hass.config.config_dir) + check_ha_config_file, hass) if not res.errors: return None From 36cf2125ce4767975d97cc30f8fe35b3e4ece9e7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 4 May 2018 13:49:13 +0200 Subject: [PATCH 599/924] Issue Template Fix CRLF (#14283) --- .github/ISSUE_TEMPLATE/Bug_report.md | 100 +++++++++++++-------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index b2721db553f..2c418c6f63e 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -1,50 +1,50 @@ ---- -name: Bug report -about: Create a report to help us improve - ---- - - - -**Home Assistant release with the issue:** - - - -**Last working Home Assistant release (if known):** - - -**Operating environment (Hass.io/Docker/Windows/etc.):** - - -**Component/platform:** - - - -**Description of problem:** - - - -**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** -```yaml - -``` - -**Traceback (if applicable):** -``` - -``` - -**Additional information:** +--- +name: Bug report +about: Create a report to help us improve + +--- + + + +**Home Assistant release with the issue:** + + + +**Last working Home Assistant release (if known):** + + +**Operating environment (Hass.io/Docker/Windows/etc.):** + + +**Component/platform:** + + + +**Description of problem:** + + + +**Problem-relevant `configuration.yaml` entries and (fill out even if it seems unimportant):** +```yaml + +``` + +**Traceback (if applicable):** +``` + +``` + +**Additional information:** From f98525acbfc8517d5b6aaac881a4e0573d5757a1 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 14:58:34 +0200 Subject: [PATCH 600/924] Upgrade attrs to 18.1.0 (#14281) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6de885942fb..0f3c9ac255d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -9,7 +9,7 @@ aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 -attrs==17.4.0 +attrs==18.1.0 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 99917ef9e35..002c5eb93e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -10,7 +10,7 @@ aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6 certifi>=2017.4.17 -attrs==17.4.0 +attrs==18.1.0 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 diff --git a/setup.py b/setup.py index 8815b0227ad..3db7c737a2c 100755 --- a/setup.py +++ b/setup.py @@ -53,7 +53,7 @@ REQUIRES = [ 'async_timeout==2.0.1', 'astral==1.6', 'certifi>=2017.4.17', - 'attrs==17.4.0', + 'attrs==18.1.0', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) From e37fd5b132666f9d57e31623adc9ecf1e00ad069 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 May 2018 16:46:00 +0200 Subject: [PATCH 601/924] Update HAP-python to 2.0.0 (#14278) * Fixed async (added 'async_add_job' and 'add_job') * Driver status * Use pyhap category constants * Changed 'set_broker' to 'set_driver' * Changed loader method names * Use 'serv.configure_char' * Use 'self.set_info_service' * Use 'self.add_preload_service' * Fix hound issue * Updated HAP-python to 2.0.0 --- homeassistant/components/homekit/__init__.py | 33 +++++--- .../components/homekit/accessories.py | 65 +++------------ homeassistant/components/homekit/const.py | 12 --- .../components/homekit/type_covers.py | 48 +++++------ .../components/homekit/type_lights.py | 35 ++++---- .../components/homekit/type_locks.py | 20 +++-- .../homekit/type_security_systems.py | 20 +++-- .../components/homekit/type_sensors.py | 55 ++++++------ .../components/homekit/type_switches.py | 12 +-- .../components/homekit/type_thermostats.py | 46 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_accessories.py | 83 +++++-------------- tests/components/homekit/test_homekit.py | 42 ++++++---- 14 files changed, 203 insertions(+), 272 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 4984cfee959..080dd2a7cbd 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -30,7 +30,13 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==1.1.9'] +REQUIREMENTS = ['HAP-python==2.0.0'] + +# #### Driver Status #### +STATUS_READY = 0 +STATUS_RUNNING = 1 +STATUS_STOPPED = 2 +STATUS_WAIT = 3 CONFIG_SCHEMA = vol.Schema({ @@ -57,7 +63,7 @@ async def async_setup(hass, config): entity_config = conf[CONF_ENTITY_CONFIG] homekit = HomeKit(hass, port, ip_address, entity_filter, entity_config) - homekit.setup() + await hass.async_add_job(homekit.setup) if auto_start: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, homekit.start) @@ -65,8 +71,10 @@ async def async_setup(hass, config): def handle_homekit_service_start(service): """Handle start HomeKit service call.""" - if homekit.started: - _LOGGER.warning('HomeKit is already running') + if homekit.status != STATUS_READY: + _LOGGER.warning( + 'HomeKit is not ready. Either it is already running or has ' + 'been stopped.') return homekit.start() @@ -162,7 +170,7 @@ class HomeKit(): self._ip_address = ip_address self._filter = entity_filter self._config = entity_config - self.started = False + self.status = STATUS_READY self.bridge = None self.driver = None @@ -191,9 +199,9 @@ class HomeKit(): def start(self, *args): """Start the accessory driver.""" - if self.started: + if self.status != STATUS_READY: return - self.started = True + self.status = STATUS_WAIT # pylint: disable=unused-variable from . import ( # noqa F401 @@ -202,19 +210,20 @@ class HomeKit(): for state in self.hass.states.all(): self.add_bridge_accessory(state) - self.bridge.set_broker(self.driver) + self.bridge.set_driver(self.driver) if not self.bridge.paired: show_setup_message(self.hass, self.bridge) _LOGGER.debug('Driver start') - self.driver.start() + self.hass.add_job(self.driver.start) + self.status = STATUS_RUNNING def stop(self, *args): """Stop the accessory driver.""" - if not self.started: + if self.status != STATUS_RUNNING: return + self.status = STATUS_STOPPED _LOGGER.debug('Driver stop') - if self.driver and self.driver.run_sentinel: - self.driver.stop() + self.hass.add_job(self.driver.stop) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c7703b221d8..c47c3f8fbe7 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -4,8 +4,9 @@ from functools import wraps from inspect import getmodule import logging -from pyhap.accessory import Accessory, Bridge, Category +from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver +from pyhap.const import CATEGORY_OTHER from homeassistant.const import __version__ from homeassistant.core import callback as ha_callback @@ -15,9 +16,8 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, - MANUFACTURER, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER) + DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, + BRIDGE_SERIAL_NUMBER, MANUFACTURER) from .util import ( show_setup_message, dismiss_setup_message) @@ -61,59 +61,20 @@ def debounce(func): return wrapper -def add_preload_service(acc, service, chars=None): - """Define and return a service to be available for the accessory.""" - from pyhap.loader import get_serv_loader, get_char_loader - service = get_serv_loader().get(service) - if chars: - chars = chars if isinstance(chars, list) else [chars] - for char_name in chars: - char = get_char_loader().get(char_name) - service.add_characteristic(char) - acc.add_service(service) - return service - - -def setup_char(char_name, service, value=None, properties=None, callback=None): - """Helper function to return fully configured characteristic.""" - char = service.get_characteristic(char_name) - if value: - char.value = value - if properties: - char.override_properties(properties) - if callback: - char.setter_callback = callback - return char - - -def set_accessory_info(acc, name, model, serial_number, - manufacturer=MANUFACTURER, - firmware_revision=__version__): - """Set the default accessory information.""" - service = acc.get_service(SERV_ACCESSORY_INFO) - service.get_characteristic(CHAR_NAME).set_value(name) - service.get_characteristic(CHAR_MODEL).set_value(model) - service.get_characteristic(CHAR_MANUFACTURER).set_value(manufacturer) - service.get_characteristic(CHAR_SERIAL_NUMBER).set_value(serial_number) - service.get_characteristic(CHAR_FIRMWARE_REVISION) \ - .set_value(firmware_revision) - - class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, category): + def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): """Initialize a Accessory object.""" super().__init__(name, aid=aid) domain = split_entity_id(entity_id)[0].replace("_", " ").title() - set_accessory_info(self, name, model=domain, serial_number=entity_id) - self.category = getattr(Category, category, Category.OTHER) + self.set_info_service( + firmware_revision=__version__, manufacturer=MANUFACTURER, + model=domain, serial_number=entity_id) + self.category = category self.entity_id = entity_id self.hass = hass - def _set_services(self): - add_preload_service(self, SERV_ACCESSORY_INFO) - def run(self): """Method called by accessory after driver is started.""" state = self.hass.states.get(self.entity_id) @@ -143,13 +104,11 @@ class HomeBridge(Bridge): def __init__(self, hass, name=BRIDGE_NAME): """Initialize a Bridge object.""" super().__init__(name) - set_accessory_info(self, name, model=BRIDGE_MODEL, - serial_number=BRIDGE_SERIAL_NUMBER) + self.set_info_service( + firmware_revision=__version__, manufacturer=MANUFACTURER, + model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) self.hass = hass - def _set_services(self): - add_preload_service(self, SERV_ACCESSORY_INFO) - def setup_message(self): """Prevent print of pyhap setup message to terminal.""" pass diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 9c9f60eef94..ce46e84a2ef 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,17 +23,6 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' -# #### Categories #### -CATEGORY_ALARM_SYSTEM = 'ALARM_SYSTEM' -CATEGORY_GARAGE_DOOR_OPENER = 'GARAGE_DOOR_OPENER' -CATEGORY_LIGHT = 'LIGHTBULB' -CATEGORY_LOCK = 'DOOR_LOCK' -CATEGORY_SENSOR = 'SENSOR' -CATEGORY_SWITCH = 'SWITCH' -CATEGORY_THERMOSTAT = 'THERMOSTAT' -CATEGORY_WINDOW_COVERING = 'WINDOW_COVERING' - - # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' @@ -56,7 +45,6 @@ SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition, PositionState - # #### Characteristics #### CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 8ec715e0e01..b30109f711d 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,6 +1,8 @@ """Class to hold all cover accessories.""" import logging +from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER + from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( @@ -9,12 +11,11 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_WINDOW_COVERING, SERV_WINDOW_COVERING, - CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - CATEGORY_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER, - CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, + CHAR_TARGET_POSITION, CHAR_POSITION_STATE, + SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) _LOGGER = logging.getLogger(__name__) @@ -32,12 +33,11 @@ class GarageDoorOpener(HomeAccessory): super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) self.flag_target_state = False - serv_garage_door = add_preload_service(self, SERV_GARAGE_DOOR_OPENER) - self.char_current_state = setup_char( - CHAR_CURRENT_DOOR_STATE, serv_garage_door, value=0) - self.char_target_state = setup_char( - CHAR_TARGET_DOOR_STATE, serv_garage_door, value=0, - callback=self.set_state) + serv_garage_door = self.add_preload_service(SERV_GARAGE_DOOR_OPENER) + self.char_current_state = serv_garage_door.configure_char( + CHAR_CURRENT_DOOR_STATE, value=0) + self.char_target_state = serv_garage_door.configure_char( + CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state) def set_state(self, value): """Change garage state if call came from HomeKit.""" @@ -74,12 +74,11 @@ class WindowCovering(HomeAccessory): super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.homekit_target = None - serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = setup_char( - CHAR_CURRENT_POSITION, serv_cover, value=0) - self.char_target_position = setup_char( - CHAR_TARGET_POSITION, serv_cover, value=0, - callback=self.move_cover) + serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self.char_current_position = serv_cover.configure_char( + CHAR_CURRENT_POSITION, value=0) + self.char_target_position = serv_cover.configure_char( + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) def move_cover(self, value): """Move cover to value if call came from HomeKit.""" @@ -115,14 +114,13 @@ class WindowCoveringBasic(HomeAccessory): .attributes.get(ATTR_SUPPORTED_FEATURES) self.supports_stop = features & SUPPORT_STOP - serv_cover = add_preload_service(self, SERV_WINDOW_COVERING) - self.char_current_position = setup_char( - CHAR_CURRENT_POSITION, serv_cover, value=0) - self.char_target_position = setup_char( - CHAR_TARGET_POSITION, serv_cover, value=0, - callback=self.move_cover) - self.char_position_state = setup_char( - CHAR_POSITION_STATE, serv_cover, value=2) + serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self.char_current_position = serv_cover.configure_char( + CHAR_CURRENT_POSITION, value=0) + self.char_target_position = serv_cover.configure_char( + CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + self.char_position_state = serv_cover.configure_char( + CHAR_POSITION_STATE, value=2) def move_cover(self, value): """Move cover to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 9a7bce76fba..3efb0e99df6 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -1,16 +1,17 @@ """Class to hold all light accessories.""" import logging +from pyhap.const import CATEGORY_LIGHTBULB + from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, debounce, setup_char) +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_LIGHT, SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, + SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) _LOGGER = logging.getLogger(__name__) @@ -27,7 +28,7 @@ class Light(HomeAccessory): def __init__(self, *args, config): """Initialize a new Light accessory object.""" - super().__init__(*args, category=CATEGORY_LIGHT) + super().__init__(*args, category=CATEGORY_LIGHTBULB) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, CHAR_HUE: False, CHAR_SATURATION: False, CHAR_COLOR_TEMPERATURE: False, RGB_COLOR: False} @@ -46,30 +47,28 @@ class Light(HomeAccessory): self._hue = None self._saturation = None - serv_light = add_preload_service(self, SERV_LIGHTBULB, self.chars) - self.char_on = setup_char( - CHAR_ON, serv_light, value=self._state, callback=self.set_state) + serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) + self.char_on = serv_light.configure_char( + CHAR_ON, value=self._state, setter_callback=self.set_state) if CHAR_BRIGHTNESS in self.chars: - self.char_brightness = setup_char( - CHAR_BRIGHTNESS, serv_light, value=0, - callback=self.set_brightness) + self.char_brightness = serv_light.configure_char( + CHAR_BRIGHTNESS, value=0, setter_callback=self.set_brightness) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MIN_MIREDS, 153) max_mireds = self.hass.states.get(self.entity_id) \ .attributes.get(ATTR_MAX_MIREDS, 500) - self.char_color_temperature = setup_char( - CHAR_COLOR_TEMPERATURE, serv_light, value=min_mireds, + self.char_color_temperature = serv_light.configure_char( + CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={'minValue': min_mireds, 'maxValue': max_mireds}, - callback=self.set_color_temperature) + setter_callback=self.set_color_temperature) if CHAR_HUE in self.chars: - self.char_hue = setup_char( - CHAR_HUE, serv_light, value=0, callback=self.set_hue) + self.char_hue = serv_light.configure_char( + CHAR_HUE, value=0, setter_callback=self.set_hue) if CHAR_SATURATION in self.chars: - self.char_saturation = setup_char( - CHAR_SATURATION, serv_light, value=75, - callback=self.set_saturation) + self.char_saturation = serv_light.configure_char( + CHAR_SATURATION, value=75, setter_callback=self.set_saturation) def set_state(self, value): """Set state if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index f34fc6c6a7f..e7f18d44805 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -1,13 +1,15 @@ """Class to hold all lock accessories.""" import logging +from pyhap.const import CATEGORY_DOOR_LOCK + from homeassistant.components.lock import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_LOCK, SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) + SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) _LOGGER = logging.getLogger(__name__) @@ -29,16 +31,16 @@ class Lock(HomeAccessory): def __init__(self, *args, config): """Initialize a Lock accessory object.""" - super().__init__(*args, category=CATEGORY_LOCK) + super().__init__(*args, category=CATEGORY_DOOR_LOCK) self.flag_target_state = False - serv_lock_mechanism = add_preload_service(self, SERV_LOCK) - self.char_current_state = setup_char( - CHAR_LOCK_CURRENT_STATE, serv_lock_mechanism, + serv_lock_mechanism = self.add_preload_service(SERV_LOCK) + self.char_current_state = serv_lock_mechanism.configure_char( + CHAR_LOCK_CURRENT_STATE, value=HASS_TO_HOMEKIT[STATE_UNKNOWN]) - self.char_target_state = setup_char( - CHAR_LOCK_TARGET_STATE, serv_lock_mechanism, - value=HASS_TO_HOMEKIT[STATE_LOCKED], callback=self.set_state) + self.char_target_state = serv_lock_mechanism.configure_char( + CHAR_LOCK_TARGET_STATE, value=HASS_TO_HOMEKIT[STATE_LOCKED], + setter_callback=self.set_state) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 0762e0f25f9..968e60f2842 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -1,16 +1,18 @@ """Class to hold all alarm control panel accessories.""" import logging +from pyhap.const import CATEGORY_ALARM_SYSTEM + from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ATTR_ENTITY_ID, ATTR_CODE) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_ALARM_SYSTEM, SERV_SECURITY_SYSTEM, - CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE) + SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, + CHAR_TARGET_SECURITY_STATE) _LOGGER = logging.getLogger(__name__) @@ -33,12 +35,12 @@ class SecuritySystem(HomeAccessory): self._alarm_code = config.get(ATTR_CODE) self.flag_target_state = False - serv_alarm = add_preload_service(self, SERV_SECURITY_SYSTEM) - self.char_current_state = setup_char( - CHAR_CURRENT_SECURITY_STATE, serv_alarm, value=3) - self.char_target_state = setup_char( - CHAR_TARGET_SECURITY_STATE, serv_alarm, value=3, - callback=self.set_security_state) + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) + self.char_current_state = serv_alarm.configure_char( + CHAR_CURRENT_SECURITY_STATE, value=3) + self.char_target_state = serv_alarm.configure_char( + CHAR_TARGET_SECURITY_STATE, value=3, + setter_callback=self.set_security_state) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7d7bbc5edd6..393b6beffd6 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -1,14 +1,16 @@ """Class to hold all sensor accessories.""" import logging +from pyhap.const import CATEGORY_SENSOR + from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char +from .accessories import HomeAccessory from .const import ( - CATEGORY_SENSOR, SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, + SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, @@ -52,10 +54,9 @@ class TemperatureSensor(HomeAccessory): def __init__(self, *args, config): """Initialize a TemperatureSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_temp = add_preload_service(self, SERV_TEMPERATURE_SENSOR) - self.char_temp = setup_char( - CHAR_CURRENT_TEMPERATURE, serv_temp, value=0, - properties=PROP_CELSIUS) + serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) + self.char_temp = serv_temp.configure_char( + CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS) self.unit = None def update_state(self, new_state): @@ -76,9 +77,9 @@ class HumiditySensor(HomeAccessory): def __init__(self, *args, config): """Initialize a HumiditySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_humidity = add_preload_service(self, SERV_HUMIDITY_SENSOR) - self.char_humidity = setup_char( - CHAR_CURRENT_HUMIDITY, serv_humidity, value=0) + serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) + self.char_humidity = serv_humidity.configure_char( + CHAR_CURRENT_HUMIDITY, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -97,12 +98,12 @@ class AirQualitySensor(HomeAccessory): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_air_quality = add_preload_service(self, SERV_AIR_QUALITY_SENSOR, - [CHAR_AIR_PARTICULATE_DENSITY]) - self.char_quality = setup_char( - CHAR_AIR_QUALITY, serv_air_quality, value=0) - self.char_density = setup_char( - CHAR_AIR_PARTICULATE_DENSITY, serv_air_quality, value=0) + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]) + self.char_quality = serv_air_quality.configure_char( + CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char( + CHAR_AIR_PARTICULATE_DENSITY, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -121,14 +122,14 @@ class CarbonDioxideSensor(HomeAccessory): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_co2 = add_preload_service(self, SERV_CARBON_DIOXIDE_SENSOR, [ + serv_co2 = self.add_preload_service(SERV_CARBON_DIOXIDE_SENSOR, [ CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL]) - self.char_co2 = setup_char( - CHAR_CARBON_DIOXIDE_LEVEL, serv_co2, value=0) - self.char_peak = setup_char( - CHAR_CARBON_DIOXIDE_PEAK_LEVEL, serv_co2, value=0) - self.char_detected = setup_char( - CHAR_CARBON_DIOXIDE_DETECTED, serv_co2, value=0) + self.char_co2 = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_LEVEL, value=0) + self.char_peak = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, value=0) + self.char_detected = serv_co2.configure_char( + CHAR_CARBON_DIOXIDE_DETECTED, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -149,9 +150,9 @@ class LightSensor(HomeAccessory): """Initialize a LightSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) - serv_light = add_preload_service(self, SERV_LIGHT_SENSOR) - self.char_light = setup_char( - CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, serv_light, value=0) + serv_light = self.add_preload_service(SERV_LIGHT_SENSOR) + self.char_light = serv_light.configure_char( + CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0) def update_state(self, new_state): """Update accessory after state change.""" @@ -174,8 +175,8 @@ class BinarySensor(HomeAccessory): if device_class in BINARY_SENSOR_SERVICE_MAP \ else BINARY_SENSOR_SERVICE_MAP[DEVICE_CLASS_OCCUPANCY] - service = add_preload_service(self, service_char[0]) - self.char_detected = setup_char(service_char[1], service, value=0) + service = self.add_preload_service(service_char[0]) + self.char_detected = service.configure_char(service_char[1], value=0) def update_state(self, new_state): """Update accessory after state change.""" diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index aaf13e4ea7e..68a4fcdab0a 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,13 +1,15 @@ """Class to hold all switch accessories.""" import logging +from pyhap.const import CATEGORY_SWITCH + from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES -from .accessories import HomeAccessory, add_preload_service, setup_char -from .const import CATEGORY_SWITCH, SERV_SWITCH, CHAR_ON +from .accessories import HomeAccessory +from .const import SERV_SWITCH, CHAR_ON _LOGGER = logging.getLogger(__name__) @@ -22,9 +24,9 @@ class Switch(HomeAccessory): self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False - serv_switch = add_preload_service(self, SERV_SWITCH) - self.char_on = setup_char( - CHAR_ON, serv_switch, value=False, callback=self.set_state) + serv_switch = self.add_preload_service(SERV_SWITCH) + self.char_on = serv_switch.configure_char( + CHAR_ON, value=False, setter_callback=self.set_state) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 4faceefe850..15fd8160a7e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -1,6 +1,8 @@ """Class to hold all thermostat accessories.""" import logging +from pyhap.const import CATEGORY_THERMOSTAT + from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -12,10 +14,9 @@ from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import ( - HomeAccessory, add_preload_service, debounce, setup_char) +from .accessories import HomeAccessory, debounce from .const import ( - CATEGORY_THERMOSTAT, SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, + SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) @@ -57,38 +58,37 @@ class Thermostat(HomeAccessory): self.chars.extend((CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)) - serv_thermostat = add_preload_service( - self, SERV_THERMOSTAT, self.chars) + serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) # Current and target mode characteristics - self.char_current_heat_cool = setup_char( - CHAR_CURRENT_HEATING_COOLING, serv_thermostat, value=0) - self.char_target_heat_cool = setup_char( - CHAR_TARGET_HEATING_COOLING, serv_thermostat, value=0, - callback=self.set_heat_cool) + self.char_current_heat_cool = serv_thermostat.configure_char( + CHAR_CURRENT_HEATING_COOLING, value=0) + self.char_target_heat_cool = serv_thermostat.configure_char( + CHAR_TARGET_HEATING_COOLING, value=0, + setter_callback=self.set_heat_cool) # Current and target temperature characteristics - self.char_current_temp = setup_char( - CHAR_CURRENT_TEMPERATURE, serv_thermostat, value=21.0) - self.char_target_temp = setup_char( - CHAR_TARGET_TEMPERATURE, serv_thermostat, value=21.0, - callback=self.set_target_temperature) + self.char_current_temp = serv_thermostat.configure_char( + CHAR_CURRENT_TEMPERATURE, value=21.0) + self.char_target_temp = serv_thermostat.configure_char( + CHAR_TARGET_TEMPERATURE, value=21.0, + setter_callback=self.set_target_temperature) # Display units characteristic - self.char_display_units = setup_char( - CHAR_TEMP_DISPLAY_UNITS, serv_thermostat, value=0) + self.char_display_units = serv_thermostat.configure_char( + CHAR_TEMP_DISPLAY_UNITS, value=0) # If the device supports it: high and low temperature characteristics self.char_cooling_thresh_temp = None self.char_heating_thresh_temp = None if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: - self.char_cooling_thresh_temp = setup_char( - CHAR_COOLING_THRESHOLD_TEMPERATURE, serv_thermostat, - value=23.0, callback=self.set_cooling_threshold) + self.char_cooling_thresh_temp = serv_thermostat.configure_char( + CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: - self.char_heating_thresh_temp = setup_char( - CHAR_HEATING_THRESHOLD_TEMPERATURE, serv_thermostat, - value=19.0, callback=self.set_heating_threshold) + self.char_heating_thresh_temp = serv_thermostat.configure_char( + CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + setter_callback=self.set_heating_threshold) def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" diff --git a/requirements_all.txt b/requirements_all.txt index 002c5eb93e2..0981ef154c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==1.1.9 +HAP-python==2.0.0 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0605b3d2e24..0e0b4f4da9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.4 # homeassistant.components.homekit -HAP-python==1.1.9 +HAP-python==2.0.0 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 3df76185a51..faa982f62f3 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -7,12 +7,12 @@ import unittest from unittest.mock import call, patch, Mock from homeassistant.components.homekit.accessories import ( - add_preload_service, set_accessory_info, debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, SERV_ACCESSORY_INFO, CHAR_FIRMWARE_REVISION, - CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, MANUFACTURER) -from homeassistant.const import ATTR_NOW, EVENT_TIME_CHANGED + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, + CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, + CHAR_SERIAL_NUMBER, MANUFACTURER) +from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant @@ -62,73 +62,25 @@ class TestAccessories(unittest.TestCase): hass.stop() - def test_add_preload_service(self): - """Test add_preload_service without additional characteristics.""" - acc = Mock() - serv = add_preload_service(acc, 'AirPurifier') - self.assertEqual(acc.mock_calls, [call.add_service(serv)]) - with self.assertRaises(ValueError): - serv.get_characteristic('Name') - - # Test with typo in service name - with self.assertRaises(KeyError): - add_preload_service(Mock(), 'AirPurifierTypo') - - # Test adding additional characteristic as string - serv = add_preload_service(Mock(), 'AirPurifier', 'Name') - serv.get_characteristic('Name') - - # Test adding additional characteristics as list - serv = add_preload_service(Mock(), 'AirPurifier', - ['Name', 'RotationSpeed']) - serv.get_characteristic('Name') - serv.get_characteristic('RotationSpeed') - - # Test adding additional characteristic with typo - with self.assertRaises(KeyError): - add_preload_service(Mock(), 'AirPurifier', 'NameTypo') - - def test_set_accessory_info(self): - """Test setting the basic accessory information.""" - # Test HomeAccessory - acc = HomeAccessory('HA', 'Home Accessory', 'homekit.accessory', 2, '') - set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') - - serv = acc.get_service(SERV_ACCESSORY_INFO) - self.assertEqual(serv.get_characteristic(CHAR_NAME).value, 'name') - self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') - - # Test HomeBridge - acc = HomeBridge('hass') - set_accessory_info(acc, 'name', 'model', '0000', MANUFACTURER, '1.2.3') - - serv = acc.get_service(SERV_ACCESSORY_INFO) - self.assertEqual(serv.get_characteristic(CHAR_MODEL).value, 'model') - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, '0000') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, '1.2.3') - def test_home_accessory(self): """Test HomeAccessory class.""" hass = get_test_home_assistant() - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, '') + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) self.assertEqual(acc.hass, hass) self.assertEqual(acc.display_name, 'Home Accessory') self.assertEqual(acc.category, 1) # Category.OTHER self.assertEqual(len(acc.services), 1) serv = acc.services[0] # SERV_ACCESSORY_INFO + self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_NAME).value, 'Home Accessory') + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, 'Homekit') + self.assertEqual(serv.get_characteristic(CHAR_SERIAL_NUMBER).value, + 'homekit.accessory') hass.states.set('homekit.accessory', 'on') hass.block_till_done() @@ -136,7 +88,7 @@ class TestAccessories(unittest.TestCase): hass.states.set('homekit.accessory', 'off') hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, '') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) self.assertEqual(acc.display_name, 'test_name') self.assertEqual(acc.aid, 2) self.assertEqual(len(acc.services), 1) @@ -155,8 +107,17 @@ class TestAccessories(unittest.TestCase): self.assertEqual(len(bridge.services), 1) serv = bridge.services[0] # SERV_ACCESSORY_INFO self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) + self.assertEqual( + serv.get_characteristic(CHAR_NAME).value, BRIDGE_NAME) + self.assertEqual( + serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, __version__) + self.assertEqual( + serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) self.assertEqual( serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) + self.assertEqual( + serv.get_characteristic(CHAR_SERIAL_NUMBER).value, + BRIDGE_SERIAL_NUMBER) bridge = HomeBridge('hass', 'test_name') self.assertEqual(bridge.display_name, 'test_name') diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 7ae37becbd5..082953038b5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -4,7 +4,9 @@ from unittest.mock import call, patch, ANY, Mock from homeassistant import setup from homeassistant.core import State -from homeassistant.components.homekit import HomeKit, generate_aid +from homeassistant.components.homekit import ( + HomeKit, generate_aid, + STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, @@ -79,24 +81,28 @@ class TestHomeKit(unittest.TestCase): CONF_IP_ADDRESS: '172.0.0.0'}} self.assertTrue(setup.setup_component( self.hass, DOMAIN, config)) - - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) self.hass.block_till_done() self.assertEqual(mock_homekit.mock_calls, [ call(self.hass, 11111, '172.0.0.0', ANY, {}), call().setup()]) - # Test start call with driver stopped. + # Test auto_start disabled homekit.reset_mock() - homekit.configure_mock(**{'started': False}) + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + self.hass.block_till_done() + self.assertEqual(homekit.mock_calls, []) + + # Test start call with driver is ready + homekit.reset_mock() + homekit.status = STATUS_READY self.hass.services.call('homekit', 'start') self.assertEqual(homekit.mock_calls, [call.start()]) - # Test start call with driver started. + # Test start call with driver started homekit.reset_mock() - homekit.configure_mock(**{'started': True}) + homekit.status = STATUS_STOPPED self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) self.assertEqual(homekit.mock_calls, []) @@ -180,34 +186,38 @@ class TestHomeKit(unittest.TestCase): state = self.hass.states.all()[0] homekit.start() + self.hass.block_till_done() self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) self.assertEqual(mock_show_setup_msg.mock_calls, [ call(self.hass, homekit.bridge)]) self.assertEqual(homekit.driver.mock_calls, [call.start()]) - self.assertTrue(homekit.started) + self.assertEqual(homekit.status, STATUS_RUNNING) # Test start() if already started homekit.driver.reset_mock() homekit.start() + self.hass.block_till_done() self.assertEqual(homekit.driver.mock_calls, []) def test_homekit_stop(self): """Test HomeKit stop method.""" - homekit = HomeKit(None, None, None, None, None) + homekit = HomeKit(self.hass, None, None, None, None) homekit.driver = Mock() - # Test if started = False + self.assertEqual(homekit.status, STATUS_READY) homekit.stop() - self.assertFalse(homekit.driver.stop.called) - - # Test if driver not started - homekit.started = True - homekit.driver.configure_mock(**{'run_sentinel': None}) + self.hass.block_till_done() + homekit.status = STATUS_WAIT homekit.stop() + self.hass.block_till_done() + homekit.status = STATUS_STOPPED + homekit.stop() + self.hass.block_till_done() self.assertFalse(homekit.driver.stop.called) # Test if driver is started - homekit.driver.configure_mock(**{'run_sentinel': 'sentinel'}) + homekit.status = STATUS_RUNNING homekit.stop() + self.hass.block_till_done() self.assertTrue(homekit.driver.stop.called) From 7900ba30bff3eb41976380515c412b8257716c3a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 4 May 2018 17:09:05 +0200 Subject: [PATCH 602/924] Upgrade holidays to 0.9.5 (#14274) --- homeassistant/components/binary_sensor/workday.py | 15 ++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/binary_sensor/workday.py b/homeassistant/components/binary_sensor/workday.py index 8935ad5115d..b37be3f6cb6 100644 --- a/homeassistant/components/binary_sensor/workday.py +++ b/homeassistant/components/binary_sensor/workday.py @@ -17,16 +17,17 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['holidays==0.9.4'] +REQUIREMENTS = ['holidays==0.9.5'] # List of all countries currently supported by holidays # There seems to be no way to get the list out at runtime -ALL_COUNTRIES = ['Australia', 'AU', 'Austria', 'AT', 'Belgium', 'BE', 'Canada', - 'CA', 'Colombia', 'CO', 'Czech', 'CZ', 'Denmark', 'DK', - 'England', 'EuropeanCentralBank', 'ECB', 'TAR', 'Finland', - 'FI', 'France', 'FRA', 'Germany', 'DE', 'Ireland', - 'Isle of Man', 'Italy', 'IT', 'Japan', 'JP', 'Mexico', 'MX', - 'Netherlands', 'NL', 'NewZealand', 'NZ', 'Northern Ireland', +ALL_COUNTRIES = ['Argentina', 'AR', 'Australia', 'AU', 'Austria', 'AT', + 'Belgium', 'BE', 'Canada', 'CA', 'Colombia', 'CO', 'Czech', + 'CZ', 'Denmark', 'DK', 'England', 'EuropeanCentralBank', + 'ECB', 'TAR', 'Finland', 'FI', 'France', 'FRA', 'Germany', + 'DE', 'Hungary', 'HU', 'Ireland', 'Isle of Man', 'Italy', + 'IT', 'Japan', 'JP', 'Mexico', 'MX', 'Netherlands', 'NL', + 'NewZealand', 'NZ', 'Northern Ireland', 'Norway', 'NO', 'Polish', 'PL', 'Portugal', 'PT', 'PortugalExt', 'PTE', 'Scotland', 'Slovenia', 'SI', 'Slovakia', 'SK', 'South Africa', 'ZA', 'Spain', 'ES', diff --git a/requirements_all.txt b/requirements_all.txt index 0981ef154c4..395253c0f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ hikvision==0.4 hipnotify==1.0.8 # homeassistant.components.binary_sensor.workday -holidays==0.9.4 +holidays==0.9.5 # homeassistant.components.frontend home-assistant-frontend==20180426.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e0b4f4da9f..7816d9c6f24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -78,7 +78,7 @@ haversine==0.4.5 hbmqtt==0.9.1 # homeassistant.components.binary_sensor.workday -holidays==0.9.4 +holidays==0.9.5 # homeassistant.components.frontend home-assistant-frontend==20180426.0 From bb76ba67f319775cc8abf1047098b0b723eab7b1 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 4 May 2018 22:48:38 +0200 Subject: [PATCH 603/924] Homekit: Changed device_class requirement Humidity Sensor (#14277) --- homeassistant/components/homekit/__init__.py | 9 ++++----- tests/components/homekit/test_get_accessories.py | 10 ++-------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 080dd2a7cbd..3abfffd67e0 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -126,10 +126,10 @@ def get_accessory(hass, state, aid, config): unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) - if device_class == DEVICE_CLASS_TEMPERATURE or unit == TEMP_CELSIUS \ - or unit == TEMP_FAHRENHEIT: + if device_class == DEVICE_CLASS_TEMPERATURE or \ + unit in (TEMP_CELSIUS, TEMP_FAHRENHEIT): a_type = 'TemperatureSensor' - elif device_class == DEVICE_CLASS_HUMIDITY or unit == '%': + elif device_class == DEVICE_CLASS_HUMIDITY and unit == '%': a_type = 'HumiditySensor' elif device_class == DEVICE_CLASS_PM25 \ or DEVICE_CLASS_PM25 in state.entity_id: @@ -141,8 +141,7 @@ def get_accessory(hass, state, aid, config): unit == 'lux' or unit == 'lx': a_type = 'LightSensor' - elif state.domain == 'switch' or state.domain == 'remote' \ - or state.domain == 'input_boolean' or state.domain == 'script': + elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): a_type = 'Switch' if a_type is None: diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 76736ce45ad..71f9c8e6656 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -68,14 +68,8 @@ class TestGetAccessories(unittest.TestCase): """Test humidity sensor with device class humidity.""" with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): state = State('sensor.humidity', '20', - {ATTR_DEVICE_CLASS: 'humidity'}) - get_accessory(None, state, 2, {}) - - def test_sensor_humidity_unit(self): - """Test humidity sensor with % as unit.""" - with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): - state = State('sensor.humidity', '20', - {ATTR_UNIT_OF_MEASUREMENT: '%'}) + {ATTR_DEVICE_CLASS: 'humidity', + ATTR_UNIT_OF_MEASUREMENT: '%'}) get_accessory(None, state, 2, {}) def test_air_quality_sensor(self): From 255a85ad022e1c3708adb25799fd01a0d9c10e3f Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 4 May 2018 18:09:16 -0400 Subject: [PATCH 604/924] HomeKit: Support triggered state for alarm_control_panel (#14285) --- .../homekit/type_security_systems.py | 22 +++++++++++-------- .../homekit/test_type_security_systems.py | 10 +++++++-- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 968e60f2842..e32860d1fef 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -6,7 +6,7 @@ from pyhap.const import CATEGORY_ALARM_SYSTEM from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - ATTR_ENTITY_ID, ATTR_CODE) + STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) from . import TYPES from .accessories import HomeAccessory @@ -16,13 +16,16 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -HASS_TO_HOMEKIT = {STATE_ALARM_DISARMED: 3, STATE_ALARM_ARMED_HOME: 0, - STATE_ALARM_ARMED_AWAY: 1, STATE_ALARM_ARMED_NIGHT: 2} +HASS_TO_HOMEKIT = {STATE_ALARM_ARMED_HOME: 0, + STATE_ALARM_ARMED_AWAY: 1, + STATE_ALARM_ARMED_NIGHT: 2, + STATE_ALARM_DISARMED: 3, + STATE_ALARM_TRIGGERED: 4} HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} -STATE_TO_SERVICE = {STATE_ALARM_DISARMED: 'alarm_disarm', - STATE_ALARM_ARMED_HOME: 'alarm_arm_home', +STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', STATE_ALARM_ARMED_AWAY: 'alarm_arm_away', - STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night'} + STATE_ALARM_ARMED_NIGHT: 'alarm_arm_night', + STATE_ALARM_DISARMED: 'alarm_disarm'} @TYPES.register('SecuritySystem') @@ -64,7 +67,8 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - if not self.flag_target_state: + # SecuritySystemTargetSTate does not support triggered + if not self.flag_target_state and \ + hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) - if self.char_target_state.value == self.char_current_state.value: - self.flag_target_state = False + self.flag_target_state = False diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 9c1ff0faf1a..baa461af772 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -7,7 +7,8 @@ from homeassistant.components.homekit.type_security_systems import ( from homeassistant.const import ( ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_UNKNOWN) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import get_test_home_assistant @@ -65,10 +66,15 @@ class TestHomekitSecuritySystems(unittest.TestCase): self.assertEqual(acc.char_target_state.value, 3) self.assertEqual(acc.char_current_state.value, 3) + self.hass.states.set(acp, STATE_ALARM_TRIGGERED) + self.hass.block_till_done() + self.assertEqual(acc.char_target_state.value, 3) + self.assertEqual(acc.char_current_state.value, 4) + self.hass.states.set(acp, STATE_UNKNOWN) self.hass.block_till_done() self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 3) + self.assertEqual(acc.char_current_state.value, 4) # Set from HomeKit acc.char_target_state.client_update_value(0) From 354470469f6b5b464218f6934eb5e0e144acfe6b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 5 May 2018 02:10:08 +0100 Subject: [PATCH 605/924] Fix filter sensor missing window_size argument (#14252) * missing window_size argument * test throttle filter configuration --- homeassistant/components/sensor/filter.py | 2 ++ tests/components/sensor/test_filter.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/filter.py b/homeassistant/components/sensor/filter.py index 5b28faf78ca..9c05028b394 100644 --- a/homeassistant/components/sensor/filter.py +++ b/homeassistant/components/sensor/filter.py @@ -89,6 +89,8 @@ FILTER_TIME_SMA_SCHEMA = FILTER_SCHEMA.extend({ FILTER_THROTTLE_SCHEMA = FILTER_SCHEMA.extend({ vol.Required(CONF_FILTER_NAME): FILTER_NAME_THROTTLE, + vol.Optional(CONF_FILTER_WINDOW_SIZE, + default=DEFAULT_WINDOW_SIZE): vol.Coerce(int), }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ diff --git a/tests/components/sensor/test_filter.py b/tests/components/sensor/test_filter.py index 43432f3304c..8e79306fe13 100644 --- a/tests/components/sensor/test_filter.py +++ b/tests/components/sensor/test_filter.py @@ -67,6 +67,9 @@ class TestFilterSensor(unittest.TestCase): 'filter': 'lowpass', 'time_constant': 10, 'precision': 2 + }, { + 'filter': 'throttle', + 'window_size': 1 }] } } From 75bf4830713c7a4dca018bc568ac37693204b831 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 5 May 2018 10:45:09 +0200 Subject: [PATCH 606/924] Upgrade astral to 1.6.1 (#14297) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0f3c9ac255d..f6666c829e0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -7,7 +7,7 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 -astral==1.6 +astral==1.6.1 certifi>=2017.4.17 attrs==18.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 395253c0f21..88dd3d60904 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ voluptuous==0.11.1 typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 -astral==1.6 +astral==1.6.1 certifi>=2017.4.17 attrs==18.1.0 diff --git a/setup.py b/setup.py index 3db7c737a2c..8a68617afd9 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ REQUIRES = [ 'typing>=3,<4', 'aiohttp==3.1.3', 'async_timeout==2.0.1', - 'astral==1.6', + 'astral==1.6.1', 'certifi>=2017.4.17', 'attrs==18.1.0', ] From 5ade84d75f70165795079c585e438c3ab46b4662 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Sat, 5 May 2018 19:17:28 +1000 Subject: [PATCH 607/924] BOM Weather throttle fix (#14234) --- homeassistant/components/sensor/bom.py | 39 +++++++++----------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 272d5d1e0b8..128f532e459 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -33,8 +33,7 @@ CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' CONF_WMO_ID = 'wmo_id' -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=60) -LAST_UPDATE = 0 +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) # Sensor types are defined like: Name, units SENSOR_TYPES = { @@ -114,13 +113,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Could not get BOM weather station from lat/lon") return False - rest = BOMCurrentData(hass, station) + bom_data = BOMCurrentData(hass, station) try: - rest.update() + bom_data.update() except ValueError as err: _LOGGER.error("Received error from BOM_Current: %s", err) return False - add_devices([BOMCurrentSensor(rest, variable, config.get(CONF_NAME)) + add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) for variable in config[CONF_MONITORED_CONDITIONS]]) return True @@ -128,9 +127,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class BOMCurrentSensor(Entity): """Implementation of a BOM current sensor.""" - def __init__(self, rest, condition, stationname): + def __init__(self, bom_data, condition, stationname): """Initialize the sensor.""" - self.rest = rest + self.bom_data = bom_data self._condition = condition self.stationname = stationname @@ -146,8 +145,8 @@ class BOMCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.rest.data and self._condition in self.rest.data: - return self.rest.data[self._condition] + if self.bom_data.data and self._condition in self.bom_data.data: + return self.bom_data.data[self._condition] return STATE_UNKNOWN @@ -156,11 +155,11 @@ class BOMCurrentSensor(Entity): """Return the state attributes of the device.""" attr = {} attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.rest.data['history_product'] - attr['Station Id'] = self.rest.data['wmo'] - attr['Station Name'] = self.rest.data['name'] + attr['Zone Id'] = self.bom_data.data['history_product'] + attr['Station Id'] = self.bom_data.data['wmo'] + attr['Station Name'] = self.bom_data.data['name'] attr['Last Update'] = datetime.datetime.strptime(str( - self.rest.data['local_date_time_full']), '%Y%m%d%H%M%S') + self.bom_data.data['local_date_time_full']), '%Y%m%d%H%M%S') attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr @@ -171,7 +170,7 @@ class BOMCurrentSensor(Entity): def update(self): """Update current conditions.""" - self.rest.update() + self.bom_data.update() class BOMCurrentData(object): @@ -182,7 +181,6 @@ class BOMCurrentData(object): self._hass = hass self._zone_id, self._wmo_id = station_id.split('.') self.data = None - self._lastupdate = LAST_UPDATE def _build_url(self): url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) @@ -192,20 +190,9 @@ class BOMCurrentData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from BOM.""" - if self._lastupdate != 0 and \ - ((datetime.datetime.now() - self._lastupdate) < - datetime.timedelta(minutes=35)): - _LOGGER.info( - "BOM was updated %s minutes ago, skipping update as" - " < 35 minutes", (datetime.datetime.now() - self._lastupdate)) - return self._lastupdate - try: result = requests.get(self._build_url(), timeout=10).json() self.data = result['observations']['data'][0] - self._lastupdate = datetime.datetime.strptime( - str(self.data['local_date_time_full']), '%Y%m%d%H%M%S') - return self._lastupdate except ValueError as err: _LOGGER.error("Check BOM %s", err.args) self.data = None From ec3ce4c80d67be9d90f5d87433e13b7a9778c941 Mon Sep 17 00:00:00 2001 From: blackwind Date: Sat, 5 May 2018 07:31:39 -0600 Subject: [PATCH 608/924] Publish attributes unconditionally (#14179) * Publish attributes unconditionally Because the attribute publish command was previously hidden behind `if val:`, falsy values like False and 0.0 weren't being published, thereby making Statestream -- particularly in the case of booleans, where the first True would be retained indefinitely -- a completely worthless indicator of state. * Change bool test to False to confirm falsy values pass --- homeassistant/components/mqtt_statestream.py | 7 +++---- tests/components/test_mqtt_statestream.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt_statestream.py b/homeassistant/components/mqtt_statestream.py index 4427870c294..205a638c574 100644 --- a/homeassistant/components/mqtt_statestream.py +++ b/homeassistant/components/mqtt_statestream.py @@ -88,10 +88,9 @@ def async_setup(hass, config): if publish_attributes: for key, val in new_state.attributes.items(): - if val: - encoded_val = json.dumps(val, cls=JSONEncoder) - hass.components.mqtt.async_publish(mybase + key, - encoded_val, 1, True) + encoded_val = json.dumps(val, cls=JSONEncoder) + hass.components.mqtt.async_publish(mybase + key, + encoded_val, 1, True) async_track_state_change(hass, MATCH_ALL, _state_publisher) return True diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index 76d8e48d03a..e120c3a7dd2 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -134,7 +134,7 @@ class TestMqttStateStream(object): test_attributes = { "testing": "YES", "list": ["a", "b", "c"], - "bool": True + "bool": False } # Set a state of an entity @@ -150,7 +150,7 @@ class TestMqttStateStream(object): 1, True), call.async_publish(self.hass, 'pub/fake/entity/list', '["a", "b", "c"]', 1, True), - call.async_publish(self.hass, 'pub/fake/entity/bool', "true", + call.async_publish(self.hass, 'pub/fake/entity/bool', "false", 1, True) ] From 95d27bd1fa5be4e839bd26e105b7d5737142e332 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 5 May 2018 15:37:40 +0200 Subject: [PATCH 609/924] Sensor device classes (#14282) * Added light device class, moved device classes to const * Removed unnecessary icons * Replace 'lux' with 'lx' * Fix comment * Changed device_class name --- homeassistant/components/homekit/__init__.py | 9 +++-- homeassistant/components/sensor/__init__.py | 10 ++++-- homeassistant/components/sensor/abode.py | 20 ++++++----- homeassistant/components/sensor/bh1750.py | 4 +-- homeassistant/components/sensor/deconz.py | 12 ++----- homeassistant/components/sensor/demo.py | 8 +++-- homeassistant/components/sensor/ecobee.py | 5 +-- homeassistant/components/sensor/homematic.py | 2 +- homeassistant/components/sensor/isy994.py | 2 +- .../components/sensor/linux_battery.py | 11 ++---- homeassistant/components/sensor/miflora.py | 2 +- homeassistant/components/sensor/mysensors.py | 2 +- homeassistant/components/sensor/nest.py | 7 ++-- homeassistant/components/sensor/tahoma.py | 2 +- .../components/sensor/tellduslive.py | 36 ++++++++++++------- homeassistant/components/sensor/vera.py | 2 +- .../components/sensor/xiaomi_aqara.py | 20 +++++++---- homeassistant/const.py | 6 ++++ .../homekit/test_get_accessories.py | 11 ++---- 19 files changed, 92 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 3abfffd67e0..c31093a5eb8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -14,7 +14,8 @@ from homeassistant.components.cover import ( from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, - TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip @@ -22,8 +23,7 @@ from homeassistant.util.decorator import Registry from .const import ( DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_LIGHT, DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PM25, DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) from .util import ( validate_entity_config, show_setup_message) @@ -137,8 +137,7 @@ def get_accessory(hass, state, aid, config): elif device_class == DEVICE_CLASS_CO2 \ or DEVICE_CLASS_CO2 in state.entity_id: a_type = 'CarbonDioxideSensor' - elif device_class == DEVICE_CLASS_LIGHT or unit == 'lm' or \ - unit == 'lux' or unit == 'lx': + elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index bed1850b34d..8550d175b63 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -12,6 +12,9 @@ import voluptuous as vol from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -21,9 +24,10 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SCAN_INTERVAL = timedelta(seconds=30) DEVICE_CLASSES = [ - 'battery', # % of battery that is left - 'humidity', # % of humidity in the air - 'temperature', # temperature (C/F) + DEVICE_CLASS_BATTERY, # % of battery that is left + DEVICE_CLASS_HUMIDITY, # % of humidity in the air + DEVICE_CLASS_ILLUMINANCE, # current light level (lx/lm) + DEVICE_CLASS_TEMPERATURE, # temperature (C/F) ] DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) diff --git a/homeassistant/components/sensor/abode.py b/homeassistant/components/sensor/abode.py index 1a700e24de6..b51ab288c1a 100644 --- a/homeassistant/components/sensor/abode.py +++ b/homeassistant/components/sensor/abode.py @@ -7,6 +7,8 @@ https://home-assistant.io/components/sensor.abode/ import logging from homeassistant.components.abode import AbodeDevice, DOMAIN as ABODE_DOMAIN +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) _LOGGER = logging.getLogger(__name__) @@ -14,9 +16,9 @@ DEPENDENCIES = ['abode'] # Sensor types: Name, icon SENSOR_TYPES = { - 'temp': ['Temperature', 'thermometer'], - 'humidity': ['Humidity', 'water-percent'], - 'lux': ['Lux', 'lightbulb'], + 'temp': ['Temperature', DEVICE_CLASS_TEMPERATURE], + 'humidity': ['Humidity', DEVICE_CLASS_HUMIDITY], + 'lux': ['Lux', DEVICE_CLASS_ILLUMINANCE], } @@ -46,20 +48,20 @@ class AbodeSensor(AbodeDevice): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self._sensor_type = sensor_type - self._icon = 'mdi:{}'.format(SENSOR_TYPES[self._sensor_type][1]) self._name = '{0} {1}'.format( self._device.name, SENSOR_TYPES[self._sensor_type][0]) - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon + self._device_class = SENSOR_TYPES[self._sensor_type][1] @property def name(self): """Return the name of the sensor.""" return self._name + @property + def device_class(self): + """Return the device class.""" + return self._device_class + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/sensor/bh1750.py b/homeassistant/components/sensor/bh1750.py index 0c538a6cfcc..6d34d4ea9f8 100644 --- a/homeassistant/components/sensor/bh1750.py +++ b/homeassistant/components/sensor/bh1750.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE from homeassistant.helpers.entity import Entity REQUIREMENTS = ['i2csense==0.0.4', @@ -130,7 +130,7 @@ class BH1750Sensor(Entity): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - return 'light' + return DEVICE_CLASS_ILLUMINANCE @asyncio.coroutine def async_update(self): diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 69be7f52d6c..b4a3cb8c6c5 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -6,10 +6,10 @@ https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_VOLTAGE +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify DEPENDENCIES = ['deconz'] @@ -126,7 +126,6 @@ class DeconzBattery(Entity): """Register dispatcher callback for update of battery state.""" self._device = device self._name = '{} {}'.format(self._device.name, 'Battery Level') - self._device_class = 'battery' self._unit_of_measurement = "%" async def async_added_to_hass(self): @@ -158,12 +157,7 @@ class DeconzBattery(Entity): @property def device_class(self): """Return the class of the sensor.""" - return self._device_class - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return icon_for_battery_level(int(self.state)) + return DEVICE_CLASS_BATTERY @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/demo.py b/homeassistant/components/sensor/demo.py index 5cae1a47c23..325d3e0ae58 100644 --- a/homeassistant/components/sensor/demo.py +++ b/homeassistant/components/sensor/demo.py @@ -4,7 +4,9 @@ Demo platform that has a couple of fake sensors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/demo/ """ -from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE) from homeassistant.helpers.entity import Entity @@ -12,9 +14,9 @@ from homeassistant.helpers.entity import Entity def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Demo sensors.""" add_devices([ - DemoSensor('Outside Temperature', 15.6, 'temperature', + DemoSensor('Outside Temperature', 15.6, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 12), - DemoSensor('Outside Humidity', 54, 'humidity', '%', None), + DemoSensor('Outside Humidity', 54, DEVICE_CLASS_HUMIDITY, '%', None), ]) diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index 7274f421f15..a478f964f5a 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.ecobee/ """ from homeassistant.components import ecobee -from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT) from homeassistant.helpers.entity import Entity DEPENDENCIES = ['ecobee'] @@ -55,7 +56,7 @@ class EcobeeSensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - if self.type in ('temperature', 'humidity'): + if self.type in (DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE): return self.type return None diff --git a/homeassistant/components/sensor/homematic.py b/homeassistant/components/sensor/homematic.py index 350f1e2eb59..bdbc207a79c 100644 --- a/homeassistant/components/sensor/homematic.py +++ b/homeassistant/components/sensor/homematic.py @@ -43,7 +43,7 @@ HM_UNIT_HA_CAST = { 'ENERGY_COUNTER': 'Wh', 'GAS_POWER': 'm3', 'GAS_ENERGY_COUNTER': 'm3', - 'LUX': 'lux', + 'LUX': 'lx', 'RAIN_COUNTER': 'mm', 'WIND_SPEED': 'km/h', 'WIND_DIRECTION': '°', diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index c34a4a8fca7..ecf7bc0b8c2 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -49,7 +49,7 @@ UOM_FRIENDLY_NAME = { '33': 'kWH', '34': 'liedu', '35': 'l', - '36': 'lux', + '36': 'lx', '37': 'mercalli', '38': 'm', '39': 'm³/hr', diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index 1f0e3e89e5c..aad8c2f7a92 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -10,7 +10,7 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -48,8 +48,6 @@ DEFAULT_SYSTEM = 'linux' SYSTEMS = ['android', 'linux'] -ICON = 'mdi:battery' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_BATTERY, default=DEFAULT_BATTERY): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -97,7 +95,7 @@ class LinuxBatterySensor(Entity): @property def device_class(self): """Return the device class of the sensor.""" - return 'battery' + return DEVICE_CLASS_BATTERY @property def state(self): @@ -109,11 +107,6 @@ class LinuxBatterySensor(Entity): """Return the unit the value is expressed in.""" return self._unit_of_measurement - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - @property def device_state_attributes(self): """Return the state attributes of the sensor.""" diff --git a/homeassistant/components/sensor/miflora.py b/homeassistant/components/sensor/miflora.py index 98cc7731d4d..f1f8adab062 100644 --- a/homeassistant/components/sensor/miflora.py +++ b/homeassistant/components/sensor/miflora.py @@ -38,7 +38,7 @@ DEFAULT_TIMEOUT = 10 # Sensor types are defined like: Name, units SENSOR_TYPES = { 'temperature': ['Temperature', '°C'], - 'light': ['Light intensity', 'lux'], + 'light': ['Light intensity', 'lx'], 'moisture': ['Moisture', '%'], 'conductivity': ['Conductivity', 'µS/cm'], 'battery': ['Battery', '%'], diff --git a/homeassistant/components/sensor/mysensors.py b/homeassistant/components/sensor/mysensors.py index 669ef3998de..1add4157f0e 100644 --- a/homeassistant/components/sensor/mysensors.py +++ b/homeassistant/components/sensor/mysensors.py @@ -26,7 +26,7 @@ SENSORS = { 'V_PERCENTAGE': ['%', 'mdi:percent'], 'V_LEVEL': { 'S_SOUND': ['dB', 'mdi:volume-high'], 'S_VIBRATION': ['Hz', None], - 'S_LIGHT_LEVEL': ['lux', 'white-balance-sunny']}, + 'S_LIGHT_LEVEL': ['lx', 'white-balance-sunny']}, 'V_ORP': ['mV', None], 'V_EC': ['μS/cm', None], 'V_VAR': ['var', None], diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 5ee4f738051..9ce50dc61e5 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -9,8 +9,9 @@ import logging from homeassistant.components.nest import DATA_NEST from homeassistant.helpers.entity import Entity -from homeassistant.const import (TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_MONITORED_CONDITIONS) +from homeassistant.const import ( + TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, + DEVICE_CLASS_TEMPERATURE) DEPENDENCIES = ['nest'] SENSOR_TYPES = ['humidity', @@ -143,7 +144,7 @@ class NestTempSensor(NestSensor): @property def device_class(self): """Return the device class of the sensor.""" - return 'temperature' + return DEVICE_CLASS_TEMPERATURE def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/sensor/tahoma.py b/homeassistant/components/sensor/tahoma.py index cafa942f65b..aedecfe61e5 100644 --- a/homeassistant/components/sensor/tahoma.py +++ b/homeassistant/components/sensor/tahoma.py @@ -49,7 +49,7 @@ class TahomaSensor(TahomaDevice, Entity): elif self.tahoma_device.type == 'io:SomfyContactIOSystemSensor': return None elif self.tahoma_device.type == 'io:LightIOSystemSensor': - return 'lux' + return 'lx' elif self.tahoma_device.type == 'Humidity Sensor': return '%' diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index 61a084c6266..048ca988e3d 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -7,7 +7,9 @@ https://home-assistant.io/components/sensor.tellduslive/ import logging from homeassistant.components.tellduslive import TelldusLiveEntity -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) @@ -25,18 +27,20 @@ SENSOR_TYPE_DEW_POINT = 'dewp' SENSOR_TYPE_BAROMETRIC_PRESSURE = 'barpress' SENSOR_TYPES = { - SENSOR_TYPE_TEMPERATURE: ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - SENSOR_TYPE_HUMIDITY: ['Humidity', '%', 'mdi:water'], - SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm/h', 'mdi:water'], - SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water'], - SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', ''], - SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', ''], - SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', ''], - SENSOR_TYPE_UV: ['UV', 'UV', ''], - SENSOR_TYPE_WATT: ['Power', 'W', ''], - SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', ''], - SENSOR_TYPE_DEW_POINT: ['Dew Point', TEMP_CELSIUS, 'mdi:thermometer'], - SENSOR_TYPE_BAROMETRIC_PRESSURE: ['Barometric Pressure', 'kPa', ''], + SENSOR_TYPE_TEMPERATURE: ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + SENSOR_TYPE_HUMIDITY: ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + SENSOR_TYPE_RAINRATE: ['Rain rate', 'mm/h', 'mdi:water', None], + SENSOR_TYPE_RAINTOTAL: ['Rain total', 'mm', 'mdi:water', None], + SENSOR_TYPE_WINDDIRECTION: ['Wind direction', '', '', None], + SENSOR_TYPE_WINDAVERAGE: ['Wind average', 'm/s', '', None], + SENSOR_TYPE_WINDGUST: ['Wind gust', 'm/s', '', None], + SENSOR_TYPE_UV: ['UV', 'UV', '', None], + SENSOR_TYPE_WATT: ['Power', 'W', '', None], + SENSOR_TYPE_LUMINANCE: ['Luminance', 'lx', None, DEVICE_CLASS_ILLUMINANCE], + SENSOR_TYPE_DEW_POINT: + ['Dew Point', TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + SENSOR_TYPE_BAROMETRIC_PRESSURE: ['Barometric Pressure', 'kPa', '', None], } @@ -117,3 +121,9 @@ class TelldusLiveSensor(TelldusLiveEntity): """Return the icon.""" return SENSOR_TYPES[self._type][2] \ if self._type in SENSOR_TYPES else None + + @property + def device_class(self): + """Return the device class.""" + return SENSOR_TYPES[self._type][3] \ + if self._type in SENSOR_TYPES else None diff --git a/homeassistant/components/sensor/vera.py b/homeassistant/components/sensor/vera.py index c81c208e33e..eb8ccae768e 100644 --- a/homeassistant/components/sensor/vera.py +++ b/homeassistant/components/sensor/vera.py @@ -52,7 +52,7 @@ class VeraSensor(VeraDevice, Entity): if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units elif self.vera_device.category == veraApi.CATEGORY_LIGHT_SENSOR: - return 'lux' + return 'lx' elif self.vera_device.category == veraApi.CATEGORY_UV_SENSOR: return 'level' elif self.vera_device.category == veraApi.CATEGORY_HUMIDITY_SENSOR: diff --git a/homeassistant/components/sensor/xiaomi_aqara.py b/homeassistant/components/sensor/xiaomi_aqara.py index 497a3915154..3192d0d2f60 100644 --- a/homeassistant/components/sensor/xiaomi_aqara.py +++ b/homeassistant/components/sensor/xiaomi_aqara.py @@ -3,16 +3,18 @@ import logging from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, XiaomiDevice) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS) _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - 'temperature': [TEMP_CELSIUS, 'mdi:thermometer'], - 'humidity': ['%', 'mdi:water-percent'], - 'illumination': ['lm', 'mdi:weather-sunset'], - 'lux': ['lx', 'mdi:weather-sunset'], - 'pressure': ['hPa', 'mdi:gauge'] + 'temperature': [TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE], + 'humidity': ['%', None, DEVICE_CLASS_HUMIDITY], + 'illumination': ['lm', None, DEVICE_CLASS_ILLUMINANCE], + 'lux': ['lx', None, DEVICE_CLASS_ILLUMINANCE], + 'pressure': ['hPa', 'mdi:gauge', None] } @@ -66,6 +68,12 @@ class XiaomiSensor(XiaomiDevice): except TypeError: return None + @property + def device_class(self): + """Return the device class of this entity.""" + return SENSOR_TYPES.get(self._data_key)[2] \ + if self._data_key in SENSOR_TYPES else None + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 2e96e2f29c0..52a50ba9607 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -166,6 +166,12 @@ EVENT_SERVICE_REMOVED = 'service_removed' EVENT_LOGBOOK_ENTRY = 'logbook_entry' EVENT_THEMES_UPDATED = 'themes_updated' +# #### DEVICE CLASSES #### +DEVICE_CLASS_BATTERY = 'battery' +DEVICE_CLASS_HUMIDITY = 'humidity' +DEVICE_CLASS_ILLUMINANCE = 'illuminance' +DEVICE_CLASS_TEMPERATURE = 'temperature' + # #### STATES #### STATE_ON = 'on' STATE_OFF = 'off' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 71f9c8e6656..cff52b2ff20 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -99,10 +99,10 @@ class TestGetAccessories(unittest.TestCase): get_accessory(None, state, 2, {}) def test_light_sensor(self): - """Test light sensor with device class lux.""" + """Test light sensor with device class illuminance.""" with patch.dict(TYPES, {'LightSensor': self.mock_type}): state = State('sensor.light', '900', - {ATTR_DEVICE_CLASS: 'light'}) + {ATTR_DEVICE_CLASS: 'illuminance'}) get_accessory(None, state, 2, {}) def test_light_sensor_unit_lm(self): @@ -112,13 +112,6 @@ class TestGetAccessories(unittest.TestCase): {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) get_accessory(None, state, 2, {}) - def test_light_sensor_unit_lux(self): - """Test light sensor with lux as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lux'}) - get_accessory(None, state, 2, {}) - def test_light_sensor_unit_lx(self): """Test light sensor with lx as unit.""" with patch.dict(TYPES, {'LightSensor': self.mock_type}): From af8cd63838d035ad80420f4296cdbc312fe407ca Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sat, 5 May 2018 16:00:36 +0200 Subject: [PATCH 610/924] Matrix Chatbot (#13355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add first version of the Matrix bot * It's a stupid but necessary change… * Dont list it twice * All hail the linter! * More linter-pleasing * Use the correct user ID * Add expression commands * Add tests for new validators * Fix room alias handling * Wording * Defer setup * Simplify commands * Handle exceptions * Update requirements * Review * Move login back to constructor * Fix review comments --- .coveragerc | 4 +- CODEOWNERS | 2 + homeassistant/components/matrix.py | 351 +++++++++++++++++++++ homeassistant/components/notify/matrix.py | 169 +--------- homeassistant/helpers/config_validation.py | 30 ++ requirements_all.txt | 4 +- tests/helpers/test_config_validation.py | 28 ++ 7 files changed, 433 insertions(+), 155 deletions(-) create mode 100644 homeassistant/components/matrix.py diff --git a/.coveragerc b/.coveragerc index cf7a5a2cd9c..d2192ca2e46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -166,6 +166,9 @@ omit = homeassistant/components/mailgun.py homeassistant/components/*/mailgun.py + homeassistant/components/matrix.py + homeassistant/components/*/matrix.py + homeassistant/components/maxcube.py homeassistant/components/*/maxcube.py @@ -519,7 +522,6 @@ omit = homeassistant/components/notify/lannouncer.py homeassistant/components/notify/llamalab_automate.py homeassistant/components/notify/mastodon.py - homeassistant/components/notify/matrix.py homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py homeassistant/components/notify/nfandroidtv.py diff --git a/CODEOWNERS b/CODEOWNERS index a62ed67db66..33966d1badb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/matrix.py @tinloaf +homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza homeassistant/components/*/rfxtrx.py @danielhiversen diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py new file mode 100644 index 00000000000..569b012b484 --- /dev/null +++ b/homeassistant/components/matrix.py @@ -0,0 +1,351 @@ +""" +The matrix bot component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/matrix/ +""" +import logging +import os +from functools import partial + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import (ATTR_TARGET, ATTR_MESSAGE) +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD, + CONF_VERIFY_SSL, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START) +from homeassistant.util.json import load_json, save_json +from homeassistant.exceptions import HomeAssistantError + +REQUIREMENTS = ['matrix-client==0.2.0'] + +_LOGGER = logging.getLogger(__name__) + +SESSION_FILE = '.matrix.conf' + +CONF_HOMESERVER = 'homeserver' +CONF_ROOMS = 'rooms' +CONF_COMMANDS = 'commands' +CONF_WORD = 'word' +CONF_EXPRESSION = 'expression' + +EVENT_MATRIX_COMMAND = 'matrix_command' + +DOMAIN = 'matrix' + +COMMAND_SCHEMA = vol.All( + # Basic Schema + vol.Schema({ + vol.Exclusive(CONF_WORD, 'trigger'): cv.string, + vol.Exclusive(CONF_EXPRESSION, 'trigger'): cv.is_regex, + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + }), + # Make sure it's either a word or an expression command + cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION) +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOMESERVER): cv.url, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Required(CONF_USERNAME): cv.matches_regex("@[^:]*:.*"), + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, + [cv.string]), + vol.Optional(CONF_COMMANDS, default=[]): [COMMAND_SCHEMA] + }) +}, extra=vol.ALLOW_EXTRA) + +SERVICE_SEND_MESSAGE = 'send_message' + +SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema({ + vol.Required(ATTR_MESSAGE): cv.string, + vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), +}) + + +def setup(hass, config): + """Set up the Matrix bot component.""" + from matrix_client.client import MatrixRequestError + + config = config[DOMAIN] + + try: + bot = MatrixBot( + hass, + os.path.join(hass.config.path(), SESSION_FILE), + config[CONF_HOMESERVER], + config[CONF_VERIFY_SSL], + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_ROOMS], + config[CONF_COMMANDS]) + hass.data[DOMAIN] = bot + except MatrixRequestError as exception: + _LOGGER.error("Matrix failed to log in: %s", str(exception)) + return False + + hass.services.register( + DOMAIN, SERVICE_SEND_MESSAGE, bot.handle_send_message, + schema=SERVICE_SCHEMA_SEND_MESSAGE) + + return True + + +class MatrixBot(object): + """The Matrix Bot.""" + + def __init__(self, hass, config_file, homeserver, verify_ssl, + username, password, listening_rooms, commands): + """Set up the client.""" + self.hass = hass + + self._session_filepath = config_file + self._auth_tokens = self._get_auth_tokens() + + self._homeserver = homeserver + self._verify_tls = verify_ssl + self._mx_id = username + self._password = password + + self._listening_rooms = listening_rooms + + # Logging in is deferred b/c it does I/O + self._setup_done = False + + # We have to fetch the aliases for every room to make sure we don't + # join it twice by accident. However, fetching aliases is costly, + # so we only do it once per room. + self._aliases_fetched_for = set() + + # word commands are stored dict-of-dict: First dict indexes by room ID + # / alias, second dict indexes by the word + self._word_commands = {} + + # regular expression commands are stored as a list of commands per + # room, i.e., a dict-of-list + self._expression_commands = {} + + for command in commands: + if not command.get(CONF_ROOMS): + command[CONF_ROOMS] = listening_rooms + + if command.get(CONF_WORD): + for room_id in command[CONF_ROOMS]: + if room_id not in self._word_commands: + self._word_commands[room_id] = {} + self._word_commands[room_id][command[CONF_WORD]] = command + else: + for room_id in command[CONF_ROOMS]: + if room_id not in self._expression_commands: + self._expression_commands[room_id] = [] + self._expression_commands[room_id].append(command) + + # Log in. This raises a MatrixRequestError if login is unsuccessful + self._client = self._login() + + def handle_matrix_exception(exception): + """Handle exceptions raised inside the Matrix SDK.""" + _LOGGER.error("Matrix exception:\n %s", str(exception)) + + self._client.start_listener_thread( + exception_handler=handle_matrix_exception) + + def stop_client(_): + """Run once when Home Assistant stops.""" + self._client.stop_listener_thread() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_client) + + # Joining rooms potentially does a lot of I/O, so we defer it + def handle_startup(_): + """Run once when Home Assistant finished startup.""" + self._join_rooms() + + self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) + + def _handle_room_message(self, room_id, room, event): + """Handle a message sent to a room.""" + if event['content']['msgtype'] != 'm.text': + return + + if event['sender'] == self._mx_id: + return + + _LOGGER.debug("Handling message: %s", event['content']['body']) + + if event['content']['body'][0] == "!": + # Could trigger a single-word command. + pieces = event['content']['body'].split(' ') + cmd = pieces[0][1:] + + command = self._word_commands.get(room_id, {}).get(cmd) + if command: + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': pieces[1:] + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + # After single-word commands, check all regex commands in the room + for command in self._expression_commands.get(room_id, []): + match = command[CONF_EXPRESSION].match(event['content']['body']) + if not match: + continue + event_data = { + 'command': command[CONF_NAME], + 'sender': event['sender'], + 'room': room_id, + 'args': match.groupdict() + } + self.hass.bus.fire(EVENT_MATRIX_COMMAND, event_data) + + def _join_or_get_room(self, room_id_or_alias): + """Join a room or get it, if we are already in the room. + + We can't just always call join_room(), since that seems to crash + the client if we're already in the room. + """ + rooms = self._client.get_rooms() + if room_id_or_alias in rooms: + _LOGGER.debug("Already in room %s", room_id_or_alias) + return rooms[room_id_or_alias] + + for room in rooms.values(): + if room.room_id not in self._aliases_fetched_for: + room.update_aliases() + self._aliases_fetched_for.add(room.room_id) + + if room_id_or_alias in room.aliases: + _LOGGER.debug("Already in room %s (known as %s)", + room.room_id, room_id_or_alias) + return room + + room = self._client.join_room(room_id_or_alias) + _LOGGER.info("Joined room %s (known as %s)", room.room_id, + room_id_or_alias) + return room + + def _join_rooms(self): + """Join the rooms that we listen for commands in.""" + from matrix_client.client import MatrixRequestError + + for room_id in self._listening_rooms: + try: + room = self._join_or_get_room(room_id) + room.add_listener(partial(self._handle_room_message, room_id), + "m.room.message") + + except MatrixRequestError as ex: + _LOGGER.error("Could not join room %s: %s", room_id, ex) + + def _get_auth_tokens(self): + """ + Read sorted authentication tokens from disk. + + Returns the auth_tokens dictionary. + """ + try: + auth_tokens = load_json(self._session_filepath) + + return auth_tokens + except HomeAssistantError as ex: + _LOGGER.warning( + "Loading authentication tokens from file '%s' failed: %s", + self._session_filepath, str(ex)) + return {} + + def _store_auth_token(self, token): + """Store authentication token to session and persistent storage.""" + self._auth_tokens[self._mx_id] = token + + save_json(self._session_filepath, self._auth_tokens) + + def _login(self): + """Login to the matrix homeserver and return the client instance.""" + from matrix_client.client import MatrixRequestError + + # Attempt to generate a valid client using either of the two possible + # login methods: + client = None + + # If we have an authentication token + if self._mx_id in self._auth_tokens: + try: + client = self._login_by_token() + _LOGGER.debug("Logged in using stored token.") + + except MatrixRequestError as ex: + _LOGGER.warning( + "Login by token failed, falling back to password. " + "login_by_token raised: (%d) %s", + ex.code, ex.content) + + # If we still don't have a client try password. + if not client: + try: + client = self._login_by_password() + _LOGGER.debug("Logged in using password.") + + except MatrixRequestError as ex: + _LOGGER.error( + "Login failed, both token and username/password invalid " + "login_by_password raised: (%d) %s", + ex.code, ex.content) + + # re-raise the error so _setup can catch it. + raise + + return client + + def _login_by_token(self): + """Login using authentication token and return the client.""" + from matrix_client.client import MatrixClient + + return MatrixClient( + base_url=self._homeserver, + token=self._auth_tokens[self._mx_id], + user_id=self._mx_id, + valid_cert_check=self._verify_tls) + + def _login_by_password(self): + """Login using password authentication and return the client.""" + from matrix_client.client import MatrixClient + + _client = MatrixClient( + base_url=self._homeserver, + valid_cert_check=self._verify_tls) + + _client.login_with_password(self._mx_id, self._password) + + self._store_auth_token(_client.token) + + return _client + + def _send_message(self, message, target_rooms): + """Send the message to the matrix server.""" + from matrix_client.client import MatrixRequestError + + for target_room in target_rooms: + try: + room = self._join_or_get_room(target_room) + _LOGGER.debug(room.send_text(message)) + except MatrixRequestError as ex: + _LOGGER.error( + "Unable to deliver message to room '%s': (%d): %s", + target_room, ex.code, ex.content) + + def handle_send_message(self, service): + """Handle the send_message service.""" + if not self._setup_done: + _LOGGER.warning("Could not send message: setup is not done!") + return + + self._send_message(service.data[ATTR_MESSAGE], + service.data[ATTR_TARGET]) diff --git a/homeassistant/components/notify/matrix.py b/homeassistant/components/notify/matrix.py index 03bc53e204c..fc29ad91dc9 100644 --- a/homeassistant/components/notify/matrix.py +++ b/homeassistant/components/notify/matrix.py @@ -5,181 +5,46 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/notify.matrix/ """ import logging -import os -from urllib.parse import urlparse import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import (ATTR_TARGET, PLATFORM_SCHEMA, - BaseNotificationService) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_VERIFY_SSL -from homeassistant.util.json import load_json, save_json - -REQUIREMENTS = ['matrix-client==0.0.6'] + BaseNotificationService, + ATTR_MESSAGE) _LOGGER = logging.getLogger(__name__) -SESSION_FILE = 'matrix.conf' - -CONF_HOMESERVER = 'homeserver' CONF_DEFAULT_ROOM = 'default_room' +DOMAIN = 'matrix' +DEPENDENCIES = [DOMAIN] + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOMESERVER): cv.url, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_DEFAULT_ROOM): cv.string, }) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - from matrix_client.client import MatrixRequestError - - try: - return MatrixNotificationService( - os.path.join(hass.config.path(), SESSION_FILE), - config.get(CONF_HOMESERVER), - config.get(CONF_DEFAULT_ROOM), - config.get(CONF_VERIFY_SSL), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD)) - - except MatrixRequestError: - return None + return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) class MatrixNotificationService(BaseNotificationService): """Send Notifications to a Matrix Room.""" - def __init__(self, config_file, homeserver, default_room, verify_ssl, - username, password): - """Set up the client.""" - self.session_filepath = config_file - self.auth_tokens = self.get_auth_tokens() + def __init__(self, default_room): + """Set up the notification service.""" + self._default_room = default_room - self.homeserver = homeserver - self.default_room = default_room - self.verify_tls = verify_ssl - self.username = username - self.password = password - - self.mx_id = "{user}@{homeserver}".format( - user=username, homeserver=urlparse(homeserver).netloc) - - # Login, this will raise a MatrixRequestError if login is unsuccessful - self.client = self.login() - - def get_auth_tokens(self): - """ - Read sorted authentication tokens from disk. - - Returns the auth_tokens dictionary. - """ - if not os.path.exists(self.session_filepath): - return {} - - try: - data = load_json(self.session_filepath) - - auth_tokens = {} - for mx_id, token in data.items(): - auth_tokens[mx_id] = token - - return auth_tokens - - except (OSError, IOError, PermissionError) as ex: - _LOGGER.warning( - "Loading authentication tokens from file '%s' failed: %s", - self.session_filepath, str(ex)) - return {} - - def store_auth_token(self, token): - """Store authentication token to session and persistent storage.""" - self.auth_tokens[self.mx_id] = token - - save_json(self.session_filepath, self.auth_tokens) - - def login(self): - """Login to the matrix homeserver and return the client instance.""" - from matrix_client.client import MatrixRequestError - - # Attempt to generate a valid client using either of the two possible - # login methods: - client = None - - # If we have an authentication token - if self.mx_id in self.auth_tokens: - try: - client = self.login_by_token() - _LOGGER.debug("Logged in using stored token.") - - except MatrixRequestError as ex: - _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", - ex.code, ex.content) - - # If we still don't have a client try password. - if not client: - try: - client = self.login_by_password() - _LOGGER.debug("Logged in using password.") - - except MatrixRequestError as ex: - _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", - ex.code, ex.content) - - # re-raise the error so the constructor can catch it. - raise - - return client - - def login_by_token(self): - """Login using authentication token and return the client.""" - from matrix_client.client import MatrixClient - - return MatrixClient( - base_url=self.homeserver, - token=self.auth_tokens[self.mx_id], - user_id=self.username, - valid_cert_check=self.verify_tls) - - def login_by_password(self): - """Login using password authentication and return the client.""" - from matrix_client.client import MatrixClient - - _client = MatrixClient( - base_url=self.homeserver, - valid_cert_check=self.verify_tls) - - _client.login_with_password(self.username, self.password) - - self.store_auth_token(_client.token) - - return _client - - def send_message(self, message, **kwargs): + def send_message(self, message="", **kwargs): """Send the message to the matrix server.""" - from matrix_client.client import MatrixRequestError + target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] - target_rooms = kwargs.get(ATTR_TARGET) or [self.default_room] + service_data = { + ATTR_TARGET: target_rooms, + ATTR_MESSAGE: message + } - rooms = self.client.get_rooms() - for target_room in target_rooms: - try: - if target_room in rooms: - room = rooms[target_room] - else: - room = self.client.join_room(target_room) - - _LOGGER.debug(room.send_text(message)) - - except MatrixRequestError as ex: - _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", - target_room, ex.code, ex.content) + return self.hass.services.call( + DOMAIN, 'send_message', service_data=service_data) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8177999cc94..0bd490940a9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -96,6 +96,36 @@ def isdevice(value): raise vol.Invalid('No device at {} found'.format(value)) +def matches_regex(regex): + """Validate that the value is a string that matches a regex.""" + regex = re.compile(regex) + + def validator(value: Any) -> str: + """Validate that value matches the given regex.""" + if not isinstance(value, str): + raise vol.Invalid('not a string value: {}'.format(value)) + + if not regex.match(value): + raise vol.Invalid('value {} does not match regular expression {}' + .format(regex.pattern, value)) + + return value + return validator + + +def is_regex(value): + """Validate that a string is a valid regular expression.""" + try: + r = re.compile(value) + return r + except TypeError: + raise vol.Invalid("value {} is of the wrong type for a regular " + "expression".format(value)) + except re.error: + raise vol.Invalid("value {} is not a valid regular expression".format( + value)) + + def isfile(value: Any) -> str: """Validate that the value is an existing file.""" if value is None: diff --git a/requirements_all.txt b/requirements_all.txt index 88dd3d60904..74f9ff8e195 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -511,8 +511,8 @@ luftdaten==0.1.3 # homeassistant.components.sensor.lyft lyft_rides==0.2 -# homeassistant.components.notify.matrix -matrix-client==0.0.6 +# homeassistant.components.matrix +matrix-client==0.2.0 # homeassistant.components.maxcube maxcube-api==0.1.0 diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index aff0acf9e3a..28efcb3e868 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -565,3 +565,31 @@ def test_socket_timeout(): # pylint: disable=invalid-name assert _GLOBAL_DEFAULT_TIMEOUT == schema(None) assert schema(1) == 1.0 + + +def test_matches_regex(): + """Test matches_regex validator.""" + schema = vol.Schema(cv.matches_regex('.*uiae.*')) + + with pytest.raises(vol.Invalid): + schema(1.0) + + with pytest.raises(vol.Invalid): + schema(" nrtd ") + + test_str = "This is a test including uiae." + assert(schema(test_str) == test_str) + + +def test_is_regex(): + """Test the is_regex validator.""" + schema = vol.Schema(cv.is_regex) + + with pytest.raises(vol.Invalid): + schema("(") + + with pytest.raises(vol.Invalid): + schema({"a dict": "is not a regex"}) + + valid_re = ".*" + schema(valid_re) From 8410b63d9c3191bb48df7b6fc9116453637c4138 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 May 2018 16:11:00 +0200 Subject: [PATCH 611/924] deCONZ add new device without restart (#14221) * Add new device without restarting hass * Remove debug prints * Fix copy paste error * Fix comments from balloob Add tests to verify signalling with new added devices * Fix hound comments Add test to verify when new sensor is added * Fix tests * Unload entry should unsubscribe all deconz dispatchers * Make sure mock setup also creates unsub in hass data * Fix copy paste issue * Lint --- .../components/binary_sensor/deconz.py | 22 ++++++--- homeassistant/components/deconz/__init__.py | 37 +++++++++++--- homeassistant/components/deconz/const.py | 1 + homeassistant/components/light/deconz.py | 37 +++++++++----- homeassistant/components/sensor/deconz.py | 30 +++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/binary_sensor/test_deconz.py | 28 ++++++++++- tests/components/deconz/test_init.py | 49 +++++++++++++++++++ tests/components/light/test_deconz.py | 30 +++++++++++- tests/components/sensor/test_deconz.py | 22 +++++++-- 11 files changed, 212 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index a9a3e28f4be..9faa703d13c 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,9 +6,10 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -21,14 +22,19 @@ async def async_setup_platform(hass, config, async_add_devices, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the deCONZ binary sensor.""" - from pydeconz.sensor import DECONZ_BINARY_SENSOR - sensors = hass.data[DATA_DECONZ].sensors - entities = [] + @callback + def async_add_sensor(sensors): + """Add binary sensor from deCONZ.""" + from pydeconz.sensor import DECONZ_BINARY_SENSOR + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_BINARY_SENSOR: + entities.append(DeconzBinarySensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_BINARY_SENSOR: - entities.append(DeconzBinarySensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 75414598693..47573be6add 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -11,15 +11,18 @@ from homeassistant.const import ( CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) from homeassistant.util import slugify from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts from .const import ( - CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DOMAIN, _LOGGER) + CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==36'] +REQUIREMENTS = ['pydeconz==37'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -69,14 +72,20 @@ async def async_setup_entry(hass, config_entry): Start websocket for push notification of state changes from deCONZ. """ from pydeconz import DeconzSession - from pydeconz.sensor import SWITCH as DECONZ_REMOTE if DOMAIN in hass.data: _LOGGER.error( "Config entry failed since one deCONZ instance already exists") return False + @callback + def async_add_device_callback(device_type, device): + """Called when a new device has been created in deCONZ.""" + async_dispatcher_send( + hass, 'deconz_new_{}'.format(device_type), [device]) + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **config_entry.data) + deconz = DeconzSession(hass.loop, session, **config_entry.data, + async_add_device=async_add_device_callback) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -84,14 +93,24 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = deconz hass.data[DATA_DECONZ_ID] = {} + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_UNSUB] = [] for component in ['binary_sensor', 'light', 'scene', 'sensor']: hass.async_add_job(hass.config_entries.async_forward_entry_setup( config_entry, component)) - hass.data[DATA_DECONZ_EVENT] = [DeconzEvent( - hass, sensor) for sensor in deconz.sensors.values() - if sensor.type in DECONZ_REMOTE] + @callback + def async_add_remote(sensors): + """Setup remote from deCONZ.""" + from pydeconz.sensor import SWITCH as DECONZ_REMOTE + for sensor in sensors: + if sensor.type in DECONZ_REMOTE: + hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) + + async_add_remote(deconz.sensors.values()) deconz.start() @@ -148,6 +167,10 @@ async def async_unload_entry(hass, config_entry): for component in ['binary_sensor', 'light', 'scene', 'sensor']: await hass.config_entries.async_forward_entry_unload( config_entry, component) + dispatchers = hass.data[DATA_DECONZ_UNSUB] + for unsub_dispatcher in dispatchers: + unsub_dispatcher() + hass.data[DATA_DECONZ_UNSUB] = [] hass.data[DATA_DECONZ_EVENT] = [] hass.data[DATA_DECONZ_ID] = [] return True diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e6d393c8ee7..48e5ea75d68 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -7,3 +7,4 @@ DOMAIN = 'deconz' CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' +DATA_DECONZ_UNSUB = 'deconz_dispatchers' diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 36ad572a263..916e60c00b1 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] @@ -19,23 +20,35 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Old way of setting up deCONZ lights.""" + """Old way of setting up deCONZ lights and group.""" pass async def async_setup_entry(hass, config_entry, async_add_devices): - """Set up the deCONZ lights from a config entry.""" - lights = hass.data[DATA_DECONZ].lights - groups = hass.data[DATA_DECONZ].groups - entities = [] + """Set up the deCONZ lights and groups from a config entry.""" + @callback + def async_add_light(lights): + """Add light from deCONZ.""" + entities = [] + for light in lights: + entities.append(DeconzLight(light)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) - for light in lights.values(): - entities.append(DeconzLight(light)) + @callback + def async_add_group(groups): + """Add group from deCONZ.""" + entities = [] + for group in groups: + if group.lights: + entities.append(DeconzLight(group)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) - for group in groups.values(): - if group.lights: # Don't create entity for group not containing light - entities.append(DeconzLight(group)) - async_add_devices(entities, True) + async_add_light(hass.data[DATA_DECONZ].lights.values()) + async_add_group(hass.data[DATA_DECONZ].groups.values()) class DeconzLight(Light): diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index b4a3cb8c6c5..221cdf2129e 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,10 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -27,18 +28,23 @@ async def async_setup_platform(hass, config, async_add_devices, async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the deCONZ sensors.""" - from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE - sensors = hass.data[DATA_DECONZ].sensors - entities = [] + @callback + def async_add_sensor(sensors): + """Add sensors from deCONZ.""" + from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_REMOTE: + if sensor.battery: + entities.append(DeconzBattery(sensor)) + else: + entities.append(DeconzSensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_SENSOR: - if sensor.type in DECONZ_REMOTE: - if sensor.battery: - entities.append(DeconzBattery(sensor)) - else: - entities.append(DeconzSensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzSensor(Entity): diff --git a/requirements_all.txt b/requirements_all.txt index 74f9ff8e195..406b460d0a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7816d9c6f24..df0f5722b86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 84ed059e97e..88dd0dae737 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -14,6 +15,13 @@ SENSOR = { "type": "ZHAPresence", "state": {"presence": False}, "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} } } @@ -30,6 +38,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') @@ -40,7 +49,7 @@ async def setup_bridge(hass, data): async def test_no_binary_sensors(hass): - """Test the update_lights function with some lights.""" + """Test that no sensors in deconz results in no sensor entities.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 @@ -48,8 +57,23 @@ async def test_no_binary_sensors(hass): async def test_binary_sensors(hass): - """Test the update_lights function with some lights.""" + """Test successful creation of binary sensor entities.""" data = {"sensors": SENSOR} await setup_bridge(hass, data) assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "binary_sensor.sensor_2_name" not in \ + hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 1 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHAPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index b09edf42a87..888094deea6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,6 +1,7 @@ """Test deCONZ component setup process.""" from unittest.mock import Mock, patch +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component from homeassistant.components import deconz @@ -97,6 +98,7 @@ async def test_setup_entry_successful(hass): assert await deconz.async_setup_entry(hass, entry) is True assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DATA_DECONZ_ID] == {} + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1 assert len(mock_add_job.mock_calls) == 4 assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ @@ -121,5 +123,52 @@ async def test_unload_entry(hass): hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} assert await deconz.async_unload_entry(hass, entry) assert deconz.DOMAIN not in hass.data + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 0 assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + + +async def test_add_new_device(hass): + """Test adding a new device generates a signal for platforms.""" + new_event = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": { + "config": { + "on": "True", + "reachable": "True" + }, + "name": "event", + "state": {}, + "type": "ZHASwitch" + } + } + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(deconz, 'async_dispatcher_send') as mock_dispatch_send, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + hass.data[deconz.DOMAIN].async_event_handler(new_event) + await hass.async_block_till_done() + assert len(mock_dispatch_send.mock_calls) == 1 + assert len(mock_dispatch_send.mock_calls[0]) == 3 + + +async def test_add_new_remote(hass): + """Test new added device creates a new remote.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + remote = Mock() + remote.name = 'name' + remote.type = 'ZHASwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index d907697354e..2608d77ce2a 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -49,6 +50,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') @@ -58,7 +60,7 @@ async def setup_bridge(hass, data): async def test_no_lights_or_groups(hass): - """Test the update_lights function with some lights.""" + """Test that no lights or groups entities are created.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 @@ -66,9 +68,33 @@ async def test_no_lights_or_groups(hass): async def test_lights_and_groups(hass): - """Test the update_lights function with some lights.""" + """Test that lights or groups entities are created.""" await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 3 + + +async def test_add_new_light(hass): + """Test successful creation of light entities.""" + data = {} + await setup_bridge(hass, data) + light = Mock() + light.name = 'name' + light.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_light', [light]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_add_new_group(hass): + """Test successful creation of group entities.""" + data = {} + await setup_bridge(hass, data) + group = Mock() + group.name = 'name' + group.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_group', [group]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index d6c026e88bd..8f6a53e6e65 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -1,8 +1,10 @@ """deCONZ sensor platform tests.""" from unittest.mock import Mock, patch + from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -51,6 +53,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( @@ -61,15 +64,15 @@ async def setup_bridge(hass, data): async def test_no_sensors(hass): - """Test the update_lights function with some lights.""" + """Test that no sensors in deconz results in no sensor entities.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 assert len(hass.states.async_all()) == 0 -async def test_binary_sensors(hass): - """Test the update_lights function with some lights.""" +async def test_sensors(hass): + """Test successful creation of sensor entities.""" data = {"sensors": SENSOR} await setup_bridge(hass, data) assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] @@ -81,3 +84,16 @@ async def test_binary_sensors(hass): assert "sensor.sensor_4_name_battery_level" in \ hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 2 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHATemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] From 1a936220e9a8741b707b10cd197f16d915b748b9 Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Sat, 5 May 2018 09:21:58 -0500 Subject: [PATCH 612/924] Add alarmdotcom sensor status (#14254) * bump to match Xorso/pyalarmdotcom#9 * Load additional status attributes * missed a spot --- .../components/alarm_control_panel/alarmdotcom.py | 9 ++++++++- requirements_all.txt | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 0e96e6448ff..31d93373286 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -17,7 +17,7 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyalarmdotcom==0.3.1'] +REQUIREMENTS = ['pyalarmdotcom==0.3.2'] _LOGGER = logging.getLogger(__name__) @@ -93,6 +93,13 @@ class AlarmDotCom(alarm.AlarmControlPanel): return STATE_ALARM_ARMED_AWAY return STATE_UNKNOWN + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'sensor_status': self._alarm.sensor_status + } + @asyncio.coroutine def async_alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/requirements_all.txt b/requirements_all.txt index 406b460d0a4..d24c2e07043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ pyads==2.2.6 pyairvisual==1.0.0 # homeassistant.components.alarm_control_panel.alarmdotcom -pyalarmdotcom==0.3.1 +pyalarmdotcom==0.3.2 # homeassistant.components.arlo pyarlo==0.1.2 From f6e29a66471f009e9c3f142970c1b113ed4f4e9e Mon Sep 17 00:00:00 2001 From: Jesse Newland Date: Sat, 5 May 2018 09:23:01 -0500 Subject: [PATCH 613/924] Add domain to labels and count state changes to Prometheus (#14253) * Add domain to labels * Count state changes --- homeassistant/components/prometheus.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus.py b/homeassistant/components/prometheus.py index dc1cbd945a7..96ed098567d 100644 --- a/homeassistant/components/prometheus.py +++ b/homeassistant/components/prometheus.py @@ -86,9 +86,16 @@ class Metrics(object): if hasattr(self, handler): getattr(self, handler)(state) + metric = self._metric( + 'state_change', + self.prometheus_client.Counter, + 'The number of state changes', + ) + metric.labels(**self._labels(state)).inc() + def _metric(self, metric, factory, documentation, labels=None): if labels is None: - labels = ['entity', 'friendly_name'] + labels = ['entity', 'friendly_name', 'domain'] try: return self._metrics[metric] @@ -100,6 +107,7 @@ class Metrics(object): def _labels(state): return { 'entity': state.entity_id, + 'domain': state.domain, 'friendly_name': state.attributes.get('friendly_name'), } From 4d085882d5d5752548c1e977da43c3e00848089d Mon Sep 17 00:00:00 2001 From: Jason Kingsbury Date: Sat, 5 May 2018 15:30:54 +0100 Subject: [PATCH 614/924] Add support for max_volume (#13822) * onkyo: add support for max volume range * onkyo: make flake8 happy * onkyo: fix PEP8 D205 on line 181 * onkyo: use range for max_volume configuration * onkyo: fix line too long --- .../components/media_player/onkyo.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 58703165385..245ab8bb54c 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -22,9 +22,11 @@ REQUIREMENTS = ['onkyo-eiscp==1.2.4'] _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' +CONF_MAX_VOLUME = 'max_volume' CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' +SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY @@ -39,6 +41,8 @@ DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): + vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, vol.Optional(CONF_ZONE2, default=False): cv.boolean, @@ -57,7 +61,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: hosts.append(OnkyoDevice( eiscp.eISCP(host), config.get(CONF_SOURCES), - name=config.get(CONF_NAME))) + name=config.get(CONF_NAME), + max_volume=config.get(CONF_MAX_VOLUME), + )) KNOWN_HOSTS.append(host) # Add Zone2 if configured @@ -80,7 +86,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - def __init__(self, receiver, sources, name=None): + def __init__(self, receiver, sources, name=None, + max_volume=SUPPORTED_MAX_VOLUME): """Initialize the Onkyo Receiver.""" self._receiver = receiver self._muted = False @@ -88,6 +95,7 @@ class OnkyoDevice(MediaPlayerDevice): self._pwstate = STATE_OFF self._name = name or '{}_{}'.format( receiver.info['model_name'], receiver.info['identifier']) + self._max_volume = max_volume self._current_source = None self._source_list = list(sources.values()) self._source_mapping = sources @@ -141,7 +149,7 @@ class OnkyoDevice(MediaPlayerDevice): self._current_source = '_'.join( [i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == 'on') - self._volume = volume_raw[1] / 80.0 + self._volume = volume_raw[1] / self._max_volume @property def name(self): @@ -183,8 +191,13 @@ class OnkyoDevice(MediaPlayerDevice): self.command('system-power standby') def set_volume_level(self, volume): - """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command('volume {}'.format(int(volume*80))) + """ + Set volume level, input is range 0..1. + + Onkyo ranges from 1-80 however 80 is usually far too loud + so allow the user to specify the upper range with CONF_MAX_VOLUME + """ + self.command('volume {}'.format(int(volume * self._max_volume))) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" From b9e893184ad2db3b36872de40f28d13843fb0086 Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 5 May 2018 15:57:53 +0100 Subject: [PATCH 615/924] Refactor ImageProcessingFaceEntity (#14296) * Refactor ImageProcessingFaceEntity * Replace STATE_UNKNOWN with None --- .../components/image_processing/__init__.py | 98 ++++++++++++++++ .../components/image_processing/demo.py | 7 +- .../image_processing/dlib_face_detect.py | 4 +- .../image_processing/dlib_face_identify.py | 5 +- .../image_processing/microsoft_face_detect.py | 5 +- .../microsoft_face_identify.py | 105 +----------------- 6 files changed, 110 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index f0cb3a66d52..c6100ff701d 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -10,6 +10,7 @@ import logging import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) @@ -17,6 +18,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,16 @@ DEVICE_CLASSES = [ SERVICE_SCAN = 'scan' +EVENT_DETECT_FACE = 'image_processing.detect_face' + +ATTR_AGE = 'age' ATTR_CONFIDENCE = 'confidence' +ATTR_FACES = 'faces' +ATTR_GENDER = 'gender' +ATTR_GLASSES = 'glasses' +ATTR_NAME = 'name' +ATTR_MOTION = 'motion' +ATTR_TOTAL_FACES = 'total_faces' CONF_SOURCE = 'source' CONF_CONFIDENCE = 'confidence' @@ -133,3 +144,90 @@ class ImageProcessingEntity(Entity): # process image data yield from self.async_process_image(image.content) + + +class ImageProcessingFaceEntity(ImageProcessingEntity): + """Base entity class for face image processing.""" + + def __init__(self): + """Initialize base face identify/verify entity.""" + self.faces = [] + self.total_faces = 0 + + @property + def state(self): + """Return the state of the entity.""" + confidence = 0 + state = None + + # No confidence support + if not self.confidence: + return self.total_faces + + # Search high confidence + for face in self.faces: + if ATTR_CONFIDENCE not in face: + continue + + f_co = face[ATTR_CONFIDENCE] + if f_co > confidence: + confidence = f_co + for attr in [ATTR_NAME, ATTR_MOTION]: + if attr in face: + state = face[attr] + break + + return state + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'face' + + @property + def state_attributes(self): + """Return device specific state attributes.""" + attr = { + ATTR_FACES: self.faces, + ATTR_TOTAL_FACES: self.total_faces, + } + + return attr + + def process_faces(self, faces, total): + """Send event with detected faces and store data.""" + run_callback_threadsafe( + self.hass.loop, self.async_process_faces, faces, total).result() + + @callback + def async_process_faces(self, faces, total): + """Send event with detected faces and store data. + + known are a dict in follow format: + [ + { + ATTR_CONFIDENCE: 80, + ATTR_NAME: 'Name', + ATTR_AGE: 12.0, + ATTR_GENDER: 'man', + ATTR_MOTION: 'smile', + ATTR_GLASSES: 'sunglasses' + }, + ] + + This method must be run in the event loop. + """ + # Send events + for face in faces: + if ATTR_CONFIDENCE in face and self.confidence: + if face[ATTR_CONFIDENCE] < self.confidence: + continue + + face.update({ATTR_ENTITY_ID: self.entity_id}) + self.hass.async_add_job( + self.hass.bus.async_fire, EVENT_DETECT_FACE, face + ) + + # Update entity store + self.faces = faces + self.total_faces = total diff --git a/homeassistant/components/image_processing/demo.py b/homeassistant/components/image_processing/demo.py index 788d12520f5..e225113b5b1 100644 --- a/homeassistant/components/image_processing/demo.py +++ b/homeassistant/components/image_processing/demo.py @@ -4,11 +4,12 @@ Support for the demo image processing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/demo/ """ -from homeassistant.components.image_processing import ATTR_CONFIDENCE +from homeassistant.components.image_processing import ( + ImageProcessingFaceEntity, ATTR_CONFIDENCE, ATTR_NAME, ATTR_AGE, + ATTR_GENDER + ) from homeassistant.components.image_processing.openalpr_local import ( ImageProcessingAlprEntity) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity, ATTR_NAME, ATTR_AGE, ATTR_GENDER) def setup_platform(hass, config, add_devices, discovery_info=None): diff --git a/homeassistant/components/image_processing/dlib_face_detect.py b/homeassistant/components/image_processing/dlib_face_detect.py index 65705feb7f7..d4a20da253c 100644 --- a/homeassistant/components/image_processing/dlib_face_detect.py +++ b/homeassistant/components/image_processing/dlib_face_detect.py @@ -11,9 +11,7 @@ from homeassistant.core import split_entity_id # pylint: disable=unused-import from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa from homeassistant.components.image_processing import ( - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity) + ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) REQUIREMENTS = ['face_recognition==1.0.0'] diff --git a/homeassistant/components/image_processing/dlib_face_identify.py b/homeassistant/components/image_processing/dlib_face_identify.py index 22594aa2547..bf34eb4c2da 100644 --- a/homeassistant/components/image_processing/dlib_face_identify.py +++ b/homeassistant/components/image_processing/dlib_face_identify.py @@ -11,9 +11,8 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity) + ImageProcessingFaceEntity, PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['face_recognition==1.0.0'] diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index 6770ff1bdf6..cd1e341a218 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -13,9 +13,8 @@ from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) -from homeassistant.components.image_processing.microsoft_face_identify import ( - ImageProcessingFaceEntity, ATTR_GENDER, ATTR_AGE, ATTR_GLASSES) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER, + ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 51f1cd42f47..32f02e1820e 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -9,30 +9,18 @@ import logging import voluptuous as vol -from homeassistant.core import split_entity_id, callback -from homeassistant.const import STATE_UNKNOWN +from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingEntity, CONF_CONFIDENCE, CONF_SOURCE, - CONF_ENTITY_ID, CONF_NAME, ATTR_ENTITY_ID, ATTR_CONFIDENCE) + PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME, + CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv -from homeassistant.util.async_ import run_callback_threadsafe DEPENDENCIES = ['microsoft_face'] _LOGGER = logging.getLogger(__name__) -EVENT_DETECT_FACE = 'image_processing.detect_face' - -ATTR_NAME = 'name' -ATTR_TOTAL_FACES = 'total_faces' -ATTR_AGE = 'age' -ATTR_GENDER = 'gender' -ATTR_MOTION = 'motion' -ATTR_GLASSES = 'glasses' -ATTR_FACES = 'faces' - CONF_GROUP = 'group' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -57,93 +45,6 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): async_add_devices(entities) -class ImageProcessingFaceEntity(ImageProcessingEntity): - """Base entity class for face image processing.""" - - def __init__(self): - """Initialize base face identify/verify entity.""" - self.faces = [] - self.total_faces = 0 - - @property - def state(self): - """Return the state of the entity.""" - confidence = 0 - state = STATE_UNKNOWN - - # No confidence support - if not self.confidence: - return self.total_faces - - # Search high confidence - for face in self.faces: - if ATTR_CONFIDENCE not in face: - continue - - f_co = face[ATTR_CONFIDENCE] - if f_co > confidence: - confidence = f_co - for attr in [ATTR_NAME, ATTR_MOTION]: - if attr in face: - state = face[attr] - break - - return state - - @property - def device_class(self): - """Return the class of this device, from component DEVICE_CLASSES.""" - return 'face' - - @property - def state_attributes(self): - """Return device specific state attributes.""" - attr = { - ATTR_FACES: self.faces, - ATTR_TOTAL_FACES: self.total_faces, - } - - return attr - - def process_faces(self, faces, total): - """Send event with detected faces and store data.""" - run_callback_threadsafe( - self.hass.loop, self.async_process_faces, faces, total).result() - - @callback - def async_process_faces(self, faces, total): - """Send event with detected faces and store data. - - known are a dict in follow format: - [ - { - ATTR_CONFIDENCE: 80, - ATTR_NAME: 'Name', - ATTR_AGE: 12.0, - ATTR_GENDER: 'man', - ATTR_MOTION: 'smile', - ATTR_GLASSES: 'sunglasses' - }, - ] - - This method must be run in the event loop. - """ - # Send events - for face in faces: - if ATTR_CONFIDENCE in face and self.confidence: - if face[ATTR_CONFIDENCE] < self.confidence: - continue - - face.update({ATTR_ENTITY_ID: self.entity_id}) - self.hass.async_add_job( - self.hass.bus.async_fire, EVENT_DETECT_FACE, face - ) - - # Update entity store - self.faces = faces - self.total_faces = total - - class MicrosoftFaceIdentifyEntity(ImageProcessingFaceEntity): """Representation of the Microsoft Face API entity for identify.""" From 2e8eaf40f728f9cbf75d3da5b2337da20a9099cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ron=20=C5=A0meral?= Date: Sat, 5 May 2018 17:06:32 +0200 Subject: [PATCH 616/924] Onkyo: SUPPORT_VOLUME_STEP (#14299) --- .../components/media_player/onkyo.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 245ab8bb54c..39c278ff95d 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.components.media_player import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, MediaPlayerDevice, PLATFORM_SCHEMA) + SUPPORT_VOLUME_STEP, SUPPORT_SELECT_SOURCE, SUPPORT_PLAY, + MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv @@ -29,7 +30,8 @@ DEFAULT_NAME = 'Onkyo Receiver' SUPPORTED_MAX_VOLUME = 80 SUPPORT_ONKYO = SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_SELECT_SOURCE | SUPPORT_PLAY + SUPPORT_VOLUME_STEP | SUPPORT_TURN_ON | SUPPORT_TURN_OFF | \ + SUPPORT_SELECT_SOURCE | SUPPORT_PLAY KNOWN_HOSTS = [] # type: List[str] DEFAULT_SOURCES = {'tv': 'TV', 'bd': 'Bluray', 'game': 'Game', 'aux1': 'Aux1', @@ -199,6 +201,14 @@ class OnkyoDevice(MediaPlayerDevice): """ self.command('volume {}'.format(int(volume * self._max_volume))) + def volume_up(self): + """Increase volume by 1 step.""" + self.command('volume level-up') + + def volume_down(self): + """Decrease volume by 1 step.""" + self.command('volume level-down') + def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: @@ -264,6 +274,14 @@ class OnkyoDeviceZone2(OnkyoDevice): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" self.command('zone2.volume={}'.format(int(volume*80))) + def volume_up(self): + """Increase volume by 1 step.""" + self.command('zone2.volume=level-up') + + def volume_down(self): + """Decrease volume by 1 step.""" + self.command('zone2.volume=level-down') + def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: From 64ba2c63c7bdf498875ce2117fc5a255d157be17 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 5 May 2018 11:15:20 -0400 Subject: [PATCH 617/924] Add All-Linking capabilities (#14065) * Setup all-linking service * Remove extra line * Remove linefeed and tab escape chars * Add services delete_all_link, load_all_link_database and print_all_link_database * Check if reload is set * Confirm entity is InsteonPLMEntity before attempting to load or print ALDB * Debug load and print ALDB * Debug print aldb * Debug print_aldb * Get entity via platform * Track Insteon entities in component * Store entity list in hass.data * Add entity to hass.data * Add ref to hass in InsteonPLMEntity * Pass hass correctly to InsteonPLMBinarySensor * Fix reference to ALDBStatus.PARTIAL * Print ALDB record as string * Get ALDB record from memory address * Reformat ALDB log output * Add print_im_aldb service * Remove reference to self in print_aldb_to_log * Remove reference to self in print_aldb_to_log * Fix spelling issue with load_all_link_database service * Bump insteonplm to 0.9.1 * Changes from code review * Code review changes * Fix syntax error * Correct reference to cv.boolean and update requirements * Update requirements * Fix flake8 errors * Reload as boolean test * Remove hass from entity init --- .../components/binary_sensor/insteon_plm.py | 2 +- homeassistant/components/fan/insteon_plm.py | 2 +- .../__init__.py} | 136 +++++++++++++++++- .../components/insteon_plm/services.yaml | 32 +++++ homeassistant/components/light/insteon_plm.py | 2 +- .../components/sensor/insteon_plm.py | 2 +- .../components/switch/insteon_plm.py | 2 +- requirements_all.txt | 2 +- 8 files changed, 171 insertions(+), 9 deletions(-) rename homeassistant/components/{insteon_plm.py => insteon_plm/__init__.py} (57%) create mode 100644 homeassistant/components/insteon_plm/services.yaml diff --git a/homeassistant/components/binary_sensor/insteon_plm.py b/homeassistant/components/binary_sensor/insteon_plm.py index 06079d6aa3b..9cb87b31749 100644 --- a/homeassistant/components/binary_sensor/insteon_plm.py +++ b/homeassistant/components/binary_sensor/insteon_plm.py @@ -23,7 +23,7 @@ SENSOR_TYPES = {'openClosedSensor': 'opening', @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/fan/insteon_plm.py b/homeassistant/components/fan/insteon_plm.py index f30abdbaa30..0911295d090 100644 --- a/homeassistant/components/fan/insteon_plm.py +++ b/homeassistant/components/fan/insteon_plm.py @@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/insteon_plm.py b/homeassistant/components/insteon_plm/__init__.py similarity index 57% rename from homeassistant/components/insteon_plm.py rename to homeassistant/components/insteon_plm/__init__.py index d867f0c3d28..246e84ec71f 100644 --- a/homeassistant/components/insteon_plm.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -11,12 +11,13 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP, - CONF_PLATFORM) + CONF_PLATFORM, + CONF_ENTITY_ID) import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.8.6'] +REQUIREMENTS = ['insteonplm==0.9.1'] _LOGGER = logging.getLogger(__name__) @@ -29,6 +30,17 @@ CONF_SUBCAT = 'subcat' CONF_FIRMWARE = 'firmware' CONF_PRODUCT_KEY = 'product_key' +SRV_ADD_ALL_LINK = 'add_all_link' +SRV_DEL_ALL_LINK = 'delete_all_link' +SRV_LOAD_ALDB = 'load_all_link_database' +SRV_PRINT_ALDB = 'print_all_link_database' +SRV_PRINT_IM_ALDB = 'print_im_all_link_database' +SRV_ALL_LINK_GROUP = 'group' +SRV_ALL_LINK_MODE = 'mode' +SRV_LOAD_DB_RELOAD = 'reload' +SRV_CONTROLLER = 'controller' +SRV_RESPONDER = 'responder' + CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( cv.deprecated(CONF_PLATFORM), vol.Schema({ vol.Required(CONF_ADDRESS): cv.string, @@ -47,6 +59,24 @@ CONFIG_SCHEMA = vol.Schema({ }) }, extra=vol.ALLOW_EXTRA) +ADD_ALL_LINK_SCHEMA = vol.Schema({ + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), + }) + +DEL_ALL_LINK_SCHEMA = vol.Schema({ + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + }) + +LOAD_ALDB_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Optional(SRV_LOAD_DB_RELOAD, default='false'): cv.boolean, + }) + +PRINT_ALDB_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + }) + @asyncio.coroutine def async_setup(hass, config): @@ -54,6 +84,7 @@ def async_setup(hass, config): import insteonplm ipdb = IPDB() + plm = None conf = config[DOMAIN] port = conf.get(CONF_PORT) @@ -79,6 +110,60 @@ def async_setup(hass, config): 'state_key': state_key}, hass_config=config)) + def add_all_link(service): + """Add an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + mode = service.data.get(SRV_ALL_LINK_MODE) + link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 + plm.start_all_linking(link_mode, group) + + def del_all_link(service): + """Delete an INSTEON All-Link between two devices.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + plm.start_all_linking(255, group) + + def load_aldb(service): + """Load the device All-Link database.""" + entity_id = service.data.get(CONF_ENTITY_ID) + reload = service.data.get(SRV_LOAD_DB_RELOAD) + entities = hass.data[DOMAIN].get('entities') + entity = entities.get(entity_id) + if entity: + entity.load_aldb(reload) + else: + _LOGGER.error('Entity %s is not an INSTEON device', entity_id) + + def print_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + entity_id = service.data.get(CONF_ENTITY_ID) + entities = hass.data[DOMAIN].get('entities') + entity = entities.get(entity_id) + if entity: + entity.print_aldb() + else: + _LOGGER.error('Entity %s is not an INSTEON device', entity_id) + + def print_im_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + print_aldb_to_log(plm.aldb) + + def _register_services(): + hass.services.register(DOMAIN, SRV_ADD_ALL_LINK, add_all_link, + schema=ADD_ALL_LINK_SCHEMA) + hass.services.register(DOMAIN, SRV_DEL_ALL_LINK, del_all_link, + schema=DEL_ALL_LINK_SCHEMA) + hass.services.register(DOMAIN, SRV_LOAD_ALDB, load_aldb, + schema=LOAD_ALDB_SCHEMA) + hass.services.register(DOMAIN, SRV_PRINT_ALDB, print_aldb, + schema=PRINT_ALDB_SCHEMA) + hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, + schema=None) + _LOGGER.debug("Insteon_plm Services registered") + _LOGGER.info("Looking for PLM on %s", port) conn = yield from insteonplm.Connection.create( device=port, @@ -100,11 +185,14 @@ def async_setup(hass, config): plm.devices.add_override(address, CONF_PRODUCT_KEY, device_override[prop]) - hass.data['insteon_plm'] = plm + hass.data[DOMAIN] = {} + hass.data[DOMAIN]['plm'] = plm + hass.data[DOMAIN]['entities'] = {} hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) plm.devices.add_device_callback(async_plm_new_device) + hass.async_add_job(_register_services) return True @@ -169,6 +257,7 @@ class InsteonPLMEntity(Entity): """Initialize the INSTEON PLM binary sensor.""" self._insteon_device_state = device.states[state_key] self._insteon_device = device + self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) @property def should_poll(self): @@ -215,3 +304,44 @@ class InsteonPLMEntity(Entity): """Register INSTEON update events.""" self._insteon_device_state.register_updates( self.async_entity_update) + self.hass.data[DOMAIN]['entities'][self.entity_id] = self + + def load_aldb(self, reload=False): + """Load the device All-Link Database.""" + if reload: + self._insteon_device.aldb.clear() + self._insteon_device.read_aldb() + + def print_aldb(self): + """Print the device ALDB to the log file.""" + print_aldb_to_log(self._insteon_device.aldb) + + @callback + def _aldb_loaded(self): + """All-Link Database loaded for the device.""" + self.print_aldb() + + +def print_aldb_to_log(aldb): + """Print the All-Link Database to the log file.""" + from insteonplm.devices import ALDBStatus + _LOGGER.info('ALDB load status is %s', aldb.status.name) + if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: + _LOGGER.warning('Device All-Link database not loaded') + _LOGGER.warning('Use service insteon_plm.load_aldb first') + return + + _LOGGER.info('RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3') + _LOGGER.info('----- ------ ---- --- ----- -------- ------ ------ ------') + for mem_addr in aldb: + rec = aldb[mem_addr] + # For now we write this to the log + # Roadmap is to create a configuration panel + in_use = 'Y' if rec.control_flags.is_in_use else 'N' + mode = 'C' if rec.control_flags.is_controller else 'R' + hwm = 'Y' if rec.control_flags.is_high_water_mark else 'N' + _LOGGER.info(' {:04x} {:s} {:s} {:s} {:3d} {:s}' + ' {:3d} {:3d} {:3d}'.format( + rec.mem_addr, in_use, mode, hwm, + rec.group, rec.address.human, + rec.data1, rec.data2, rec.data3)) diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml new file mode 100644 index 00000000000..a0e250fef1f --- /dev/null +++ b/homeassistant/components/insteon_plm/services.yaml @@ -0,0 +1,32 @@ +add_all_link: + description: Tells the Insteom Modem (IM) start All-Linking mode. Once the the IM is in All-Linking mode, press the link button on the device to complete All-Linking. + fields: + group: + description: All-Link group number. + example: 1 + mode: + description: Linking mode controller - IM is controller responder - IM is responder + example: 'controller' +delete_all_link: + description: Tells the Insteon Modem (IM) to remove an All-Link record from the All-Link Database of the IM and a device. Once the IM is set to delete the link, press the link button on the corresponding device to complete the process. + fields: + group: + description: All-Link group number. + example: 1 +load_all_link_database: + description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. + fields: + entity_id: + description: Name of the device to print + example: 'light.1a2b3c' + reload: + description: Reload all records. If true the current records are cleared from memory (does not effect the device) and the records are reloaded. If false the existing records are left in place and only missing records are added. Default is false. + example: 'true' +print_all_link_database: + description: Print the All-Link Database for a device. Requires that the All-Link Database is loaded into memory. + fields: + entity_id: + description: Name of the device to print + example: 'light.1a2b3c' +print_im_all_link_database: + description: Print the All-Link Database for the INSTEON Modem (IM). diff --git a/homeassistant/components/light/insteon_plm.py b/homeassistant/components/light/insteon_plm.py index 40453da38e5..8a3b463c2bd 100644 --- a/homeassistant/components/light/insteon_plm.py +++ b/homeassistant/components/light/insteon_plm.py @@ -21,7 +21,7 @@ MAX_BRIGHTNESS = 255 @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the Insteon PLM device.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/sensor/insteon_plm.py b/homeassistant/components/sensor/insteon_plm.py index a72b8efbc05..61f5877ed78 100644 --- a/homeassistant/components/sensor/insteon_plm.py +++ b/homeassistant/components/sensor/insteon_plm.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/homeassistant/components/switch/insteon_plm.py b/homeassistant/components/switch/insteon_plm.py index 5f9482ce955..be562e9d909 100644 --- a/homeassistant/components/switch/insteon_plm.py +++ b/homeassistant/components/switch/insteon_plm.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Set up the INSTEON PLM device class for the hass platform.""" - plm = hass.data['insteon_plm'] + plm = hass.data['insteon_plm'].get('plm') address = discovery_info['address'] device = plm.devices[address] diff --git a/requirements_all.txt b/requirements_all.txt index d24c2e07043..e9033091f3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -446,7 +446,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.8.6 +insteonplm==0.9.1 # homeassistant.components.verisure jsonpath==0.75 From a4b69833d4e9ac99cc3f196e5cbb2fa5c5f644c4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:32:36 -0400 Subject: [PATCH 618/924] Update translations --- .../components/deconz/.translations/bg.json | 25 +++++++++++++ .../components/deconz/.translations/cy.json | 26 ++++++++++++++ .../components/deconz/.translations/da.json | 11 ++++++ .../components/deconz/.translations/de.json | 26 ++++++++++++++ .../components/deconz/.translations/en.json | 36 +++++++++---------- .../components/deconz/.translations/hu.json | 22 ++++++++++++ .../components/deconz/.translations/ko.json | 26 ++++++++++++++ .../components/deconz/.translations/lb.json | 26 ++++++++++++++ .../components/deconz/.translations/nl.json | 26 ++++++++++++++ .../components/deconz/.translations/no.json | 26 ++++++++++++++ .../components/deconz/.translations/pl.json | 26 ++++++++++++++ .../components/deconz/.translations/pt.json | 7 ++++ .../components/deconz/.translations/ru.json | 26 ++++++++++++++ .../components/deconz/.translations/sl.json | 26 ++++++++++++++ .../deconz/.translations/zh-Hans.json | 26 ++++++++++++++ .../deconz/.translations/zh-Hant.json | 25 +++++++++++++ .../components/hue/.translations/bg.json | 29 +++++++++++++++ .../components/hue/.translations/cy.json | 29 +++++++++++++++ .../components/hue/.translations/da.json | 19 ++++++++++ .../components/hue/.translations/de.json | 5 ++- .../components/hue/.translations/en.json | 5 ++- .../components/hue/.translations/es.json | 11 ++++++ .../components/hue/.translations/hu.json | 28 +++++++++++++++ .../components/hue/.translations/it.json | 10 ++++++ .../components/hue/.translations/ko.json | 5 ++- .../components/hue/.translations/lb.json | 29 +++++++++++++++ .../components/hue/.translations/nl.json | 7 ++-- .../components/hue/.translations/no.json | 5 ++- .../components/hue/.translations/pl.json | 5 ++- .../components/hue/.translations/pt.json | 5 +++ .../components/hue/.translations/ru.json | 29 +++++++++++++++ .../components/hue/.translations/sl.json | 5 ++- .../components/hue/.translations/zh-Hans.json | 7 ++-- .../components/hue/.translations/zh-Hant.json | 29 +++++++++++++++ .../sensor/.translations/season.bg.json | 8 +++++ .../sensor/.translations/season.da.json | 8 +++++ .../sensor/.translations/season.hu.json | 8 +++++ .../sensor/.translations/season.it.json | 8 +++++ .../sensor/.translations/season.lb.json | 8 +++++ .../sensor/.translations/season.ru.json | 8 +++++ .../components/zone/.translations/cy.json | 21 +++++++++++ .../components/zone/.translations/de.json | 21 +++++++++++ .../components/zone/.translations/en.json | 30 ++++++++-------- .../components/zone/.translations/ko.json | 21 +++++++++++ .../components/zone/.translations/lb.json | 21 +++++++++++ .../components/zone/.translations/nl.json | 21 +++++++++++ .../components/zone/.translations/no.json | 21 +++++++++++ .../components/zone/.translations/pl.json | 21 +++++++++++ .../components/zone/.translations/pt.json | 20 +++++++++++ .../components/zone/.translations/ru.json | 21 +++++++++++ .../zone/.translations/zh-Hans.json | 21 +++++++++++ 51 files changed, 892 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/deconz/.translations/bg.json create mode 100644 homeassistant/components/deconz/.translations/cy.json create mode 100644 homeassistant/components/deconz/.translations/da.json create mode 100644 homeassistant/components/deconz/.translations/de.json create mode 100644 homeassistant/components/deconz/.translations/hu.json create mode 100644 homeassistant/components/deconz/.translations/ko.json create mode 100644 homeassistant/components/deconz/.translations/lb.json create mode 100644 homeassistant/components/deconz/.translations/nl.json create mode 100644 homeassistant/components/deconz/.translations/no.json create mode 100644 homeassistant/components/deconz/.translations/pl.json create mode 100644 homeassistant/components/deconz/.translations/pt.json create mode 100644 homeassistant/components/deconz/.translations/ru.json create mode 100644 homeassistant/components/deconz/.translations/sl.json create mode 100644 homeassistant/components/deconz/.translations/zh-Hans.json create mode 100644 homeassistant/components/deconz/.translations/zh-Hant.json create mode 100644 homeassistant/components/hue/.translations/bg.json create mode 100644 homeassistant/components/hue/.translations/cy.json create mode 100644 homeassistant/components/hue/.translations/da.json create mode 100644 homeassistant/components/hue/.translations/es.json create mode 100644 homeassistant/components/hue/.translations/hu.json create mode 100644 homeassistant/components/hue/.translations/it.json create mode 100644 homeassistant/components/hue/.translations/lb.json create mode 100644 homeassistant/components/hue/.translations/pt.json create mode 100644 homeassistant/components/hue/.translations/ru.json create mode 100644 homeassistant/components/hue/.translations/zh-Hant.json create mode 100644 homeassistant/components/sensor/.translations/season.bg.json create mode 100644 homeassistant/components/sensor/.translations/season.da.json create mode 100644 homeassistant/components/sensor/.translations/season.hu.json create mode 100644 homeassistant/components/sensor/.translations/season.it.json create mode 100644 homeassistant/components/sensor/.translations/season.lb.json create mode 100644 homeassistant/components/sensor/.translations/season.ru.json create mode 100644 homeassistant/components/zone/.translations/cy.json create mode 100644 homeassistant/components/zone/.translations/de.json create mode 100644 homeassistant/components/zone/.translations/ko.json create mode 100644 homeassistant/components/zone/.translations/lb.json create mode 100644 homeassistant/components/zone/.translations/nl.json create mode 100644 homeassistant/components/zone/.translations/no.json create mode 100644 homeassistant/components/zone/.translations/pl.json create mode 100644 homeassistant/components/zone/.translations/pt.json create mode 100644 homeassistant/components/zone/.translations/ru.json create mode 100644 homeassistant/components/zone/.translations/zh-Hans.json diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json new file mode 100644 index 00000000000..91727cae257 --- /dev/null +++ b/homeassistant/components/deconz/.translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u043e \u043a\u043e\u043f\u0438\u0435 \u043d\u0430 deCONZ" + }, + "error": { + "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u0441\u0442\u043e\u0439\u043d\u043e\u0441\u0442 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: '80')" + }, + "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0448\u043b\u044e\u0437" + }, + "link": { + "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0432\u043e\u0440\u0435\u0442\u0435 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0430 deCONZ\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Unlock Gateway\"", + "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/cy.json b/homeassistant/components/deconz/.translations/cy.json new file mode 100644 index 00000000000..fff54bb3f6c --- /dev/null +++ b/homeassistant/components/deconz/.translations/cy.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Pont eisoes wedi'i ffurfweddu", + "no_bridges": "Dim pontydd deCONZ wedi eu darganfod", + "one_instance_only": "Elfen dim ond yn cefnogi enghraifft deCONZ" + }, + "error": { + "no_key": "Methu cael allwedd API" + }, + "step": { + "init": { + "data": { + "host": "Gwesteiwr", + "port": "Port (gwerth diofyn: '80')" + }, + "title": "Diffiniwch porth dad-adeiladu" + }, + "link": { + "description": "Datgloi eich porth deCONZ i gofrestru gyda Cynorthwydd Cartref.\n\n1. Ewch i osodiadau system deCONZ \n2. Bwyso botwm \"Datgloi porth\"", + "title": "Cysylltu \u00e2 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json new file mode 100644 index 00000000000..698f55c59ec --- /dev/null +++ b/homeassistant/components/deconz/.translations/da.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "V\u00e6rt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json new file mode 100644 index 00000000000..9d3dc9e6e62 --- /dev/null +++ b/homeassistant/components/deconz/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ist bereits konfiguriert", + "no_bridges": "Keine deCON-Bridges entdeckt", + "one_instance_only": "Komponente unterst\u00fctzt nur eine deCONZ-Instanz" + }, + "error": { + "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (Standartwert : '80')" + }, + "title": "Definieren Sie den deCONZ-Gateway" + }, + "link": { + "description": "Entsperren Sie Ihr deCONZ-Gateway, um sich bei Home Assistant zu registrieren. \n\n 1. Gehen Sie zu den deCONZ-Systemeinstellungen \n 2. Dr\u00fccken Sie die Taste \"Gateway entsperren\"", + "title": "Mit deCONZ verbinden" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 7ea68af01c1..0009986d45f 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -1,26 +1,26 @@ { "config": { - "title": "deCONZ", - "step": { - "init": { - "title": "Define deCONZ gateway", - "data": { - "host": "Host", - "port": "Port (default value: '80')" - } - }, - "link": { - "title": "Link with deCONZ", - "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" - } - }, - "error": { - "no_key": "Couldn't get an API key" - }, "abort": { "already_configured": "Bridge is already configured", "no_bridges": "No deCONZ bridges discovered", "one_instance_only": "Component only supports one deCONZ instance" - } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (default value: '80')" + }, + "title": "Define deCONZ gateway" + }, + "link": { + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", + "title": "Link with deCONZ" + } + }, + "title": "deCONZ" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json new file mode 100644 index 00000000000..42aab9c6d7e --- /dev/null +++ b/homeassistant/components/deconz/.translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "one_instance_only": "Ez a komponens csak egy deCONZ egys\u00e9get t\u00e1mogat" + }, + "error": { + "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" + }, + "step": { + "init": { + "data": { + "host": "H\u00e1zigazda (Host)", + "port": "Port (alap\u00e9rtelmezett \u00e9rt\u00e9k: '80')" + } + }, + "link": { + "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json new file mode 100644 index 00000000000..d6de1028218 --- /dev/null +++ b/homeassistant/components/deconz/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "one_instance_only": "\uad6c\uc131\uc694\uc18c\ub294 \ud558\ub098\uc758 deCONZ \uc778\uc2a4\ud134\uc2a4 \ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4" + }, + "error": { + "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8 (\uae30\ubcf8\uac12: '80')" + }, + "title": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774 \uc815\uc758" + }, + "link": { + "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Unlock Gateway\" \ubc84\ud2bc\uc744 \ub204\ub974\uc138\uc694 ", + "title": "deCONZ \uc640 \uc5f0\uacb0" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json new file mode 100644 index 00000000000..2a9dfc5e543 --- /dev/null +++ b/homeassistant/components/deconz/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge ass schon konfigur\u00e9iert", + "no_bridges": "Keng dECONZ bridges fonnt", + "one_instance_only": "Komponent \u00ebnnerst\u00ebtzt n\u00ebmmen eng deCONZ Instanz" + }, + "error": { + "no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (Standard Wert: '80')" + }, + "title": "deCONZ gateway d\u00e9fin\u00e9ieren" + }, + "link": { + "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op\u00a0deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", + "title": "Link mat deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json new file mode 100644 index 00000000000..90d13bb39b4 --- /dev/null +++ b/homeassistant/components/deconz/.translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge is al geconfigureerd", + "no_bridges": "Geen deCONZ bruggen ontdekt", + "one_instance_only": "Component ondersteunt slechts \u00e9\u00e9n deCONZ instance" + }, + "error": { + "no_key": "Kon geen API-sleutel ophalen" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Poort (standaard: '80')" + }, + "title": "Definieer deCONZ gateway" + }, + "link": { + "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen\n2. Druk op de knop \"Gateway ontgrendelen\"", + "title": "Koppel met deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json new file mode 100644 index 00000000000..25e3b0b7d68 --- /dev/null +++ b/homeassistant/components/deconz/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Broen er allerede konfigurert", + "no_bridges": "Ingen deCONZ broer oppdaget", + "one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst" + }, + "error": { + "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" + }, + "step": { + "init": { + "data": { + "host": "Vert", + "port": "Port (standardverdi: '80')" + }, + "title": "Definer deCONZ-gatewayen" + }, + "link": { + "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", + "title": "Koble til deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json new file mode 100644 index 00000000000..bb7488fcbec --- /dev/null +++ b/homeassistant/components/deconz/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Mostek jest ju\u017c skonfigurowany", + "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", + "one_instance_only": "Komponent obs\u0142uguje tylko jedn\u0105 instancj\u0119 deCONZ" + }, + "error": { + "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" + }, + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port (warto\u015b\u0107 domy\u015blna: \"80\")" + }, + "title": "Zdefiniuj bramk\u0119 deCONZ" + }, + "link": { + "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", + "title": "Po\u0142\u0105cz z deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json new file mode 100644 index 00000000000..2a00c698691 --- /dev/null +++ b/homeassistant/components/deconz/.translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Bridge j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json new file mode 100644 index 00000000000..b0dc6a8a4a8 --- /dev/null +++ b/homeassistant/components/deconz/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ" + }, + "error": { + "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: '80')" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0448\u043b\u044e\u0437 deCONZ" + }, + "link": { + "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00ab\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437\u00bb", + "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json new file mode 100644 index 00000000000..b738002b273 --- /dev/null +++ b/homeassistant/components/deconz/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Most je \u017ee nastavljen", + "no_bridges": "Ni odkritih mostov deCONZ", + "one_instance_only": "Komponenta podpira le en primerek deCONZ" + }, + "error": { + "no_key": "Klju\u010da API ni mogo\u010de dobiti" + }, + "step": { + "init": { + "data": { + "host": "Gostitelj", + "port": "Vrata (privzeta vrednost: '80')" + }, + "title": "Dolo\u010dite deCONZ prehod" + }, + "link": { + "description": "Odklenite va\u0161 deCONZ gateway za registracijo z Home Assistant-om. \n1. Pojdite v deCONT sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", + "title": "Povezava z deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json new file mode 100644 index 00000000000..f41b5b5111c --- /dev/null +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u6865\u63a5\u5668\u5df2\u914d\u7f6e\u5b8c\u6210", + "no_bridges": "\u6ca1\u6709\u53d1\u73b0 deCONZ \u7684\u6865\u63a5\u8bbe\u5907", + "one_instance_only": "\u7ec4\u4ef6\u53ea\u652f\u6301\u4e00\u4e2a deCONZ \u5b9e\u4f8b" + }, + "error": { + "no_key": "\u65e0\u6cd5\u83b7\u53d6 API \u5bc6\u94a5" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u673a", + "port": "\u7aef\u53e3\uff08\u9ed8\u8ba4\u503c\uff1a'80'\uff09" + }, + "title": "\u5b9a\u4e49 deCONZ \u7f51\u5173" + }, + "link": { + "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", + "title": "\u8fde\u63a5 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json new file mode 100644 index 00000000000..33be3846eb8 --- /dev/null +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", + "one_instance_only": "\u7d44\u4ef6\u50c5\u652f\u63f4\u4e00\u7d44 deCONZ \u5be6\u4f8b" + }, + "error": { + "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0\uff08\u9810\u8a2d\u503c\uff1a'80'\uff09" + }, + "title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc" + }, + "link": { + "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u89e3\u9664\u7db2\u95dc\u9396\u5b9a\uff08Unlock Gateway\uff09\u300d\u6309\u9215", + "title": "\u9023\u7d50\u81f3 deCONZ" + } + }, + "title": "deCONZ" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/bg.json b/homeassistant/components/hue/.translations/bg.json new file mode 100644 index 00000000000..276f5053bf7 --- /dev/null +++ b/homeassistant/components/hue/.translations/bg.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Philips Hue \u0441\u0430 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", + "already_configured": "\u0411\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "cannot_connect": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", + "discover_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "error": { + "linking": "\u041f\u043e\u044f\u0432\u0438 \u0441\u0435 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e.", + "register_failed": "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0431\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u0437\u0430 \u0434\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0442\u0435 Philips Hue \u0441 Home Assistant. \n\n![\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0431\u0443\u0442\u043e\u043d\u0430 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f](/static/images/config_philips_hue.jpg)", + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0445\u044a\u0431" + } + }, + "title": "\u0411\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/cy.json b/homeassistant/components/hue/.translations/cy.json new file mode 100644 index 00000000000..f5476f73edb --- /dev/null +++ b/homeassistant/components/hue/.translations/cy.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "Mae holl bontydd Philips Hue eisoes wedi eu ffurfweddu", + "already_configured": "Pont eisoes wedi'i ffurfweddu", + "cannot_connect": "Methu cysylltu i'r bont", + "discover_timeout": "Methu darganfod pontydd Hue", + "no_bridges": "Dim pontydd Philips Hue wedi'i ddarganfod", + "unknown": "Digwyddodd gwall anhysbys" + }, + "error": { + "linking": "Digwyddodd gwall cysylltu anhysbys.", + "register_failed": "Wedi methu \u00e2 chofrestru, pl\u00eds ceisiwch eto" + }, + "step": { + "init": { + "data": { + "host": "Gwesteiwr" + }, + "title": "Dewiswch bont Hue" + }, + "link": { + "description": "Pwyswch y botwm ar y bont i gofrestru Philips Hue gyda Cynorthwydd Cartref.\n\n![Lleoliad botwm ar bont](/static/images/config_philips_hue.jpg)", + "title": "Hwb cyswllt" + } + }, + "title": "Pont Phillips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json new file mode 100644 index 00000000000..3e5e2b1d3d7 --- /dev/null +++ b/homeassistant/components/hue/.translations/da.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "no_bridges": "Ingen Philips Hue bridge fundet" + }, + "step": { + "init": { + "data": { + "host": "V\u00e6rt" + }, + "title": "V\u00e6lg Hue bridge" + }, + "link": { + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index f11af7756c7..d466488e9fc 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", + "already_configured": "Bridge ist bereits konfiguriert", + "cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich", "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", - "no_bridges": "Philips Hue Bridges entdeckt" + "no_bridges": "Keine Philips Hue Bridges entdeckt", + "unknown": "Unbekannter Fehler ist aufgetreten" }, "error": { "linking": "Unbekannter Link-Fehler aufgetreten.", diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index cbf63301da2..b0459ec3916 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "All Philips Hue bridges are already configured", + "already_configured": "Bridge is already configured", + "cannot_connect": "Unable to connect to the bridge", "discover_timeout": "Unable to discover Hue bridges", - "no_bridges": "No Philips Hue bridges discovered" + "no_bridges": "No Philips Hue bridges discovered", + "unknown": "Unknown error occurred" }, "error": { "linking": "Unknown linking error occurred.", diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json new file mode 100644 index 00000000000..d58469af044 --- /dev/null +++ b/homeassistant/components/hue/.translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "unknown": "Se produjo un error desconocido" + }, + "error": { + "linking": "Se produjo un error de enlace desconocido.", + "register_failed": "No se pudo registrar, intente de nuevo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/hu.json b/homeassistant/components/hue/.translations/hu.json new file mode 100644 index 00000000000..a4032dcbcfc --- /dev/null +++ b/homeassistant/components/hue/.translations/hu.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "all_configured": "M\u00e1r minden Philips Hue bridge konfigur\u00e1lt", + "already_configured": "A bridge m\u00e1r konfigur\u00e1lt", + "cannot_connect": "Nem siker\u00fclt csatlakozni a bridge-hez.", + "discover_timeout": "Nem tal\u00e1ltam a Hue bridget", + "no_bridges": "Nem tal\u00e1ltam Philips Hue bridget", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" + }, + "error": { + "linking": "Ismeretlen \u00f6sszekapcsol\u00e1si hiba t\u00f6rt\u00e9nt.", + "register_failed": "Regisztr\u00e1ci\u00f3 nem siker\u00fclt, k\u00e9rem pr\u00f3b\u00e1lja \u00fajra" + }, + "step": { + "init": { + "data": { + "host": "H\u00e1zigazda (Host)" + }, + "title": "V\u00e1lassz Hue bridge-t" + }, + "link": { + "title": "Kapcsol\u00f3d\u00e1s a hubhoz" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json new file mode 100644 index 00000000000..2c7a8c1924d --- /dev/null +++ b/homeassistant/components/hue/.translations/it.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", + "discover_timeout": "Impossibile trovare i bridge Hue", + "no_bridges": "Nessun bridge Hue di Philips trovato" + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 226ae8ba1f6..47306a35414 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + "no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "linking": "\uc54c \uc218\uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/hue/.translations/lb.json b/homeassistant/components/hue/.translations/lb.json new file mode 100644 index 00000000000..c4ad10da278 --- /dev/null +++ b/homeassistant/components/hue/.translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "All Philips Hue Bridge si scho\u00a0konfigur\u00e9iert", + "already_configured": "Bridge ass scho konfigur\u00e9iert", + "cannot_connect": "Keng Verbindung mat der bridge m\u00e9iglech", + "discover_timeout": "Keng Hue bridge fonnt", + "no_bridges": "Keng Philips Hue Bridge fonnt", + "unknown": "Onbekannten Feeler opgetrueden" + }, + "error": { + "linking": "Onbekannte Liaisoun's Feeler opgetrueden", + "register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9iert w.e.g. nach emol" + }, + "step": { + "init": { + "data": { + "host": "Host" + }, + "title": "Hue Bridge auswielen" + }, + "link": { + "description": "Dr\u00e9ckt de Kn\u00e4ppchen un der Bridge fir den Philips Hue mam Home Assistant ze registr\u00e9ieren.\n\n![Kn\u00e4ppchen un der Bridge](/static/images/config_philips_hue.jpg)", + "title": "Link Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/nl.json b/homeassistant/components/hue/.translations/nl.json index 750ae39db12..88c611b1633 100644 --- a/homeassistant/components/hue/.translations/nl.json +++ b/homeassistant/components/hue/.translations/nl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", + "already_configured": "Bridge is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken met bridge", "discover_timeout": "Hue bridges kunnen niet worden gevonden", - "no_bridges": "Geen Philips Hue bridges ontdekt" + "no_bridges": "Geen Philips Hue bridges ontdekt", + "unknown": "Onbekende fout opgetreden" }, "error": { "linking": "Er is een onbekende verbindingsfout opgetreden.", @@ -17,7 +20,7 @@ "title": "Kies Hue bridge" }, "link": { - "description": "Druk op de knop van de bridge om Philips Hue te registreren met de Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", + "description": "Druk op de knop van de bridge om Philips Hue te registreren met Home Assistant. ![Locatie van de knop op bridge] (/static/images/config_philips_hue.jpg)", "title": "Link Hub" } }, diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index 604475d2ff2..309e9f6a299 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Alle Philips Hue Bridger er allerede konfigurert", + "already_configured": "Bridge er allerede konfigurert", + "cannot_connect": "Kan ikke koble til Bridge", "discover_timeout": "Kunne ikke oppdage Hue Bridger", - "no_bridges": "Ingen Philips Hue Bridger oppdaget" + "no_bridges": "Ingen Philips Hue Bridger oppdaget", + "unknown": "Ukjent feil oppstod" }, "error": { "linking": "Ukjent koblingsfeil oppstod.", diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index e364b7033a1..784fa0d99a6 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane", + "already_configured": "Mostek jest ju\u017c skonfigurowany", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", - "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue" + "no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue", + "unknown": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", diff --git a/homeassistant/components/hue/.translations/pt.json b/homeassistant/components/hue/.translations/pt.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/hue/.translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json new file mode 100644 index 00000000000..ea1e4fff1bf --- /dev/null +++ b/homeassistant/components/hue/.translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", + "already_configured": "\u0428\u043b\u044e\u0437 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", + "cannot_connect": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", + "discover_timeout": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437\u044b Philips Hue", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + }, + "error": { + "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Hue" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", + "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" + } + }, + "title": "\u0428\u043b\u044e\u0437 Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json index a6c858e0e40..4245ce02c66 100644 --- a/homeassistant/components/hue/.translations/sl.json +++ b/homeassistant/components/hue/.translations/sl.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani", + "already_configured": "Most je \u017ee konfiguriran", + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z mostom", "discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov", - "no_bridges": "Ni odkritih mostov Philips Hue" + "no_bridges": "Ni odkritih mostov Philips Hue", + "unknown": "Pri\u0161lo je do neznane napake" }, "error": { "linking": "Pri\u0161lo je do neznane napake pri povezavi.", diff --git a/homeassistant/components/hue/.translations/zh-Hans.json b/homeassistant/components/hue/.translations/zh-Hans.json index 5a94e084dd2..1d904070b81 100644 --- a/homeassistant/components/hue/.translations/zh-Hans.json +++ b/homeassistant/components/hue/.translations/zh-Hans.json @@ -2,8 +2,11 @@ "config": { "abort": { "all_configured": "\u5168\u90e8\u98de\u5229\u6d66 Hue \u6865\u63a5\u5668\u5df2\u914d\u7f6e", + "already_configured": "\u98de\u5229\u6d66 Hue Bridge \u5df2\u914d\u7f6e\u5b8c\u6210", + "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 \u98de\u5229\u6d66 Hue Bridge", "discover_timeout": "\u65e0\u6cd5\u55c5\u63a2 Hue \u6865\u63a5\u5668", - "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge" + "no_bridges": "\u672a\u53d1\u73b0\u98de\u5229\u6d66 Hue Bridge", + "unknown": "\u51fa\u73b0\u672a\u77e5\u7684\u9519\u8bef" }, "error": { "linking": "\u53d1\u751f\u672a\u77e5\u7684\u8fde\u63a5\u9519\u8bef\u3002", @@ -17,7 +20,7 @@ "title": "\u9009\u62e9 Hue Bridge" }, "link": { - "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue ![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", + "description": "\u8bf7\u6309\u4e0b\u6865\u63a5\u5668\u4e0a\u7684\u6309\u94ae\uff0c\u4ee5\u5728 Home Assistant \u4e0a\u6ce8\u518c\u98de\u5229\u6d66 Hue\u3002\n\n![\u6865\u63a5\u5668\u6309\u94ae\u4f4d\u7f6e](/static/images/config_philips_hue.jpg)", "title": "\u8fde\u63a5\u4e2d\u67a2" } }, diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json new file mode 100644 index 00000000000..eae4c09da49 --- /dev/null +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge", + "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", + "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" + }, + "error": { + "linking": "\u767c\u751f\u672a\u77e5\u9023\u7d50\u932f\u8aa4\u3002", + "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66" + }, + "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u9078\u64c7 Hue Bridge" + }, + "link": { + "description": "\u6309\u4e0b Bridge \u4e0a\u7684\u6309\u9215\uff0c\u4ee5\u5c07 Philips Hue \u8a3b\u518a\u81f3 Home Assistant\u3002\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "\u9023\u7d50 Hub" + } + }, + "title": "Philips Hue Bridge" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.bg.json b/homeassistant/components/sensor/.translations/season.bg.json new file mode 100644 index 00000000000..e3865ca42e5 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0415\u0441\u0435\u043d", + "spring": "\u041f\u0440\u043e\u043b\u0435\u0442", + "summer": "\u041b\u044f\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.da.json b/homeassistant/components/sensor/.translations/season.da.json new file mode 100644 index 00000000000..9cded2f9c0f --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.da.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Efter\u00e5r", + "spring": "For\u00e5r", + "summer": "Sommer", + "winter": "Vinter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.hu.json b/homeassistant/components/sensor/.translations/season.hu.json new file mode 100644 index 00000000000..63596b09784 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.hu.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u0150sz", + "spring": "Tavasz", + "summer": "Ny\u00e1r", + "winter": "T\u00e9l" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.it.json b/homeassistant/components/sensor/.translations/season.it.json new file mode 100644 index 00000000000..d9138f6b16e --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Autunno", + "spring": "Primavera", + "summer": "Estate", + "winter": "Inverno" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.lb.json b/homeassistant/components/sensor/.translations/season.lb.json new file mode 100644 index 00000000000..f33afde7a07 --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.lb.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Hierscht", + "spring": "Fr\u00e9ijoer", + "summer": "Summer", + "winter": "Wanter" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.ru.json b/homeassistant/components/sensor/.translations/season.ru.json new file mode 100644 index 00000000000..2b04886b72d --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "\u041e\u0441\u0435\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0435\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/cy.json b/homeassistant/components/zone/.translations/cy.json new file mode 100644 index 00000000000..e34fae81b61 --- /dev/null +++ b/homeassistant/components/zone/.translations/cy.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Enw eisoes yn bodoli" + }, + "step": { + "init": { + "data": { + "icon": "Eicon", + "latitude": "Lledred", + "longitude": "Hydred", + "name": "Enw", + "passive": "Goddefol", + "radius": "Radiws" + }, + "title": "Ddiffinio paramedrau parth" + } + }, + "title": "Parth" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/de.json b/homeassistant/components/zone/.translations/de.json new file mode 100644 index 00000000000..fc1e3537f33 --- /dev/null +++ b/homeassistant/components/zone/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "init": { + "data": { + "icon": "Symbol", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definieren Sie die Zonenparameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/en.json b/homeassistant/components/zone/.translations/en.json index ff2c7c07c14..1faf0110a53 100644 --- a/homeassistant/components/zone/.translations/en.json +++ b/homeassistant/components/zone/.translations/en.json @@ -1,21 +1,21 @@ { "config": { - "title": "Zone", - "step": { - "init": { - "title": "Define zone parameters", - "data": { - "name": "Name", - "latitude": "Latitude", - "longitude": "Longitude", - "radius": "Radius", - "passive": "Passive", - "icon": "Icon" - } - } - }, "error": { "name_exists": "Name already exists" - } + }, + "step": { + "init": { + "data": { + "icon": "Icon", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name", + "passive": "Passive", + "radius": "Radius" + }, + "title": "Define zone parameters" + } + }, + "title": "Zone" } } \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ko.json b/homeassistant/components/zone/.translations/ko.json new file mode 100644 index 00000000000..364f8f3cc77 --- /dev/null +++ b/homeassistant/components/zone/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "icon": "\uc544\uc774\ucf58", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984", + "passive": "\uc790\ub3d9\ud654 \uc804\uc6a9", + "radius": "\ubc18\uacbd" + }, + "title": "\uad6c\uc5ed \ub9e4\uac1c \ubcc0\uc218 \uc815\uc758" + } + }, + "title": "\uad6c\uc5ed" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/lb.json b/homeassistant/components/zone/.translations/lb.json new file mode 100644 index 00000000000..10b65bcca30 --- /dev/null +++ b/homeassistant/components/zone/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "init": { + "data": { + "icon": "Ikone", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm", + "passive": "Passif", + "radius": "Radius" + }, + "title": "D\u00e9fin\u00e9iert Zone Parameter" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/nl.json b/homeassistant/components/zone/.translations/nl.json new file mode 100644 index 00000000000..6dcf565ada6 --- /dev/null +++ b/homeassistant/components/zone/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Naam bestaat al" + }, + "step": { + "init": { + "data": { + "icon": "Pictogram", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam", + "passive": "Passief", + "radius": "Straal" + }, + "title": "Definieer zone parameters" + } + }, + "title": "Zone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/no.json b/homeassistant/components/zone/.translations/no.json new file mode 100644 index 00000000000..3c1a91976f0 --- /dev/null +++ b/homeassistant/components/zone/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Navnet eksisterer allerede" + }, + "step": { + "init": { + "data": { + "icon": "Ikon", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn", + "passive": "Passiv", + "radius": "Radius" + }, + "title": "Definer sone parametere" + } + }, + "title": "Sone" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pl.json b/homeassistant/components/zone/.translations/pl.json new file mode 100644 index 00000000000..e649de4c75e --- /dev/null +++ b/homeassistant/components/zone/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Nazwa ju\u017c istnieje" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa", + "passive": "Pasywnie", + "radius": "Promie\u0144" + }, + "title": "Zdefiniuj parametry strefy" + } + }, + "title": "Strefa" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/pt.json b/homeassistant/components/zone/.translations/pt.json new file mode 100644 index 00000000000..a4ced557805 --- /dev/null +++ b/homeassistant/components/zone/.translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "Nome j\u00e1 existente" + }, + "step": { + "init": { + "data": { + "icon": "\u00cdcone", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nome", + "passive": "Passivo", + "radius": "Raio" + } + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json new file mode 100644 index 00000000000..f0619f2163c --- /dev/null +++ b/homeassistant/components/zone/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + }, + "step": { + "init": { + "data": { + "icon": "\u0417\u043d\u0430\u0447\u043e\u043a", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u0430\u044f", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0437\u043e\u043d\u044b" + } + }, + "title": "\u0417\u043e\u043d\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/.translations/zh-Hans.json b/homeassistant/components/zone/.translations/zh-Hans.json new file mode 100644 index 00000000000..6d06b68dad8 --- /dev/null +++ b/homeassistant/components/zone/.translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728" + }, + "step": { + "init": { + "data": { + "icon": "\u56fe\u6807", + "latitude": "\u7eac\u5ea6", + "longitude": "\u7ecf\u5ea6", + "name": "\u540d\u79f0", + "passive": "\u88ab\u52a8", + "radius": "\u534a\u5f84" + }, + "title": "\u5b9a\u4e49\u533a\u57df\u76f8\u5173\u53d8\u91cf" + } + }, + "title": "\u533a\u57df" + } +} \ No newline at end of file From 83e342daf2b2d71170b227a30883b85e2be94746 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:35:42 -0400 Subject: [PATCH 619/924] Update frontend to 20180505.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 58cea0e0c66..b4eb6df07e1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180426.0'] +REQUIREMENTS = ['home-assistant-frontend==20180505.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index e9033091f3e..e74070cb98c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180426.0 +home-assistant-frontend==20180505.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df0f5722b86..939e4314718 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.1 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180426.0 +home-assistant-frontend==20180505.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 1e31af77de90c9b3e89cb5e6f7c15334b05b3c46 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:41:55 -0400 Subject: [PATCH 620/924] Version bump to 0.69.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 30c73546cf7..7462710ea23 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From f3411f8db29ad8b07afe4696e4baf9eadd5a450c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:42:32 -0400 Subject: [PATCH 621/924] Version bump to 0.70.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 30c73546cf7..37e0c32ca03 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 69 +MINOR_VERSION = 70 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 2bb1a95098f7fef0e9276c0ec4d4c6fa4a2bea19 Mon Sep 17 00:00:00 2001 From: thepotoo <31549428+thepotoo@users.noreply.github.com> Date: Sun, 6 May 2018 02:21:02 -0400 Subject: [PATCH 622/924] Add unique_id to MQTT switch (#13719) --- homeassistant/components/switch/mqtt.py | 13 ++++++++++++- tests/components/switch/test_mqtt.py | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 69f12536c5f..1075888e199 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -29,12 +30,14 @@ DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False +CONF_UNIQUE_ID = 'unique_id' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -62,6 +65,7 @@ async def async_setup_platform(hass, config, async_add_devices, config.get(CONF_OPTIMISTIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_UNIQUE_ID), value_template, )]) @@ -72,7 +76,8 @@ class MqttSwitch(MqttAvailability, SwitchDevice): def __init__(self, name, icon, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, - payload_available, payload_not_available, value_template): + payload_available, payload_not_available, + unique_id: Optional[str], value_template): """Initialize the MQTT switch.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -87,6 +92,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._payload_off = payload_off self._optimistic = optimistic self._template = value_template + self._unique_id = unique_id async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -139,6 +145,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return true if we do optimistic updates.""" return self._optimistic + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the icon.""" diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index b5e2a0b0395..24db0540012 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -248,3 +248,26 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) + + def test_unique_id(self): + """Test unique id option only creates one switch per unique_id.""" + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + fire_mqtt_message(self.hass, 'test-topic', 'payload') + self.hass.block_till_done() + assert len(self.hass.states.async_entity_ids()) == 2 + # all switches group is 1, unique id created is 1 From 63cc179ea23a8e60091a0064388843f805db9642 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sun, 6 May 2018 02:17:05 -0700 Subject: [PATCH 623/924] zha: Bump to zigpy 0.1.0 (#14305) --- homeassistant/components/zha/__init__.py | 6 +++--- requirements_all.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 9b66c4c6ded..9d7556fc334 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -16,9 +16,9 @@ from homeassistant.helpers import discovery, entity from homeassistant.util import slugify REQUIREMENTS = [ - 'bellows==0.5.2', - 'zigpy==0.0.3', - 'zigpy-xbee==0.0.2', + 'bellows==0.6.0', + 'zigpy==0.1.0', + 'zigpy-xbee==0.1.0', ] DOMAIN = 'zha' diff --git a/requirements_all.txt b/requirements_all.txt index e74070cb98c..5162feb6bd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -146,7 +146,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.5.2 +bellows==0.6.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.0 @@ -1382,7 +1382,7 @@ zeroconf==0.20.0 ziggo-mediabox-xl==1.0.0 # homeassistant.components.zha -zigpy-xbee==0.0.2 +zigpy-xbee==0.1.0 # homeassistant.components.zha -zigpy==0.0.3 +zigpy==0.1.0 From 107769ab8198bd8e63e821222ec77ece78f38a74 Mon Sep 17 00:00:00 2001 From: Justin Loutsenhizer Date: Sun, 6 May 2018 13:18:26 -0400 Subject: [PATCH 624/924] Add missing 'sensor' to ABODE_PLATFORMS (#14313) This fixes missing light, humidity, temperature sensors from abode component. --- homeassistant/components/abode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 2f56bb7c2b5..6d5feb87dc2 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({ ABODE_PLATFORMS = [ 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light' + 'camera', 'light', 'sensor' ] From 34727be5ac23d6d0a2a4585c76ea6a5b56726531 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 May 2018 20:54:56 -0400 Subject: [PATCH 625/924] Fix module names for custom components (#14317) * Fix module names for custom components * Also set __package__ correctly * bla * Remove print --- homeassistant/loader.py | 23 +++++++++++++++---- tests/test_loader.py | 18 +++++++++++++-- .../test_package/__init__.py | 2 +- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 322870952f2..b6dabb1d883 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -73,13 +73,15 @@ def get_component(hass, comp_or_platform): # Try custom component module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - comp_or_platform) + PATH_CUSTOM_COMPONENTS, comp_or_platform) if module is None: try: module = importlib.import_module( '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) + _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) except ImportError: + _LOGGER.warning('Unable to find %s', comp_or_platform) module = None cache = hass.data.get(DATA_KEY) @@ -102,18 +104,20 @@ def _find_spec(path, name): return None -def _load_module(path, name): +def _load_module(path, base_module, name): """Load a module based on a folder and a name.""" + mod_name = "{}.{}".format(base_module, name) spec = _find_spec([path], name) # Special handling if loading platforms and the folder is a namespace # (namespace is a folder without __init__.py) if spec is None and '.' in name: - parent_spec = _find_spec([path], name.split('.')[0]) + mod_parent_name = name.split('.')[0] + parent_spec = _find_spec([path], mod_parent_name) if (parent_spec is None or parent_spec.submodule_search_locations is None): return None - spec = _find_spec(parent_spec.submodule_search_locations, name) + spec = _find_spec(parent_spec.submodule_search_locations, mod_name) # Not found if spec is None: @@ -123,8 +127,19 @@ def _load_module(path, name): if spec.loader is None: return None + _LOGGER.debug('Loaded %s (%s)', name, base_module) + module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + # A hack, I know. Don't currently know how to work around it. + if not module.__name__.startswith(base_module): + module.__name__ = "{}.{}".format(base_module, name) + + if not module.__package__: + module.__package__ = base_module + elif not module.__package__.startswith(base_module): + module.__package__ = "{}.{}".format(base_module, name) + return module diff --git a/tests/test_loader.py b/tests/test_loader.py index 646526e94ea..e8a79c6501f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -30,8 +30,7 @@ class TestLoader(unittest.TestCase): comp = object() loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(comp, - loader.get_component(self.hass, 'switch.test_set')) + assert loader.get_component(self.hass, 'switch.test_set') is comp def test_get_component(self): """Test if get_component works.""" @@ -106,3 +105,18 @@ def test_helpers_wrapper(hass): yield from hass.async_block_till_done() assert result == ['hello'] + + +async def test_custom_component_name(hass): + """Test the name attribte of custom components.""" + comp = loader.get_component(hass, 'test_standalone') + assert comp.__name__ == 'custom_components.test_standalone' + assert comp.__package__ == 'custom_components' + + comp = loader.get_component(hass, 'test_package') + assert comp.__name__ == 'custom_components.test_package' + assert comp.__package__ == 'custom_components.test_package' + + comp = loader.get_component(hass, 'light.test') + assert comp.__name__ == 'custom_components.light.test' + assert comp.__package__ == 'custom_components.light' diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 528f056948b..ee669c6c9b5 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -2,6 +2,6 @@ DOMAIN = 'test_package' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True From 91fe6e4e5611f5b385f2bb5373400cc42817515c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 7 May 2018 02:55:38 +0200 Subject: [PATCH 626/924] Add debounce to move_cover (#14314) * Add debounce to move_cover * Fix spelling mistake --- .../components/homekit/type_covers.py | 4 ++- .../homekit/type_security_systems.py | 2 +- tests/components/homekit/test_type_covers.py | 34 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b30109f711d..3de87cf63e8 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, @@ -80,6 +80,7 @@ class WindowCovering(HomeAccessory): self.char_target_position = serv_cover.configure_char( CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) @@ -122,6 +123,7 @@ class WindowCoveringBasic(HomeAccessory): self.char_position_state = serv_cover.configure_char( CHAR_POSITION_STATE, value=2) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index e32860d1fef..ab16f921e99 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -67,7 +67,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - # SecuritySystemTargetSTate does not support triggered + # SecuritySystemTargetState does not support triggered if not self.flag_target_state and \ hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 2dcb48a4d4c..313d58e78fd 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,19 +4,35 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) -from homeassistant.components.homekit.type_covers import ( - GarageDoorOpener, WindowCovering, WindowCoveringBasic) from homeassistant.const import ( STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, ATTR_SUPPORTED_FEATURES) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitSensors(unittest.TestCase): +class TestHomekitCovers(unittest.TestCase): """Test class for all accessory types regarding covers.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + cls.garage_cls = _import.GarageDoorOpener + cls.window_cls = _import.WindowCovering + cls.window_basic_cls = _import.WindowCoveringBasic + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +53,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" garage_door = 'cover.garage_door' - acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None) + acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -95,7 +111,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None) + acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -146,8 +162,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -214,8 +230,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() # Set from HomeKit From e60d0665141e94683bb3ba788d5cfaebb0b719d7 Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Mon, 7 May 2018 00:35:55 -0700 Subject: [PATCH 627/924] Converted SABnzbd to a component (#12915) * Converted SABnzbd to a component * fixed async issues * Made sabnzbd scan interval static. More async fixes. * Sabnzbd component code cleanup * Skip sensor platform setup if discovery_info is None --- .coveragerc | 4 +- homeassistant/components/discovery.py | 3 +- homeassistant/components/sabnzbd.py | 254 +++++++++++++++++++++ homeassistant/components/sensor/sabnzbd.py | 213 +++-------------- requirements_all.txt | 2 +- 5 files changed, 296 insertions(+), 180 deletions(-) create mode 100644 homeassistant/components/sabnzbd.py diff --git a/.coveragerc b/.coveragerc index d2192ca2e46..9030cc9a097 100644 --- a/.coveragerc +++ b/.coveragerc @@ -226,6 +226,9 @@ omit = homeassistant/components/rpi_pfio.py homeassistant/components/*/rpi_pfio.py + homeassistant/components/sabnzbd.py + homeassistant/components/*/sabnzbd.py + homeassistant/components/satel_integra.py homeassistant/components/*/satel_integra.py @@ -650,7 +653,6 @@ omit = homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/ripple.py - homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 07eb5aaab82..46ac58d43b1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -39,6 +39,7 @@ SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' @@ -59,6 +60,7 @@ SERVICE_HANDLERS = { SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), @@ -74,7 +76,6 @@ SERVICE_HANDLERS = { 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), 'harmony': ('remote', 'harmony'), - 'sabnzbd': ('sensor', 'sabnzbd'), 'bose_soundtouch': ('media_player', 'soundtouch'), 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py new file mode 100644 index 00000000000..a7b33b4c697 --- /dev/null +++ b/homeassistant/components/sabnzbd.py @@ -0,0 +1,254 @@ +""" +Support for monitoring an SABnzbd NZB client. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sabnzbd/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.discovery import SERVICE_SABNZBD +from homeassistant.const import ( + CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.json import load_json, save_json + +REQUIREMENTS = ['pysabnzbd==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'sabnzbd' +DATA_SABNZBD = 'sabznbd' + +_CONFIGURING = {} + +ATTR_SPEED = 'speed' +BASE_URL_FORMAT = '{}://{}:{}/' +CONFIG_FILE = 'sabnzbd.conf' +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'SABnzbd' +DEFAULT_PORT = 8080 +DEFAULT_SPEED_LIMIT = '100' +DEFAULT_SSL = False + +UPDATE_INTERVAL = timedelta(seconds=30) + +SERVICE_PAUSE = 'pause' +SERVICE_RESUME = 'resume' +SERVICE_SET_SPEED = 'set_speed' + +SIGNAL_SABNZBD_UPDATED = 'sabnzbd_updated' + +SENSOR_TYPES = { + 'current_status': ['Status', None, 'status'], + 'speed': ['Speed', 'MB/s', 'kbpersec'], + 'queue_size': ['Queue', 'MB', 'mb'], + 'queue_remaining': ['Left', 'MB', 'mbleft'], + 'disk_size': ['Disk', 'GB', 'diskspacetotal1'], + 'disk_free': ['Disk Free', 'GB', 'diskspace1'], + 'queue_count': ['Queue Count', None, 'noofslots_total'], + 'day_size': ['Daily Total', 'GB', 'day_size'], + 'week_size': ['Weekly Total', 'GB', 'week_size'], + 'month_size': ['Monthly Total', 'GB', 'month_size'], + 'total_size': ['Total', 'GB', 'total_size'], +} + +SPEED_LIMIT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_check_sabnzbd(sab_api): + """Check if we can reach SABnzbd.""" + from pysabnzbd import SabnzbdApiException + + try: + await sab_api.check_available() + return True + except SabnzbdApiException: + _LOGGER.error("Connection to SABnzbd API failed") + return False + + +async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, + api_key=None): + """Try to configure Sabnzbd and request api key if configuration fails.""" + from pysabnzbd import SabnzbdApi + + host = config[CONF_HOST] + port = config[CONF_PORT] + uri_scheme = 'https' if use_ssl else 'http' + base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) + if api_key is None: + conf = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) + api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') + + sab_api = SabnzbdApi(base_url, api_key) + if await async_check_sabnzbd(sab_api): + async_setup_sabnzbd(hass, sab_api, config, name) + else: + async_request_configuration(hass, config, base_url) + + +async def async_setup(hass, config): + """Setup the SABnzbd component.""" + async def sabnzbd_discovered(service, info): + """Handle service discovery.""" + ssl = info.get('properties', {}).get('https', '0') == '1' + await async_configure_sabnzbd(hass, info, ssl) + + discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) + + conf = config.get(DOMAIN) + if conf is not None: + use_ssl = conf.get(CONF_SSL) + name = conf.get(CONF_NAME) + api_key = conf.get(CONF_API_KEY) + await async_configure_sabnzbd(hass, conf, use_ssl, name, api_key) + return True + + +@callback +def async_setup_sabnzbd(hass, sab_api, config, name): + """Setup SABnzbd sensors and services.""" + sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {})) + + if config.get(CONF_SENSORS): + hass.data[DATA_SABNZBD] = sab_api_data + hass.async_add_job( + discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) + + async def async_service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + await sab_api_data.async_pause_queue() + elif service.service == SERVICE_RESUME: + await sab_api_data.async_resume_queue() + elif service.service == SERVICE_SET_SPEED: + speed = service.data.get(ATTR_SPEED) + await sab_api_data.async_set_queue_speed(speed) + + hass.services.async_register(DOMAIN, SERVICE_PAUSE, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_RESUME, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_SET_SPEED, + async_service_handler, + schema=SPEED_LIMIT_SCHEMA) + + async def async_update_sabnzbd(now): + """Refresh SABnzbd queue data.""" + from pysabnzbd import SabnzbdApiException + try: + await sab_api.refresh_data() + async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) + except SabnzbdApiException as err: + _LOGGER.error(err) + + async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + + +@callback +def async_request_configuration(hass, config, host): + """Request configuration steps from the user.""" + from pysabnzbd import SabnzbdApi + + configurator = hass.components.configurator + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.async_notify_errors( + _CONFIGURING[host], + 'Failed to register, please try again.') + + return + + async def async_configuration_callback(data): + """Handle configuration changes.""" + api_key = data.get(CONF_API_KEY) + sab_api = SabnzbdApi(host, api_key) + if not await async_check_sabnzbd(sab_api): + return + + def success(): + """Setup was successful.""" + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {CONF_API_KEY: api_key} + save_json(hass.config.path(CONFIG_FILE), conf) + req_config = _CONFIGURING.pop(host) + configurator.request_done(req_config) + + hass.async_add_job(success) + async_setup_sabnzbd(hass, sab_api, config, + config.get(CONF_NAME, DEFAULT_NAME)) + + _CONFIGURING[host] = configurator.async_request_config( + DEFAULT_NAME, + async_configuration_callback, + description='Enter the API Key', + submit_caption='Confirm', + fields=[{'id': CONF_API_KEY, 'name': 'API Key', 'type': ''}] + ) + + +class SabnzbdApiData: + """Class for storing/refreshing sabnzbd api queue data.""" + + def __init__(self, sab_api, name, sensors): + """Initialize component.""" + self.sab_api = sab_api + self.name = name + self.sensors = sensors + + async def async_pause_queue(self): + """Pause Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.pause_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_resume_queue(self): + """Resume Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.resume_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_set_queue_speed(self, limit): + """Set speed limit for the Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.set_speed_limit(limit) + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + def get_queue_field(self, field): + """Return the value for the given field from the Sabnzbd queue.""" + return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 194ff71222a..185f83c9405 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -4,216 +4,75 @@ Support for monitoring an SABnzbd NZB client. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sabnzbd/ """ -import asyncio import logging -from datetime import timedelta -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, - CONF_SSL) +from homeassistant.components.sabnzbd import DATA_SABNZBD, \ + SIGNAL_SABNZBD_UPDATED, SENSOR_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util.json import load_json, save_json -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysabnzbd==1.0.1'] +DEPENDENCIES = ['sabnzbd'] -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -CONFIG_FILE = 'sabnzbd.conf' -DEFAULT_NAME = 'SABnzbd' -DEFAULT_PORT = 8080 -DEFAULT_SSL = False - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) - -SENSOR_TYPES = { - 'current_status': ['Status', None], - 'speed': ['Speed', 'MB/s'], - 'queue_size': ['Queue', 'MB'], - 'queue_remaining': ['Left', 'MB'], - 'disk_size': ['Disk', 'GB'], - 'disk_free': ['Disk Free', 'GB'], - 'queue_count': ['Queue Count', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=['current_status']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, -}) - - -@asyncio.coroutine -def async_check_sabnzbd(sab_api, base_url, api_key): - """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException - sab_api = sab_api(base_url, api_key) - - try: - yield from sab_api.check_available() - except SabnzbdApiException: - _LOGGER.error("Connection to SABnzbd API failed") - return False - return True - - -def setup_sabnzbd(base_url, apikey, name, config, - async_add_devices, sab_api): - """Set up polling from SABnzbd and sensors.""" - sab_api = sab_api(base_url, apikey) - monitored = config.get(CONF_MONITORED_VARIABLES) - async_add_devices([SabnzbdSensor(variable, sab_api, name) - for variable in monitored]) - - -@Throttle(MIN_TIME_BETWEEN_UPDATES) -async def async_update_queue(sab_api): - """ - Throttled function to update SABnzbd queue. - - This ensures that the queue info only gets updated once for all sensors - """ - await sab_api.refresh_data() - - -def request_configuration(host, name, hass, config, async_add_devices, - sab_api): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors(_CONFIGURING[host], - 'Failed to register, please try again.') +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the SABnzbd sensors.""" + if discovery_info is None: return - @asyncio.coroutine - def async_configuration_callback(data): - """Handle configuration changes.""" - api_key = data.get('api_key') - if (yield from async_check_sabnzbd(sab_api, host, api_key)): - setup_sabnzbd(host, api_key, name, config, - async_add_devices, sab_api) - - def success(): - """Set up was successful.""" - conf = load_json(hass.config.path(CONFIG_FILE)) - conf[host] = {'api_key': api_key} - save_json(hass.config.path(CONFIG_FILE), conf) - req_config = _CONFIGURING.pop(host) - configurator.async_request_done(req_config) - - hass.async_add_job(success) - - _CONFIGURING[host] = configurator.async_request_config( - DEFAULT_NAME, - async_configuration_callback, - description='Enter the API Key', - submit_caption='Confirm', - fields=[{'id': 'api_key', 'name': 'API Key', 'type': ''}] - ) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the SABnzbd platform.""" - from pysabnzbd import SabnzbdApi - - if discovery_info is not None: - host = discovery_info.get(CONF_HOST) - port = discovery_info.get(CONF_PORT) - name = DEFAULT_NAME - use_ssl = discovery_info.get('properties', {}).get('https', '0') == '1' - else: - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME, DEFAULT_NAME) - use_ssl = config.get(CONF_SSL) - - api_key = config.get(CONF_API_KEY) - - uri_scheme = 'https://' if use_ssl else 'http://' - base_url = "{}{}:{}/".format(uri_scheme, host, port) - - if not api_key: - conf = load_json(hass.config.path(CONFIG_FILE)) - if conf.get(base_url, {}).get('api_key'): - api_key = conf[base_url]['api_key'] - - if not (yield from async_check_sabnzbd(SabnzbdApi, base_url, api_key)): - request_configuration(base_url, name, hass, config, - async_add_devices, SabnzbdApi) - return - - setup_sabnzbd(base_url, api_key, name, config, - async_add_devices, SabnzbdApi) + sab_api_data = hass.data[DATA_SABNZBD] + sensors = sab_api_data.sensors + client_name = sab_api_data.name + async_add_devices([SabnzbdSensor(sensor, sab_api_data, client_name) + for sensor in sensors]) class SabnzbdSensor(Entity): """Representation of an SABnzbd sensor.""" - def __init__(self, sensor_type, sabnzbd_api, client_name): + def __init__(self, sensor_type, sabnzbd_api_data, client_name): """Initialize the sensor.""" + self._client_name = client_name + self._field_name = SENSOR_TYPES[sensor_type][2] self._name = SENSOR_TYPES[sensor_type][0] - self.sabnzbd_api = sabnzbd_api - self.type = sensor_type - self.client_name = client_name + self._sabnzbd_api = sabnzbd_api_data self._state = None + self._type = sensor_type self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + async_dispatcher_connect(self.hass, SIGNAL_SABNZBD_UPDATED, + self.update_state) + @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return '{} {}'.format(self._client_name, self._name) @property def state(self): """Return the state of the sensor.""" return self._state + def should_poll(self): + """Don't poll. Will be updated by dispatcher signal.""" + return False + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @asyncio.coroutine - def async_refresh_sabnzbd_data(self): - """Call the throttled SABnzbd refresh method.""" - from pysabnzbd import SabnzbdApiException - try: - yield from async_update_queue(self.sabnzbd_api) - except SabnzbdApiException: - _LOGGER.exception("Connection to SABnzbd API failed") - - @asyncio.coroutine - def async_update(self): + def update_state(self, args): """Get the latest data and updates the states.""" - yield from self.async_refresh_sabnzbd_data() + self._state = self._sabnzbd_api.get_queue_field(self._field_name) - if self.sabnzbd_api.queue: - if self.type == 'current_status': - self._state = self.sabnzbd_api.queue.get('status') - elif self.type == 'speed': - mb_spd = float(self.sabnzbd_api.queue.get('kbpersec')) / 1024 - self._state = round(mb_spd, 1) - elif self.type == 'queue_size': - self._state = self.sabnzbd_api.queue.get('mb') - elif self.type == 'queue_remaining': - self._state = self.sabnzbd_api.queue.get('mbleft') - elif self.type == 'disk_size': - self._state = self.sabnzbd_api.queue.get('diskspacetotal1') - elif self.type == 'disk_free': - self._state = self.sabnzbd_api.queue.get('diskspace1') - elif self.type == 'queue_count': - self._state = self.sabnzbd_api.queue.get('noofslots_total') - else: - self._state = 'Unknown' + if self._type == 'speed': + self._state = round(float(self._state) / 1024, 1) + elif 'size' in self._type: + self._state = round(float(self._state), 2) + + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 5162feb6bd2..79c7e38efbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -909,7 +909,7 @@ pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 -# homeassistant.components.sensor.sabnzbd +# homeassistant.components.sabnzbd pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo From 5c95c53c6c33e22eacc742e7b9761cc1b0ed7d03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 05:25:48 -0400 Subject: [PATCH 628/924] Revert custom component loading logic (#14327) * Revert custom component loading logic * Lint * Fix tests * Guard for infinite inserts into sys.path --- homeassistant/loader.py | 114 +++++++----------- tests/components/notify/test_file.py | 46 ++++--- tests/test_loader.py | 4 + .../image_processing/test.py | 6 +- .../custom_components/light/test.py | 5 +- .../custom_components/switch/test.py | 5 +- .../test_package/__init__.py | 3 + .../custom_components/test_package/const.py | 2 + 8 files changed, 84 insertions(+), 101 deletions(-) create mode 100644 tests/testing_config/custom_components/test_package/const.py diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b6dabb1d883..e94fb2d6833 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -31,12 +31,6 @@ PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) -# List of available components -AVAILABLE_COMPONENTS = [] # type: List[str] - -# Dict of loaded components mapped name => module -_COMPONENT_CACHE = {} # type: Dict[str, ModuleType] - _LOGGER = logging.getLogger(__name__) @@ -64,85 +58,63 @@ def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(hass, comp_or_platform): - """Load a module from either custom component or built-in.""" +def get_component(hass, comp_or_platform) -> Optional[ModuleType]: + """Try to load specified component. + + Looks in config dir first, then built-in components. + Only returns it if also found to be valid. + Async friendly. + """ try: return hass.data[DATA_KEY][comp_or_platform] except KeyError: pass - # Try custom component - module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - PATH_CUSTOM_COMPONENTS, comp_or_platform) - - if module is None: - try: - module = importlib.import_module( - '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) - _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) - except ImportError: - _LOGGER.warning('Unable to find %s', comp_or_platform) - module = None - cache = hass.data.get(DATA_KEY) if cache is None: + # Only insert if it's not there (happens during tests) + if sys.path[0] != hass.config.config_dir: + sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - cache[comp_or_platform] = module - return module + # First check custom, then built-in + potential_paths = ['custom_components.{}'.format(comp_or_platform), + 'homeassistant.components.{}'.format(comp_or_platform)] - -def _find_spec(path, name): - for finder in sys.meta_path: + for path in potential_paths: try: - spec = finder.find_spec(name, path=path) - if spec is not None: - return spec - except AttributeError: - # Not all finders have the find_spec method - pass + module = importlib.import_module(path) + + # In Python 3 you can import files from directories that do not + # contain the file __init__.py. A directory is a valid module if + # it contains a file with the .py extension. In this case Python + # will succeed in importing the directory as a module and call it + # a namespace. We do not care about namespaces. + # This prevents that when only + # custom_components/switch/some_platform.py exists, + # the import custom_components.switch would succeed. + if module.__spec__.origin == 'namespace': + continue + + _LOGGER.info("Loaded %s from %s", comp_or_platform, path) + + cache[comp_or_platform] = module + + return module + + except ImportError as err: + # This error happens if for example custom_components/switch + # exists and we try to load switch.demo. + if str(err) != "No module named '{}'".format(path): + _LOGGER.exception( + ("Error loading %s. Make sure all " + "dependencies are installed"), path) + + _LOGGER.error("Unable to find component %s", comp_or_platform) + return None -def _load_module(path, base_module, name): - """Load a module based on a folder and a name.""" - mod_name = "{}.{}".format(base_module, name) - spec = _find_spec([path], name) - - # Special handling if loading platforms and the folder is a namespace - # (namespace is a folder without __init__.py) - if spec is None and '.' in name: - mod_parent_name = name.split('.')[0] - parent_spec = _find_spec([path], mod_parent_name) - if (parent_spec is None or - parent_spec.submodule_search_locations is None): - return None - spec = _find_spec(parent_spec.submodule_search_locations, mod_name) - - # Not found - if spec is None: - return None - - # This is a namespace - if spec.loader is None: - return None - - _LOGGER.debug('Loaded %s (%s)', name, base_module) - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - # A hack, I know. Don't currently know how to work around it. - if not module.__name__.startswith(base_module): - module.__name__ = "{}.{}".format(base_module, name) - - if not module.__package__: - module.__package__ = base_module - elif not module.__package__.startswith(base_module): - module.__package__ = "{}.{}".format(base_module, name) - - return module - - class Components: """Helper to load components.""" diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 42b9eb9d82d..c5064fca851 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -35,28 +35,30 @@ class TestNotifyFile(unittest.TestCase): assert setup_component(self.hass, notify.DOMAIN, config) assert not handle_config[notify.DOMAIN] - def _test_notify_file(self, timestamp, mock_utcnow, mock_stat): + def _test_notify_file(self, timestamp): """Test the notify file output.""" - mock_utcnow.return_value = dt_util.as_utc(dt_util.now()) - mock_stat.return_value.st_size = 0 + filename = 'mock_file' + message = 'one, two, testing, testing' + with assert_setup_component(1) as handle_config: + self.assertTrue(setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'file', + 'filename': filename, + 'timestamp': timestamp, + } + })) + assert handle_config[notify.DOMAIN] m_open = mock_open() with patch( 'homeassistant.components.notify.file.open', m_open, create=True - ): - filename = 'mock_file' - message = 'one, two, testing, testing' - with assert_setup_component(1) as handle_config: - self.assertTrue(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - 'filename': filename, - 'timestamp': timestamp, - } - })) - assert handle_config[notify.DOMAIN] + ), patch('homeassistant.components.notify.file.os.stat') as mock_st, \ + patch('homeassistant.util.dt.utcnow', + return_value=dt_util.utcnow()): + + mock_st.return_value.st_size = 0 title = '{} notifications (Log started: {})\n{}\n'.format( ATTR_TITLE_DEFAULT, dt_util.utcnow().isoformat(), @@ -82,14 +84,10 @@ class TestNotifyFile(unittest.TestCase): dt_util.utcnow().isoformat(), message))] ) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file(self, mock_utcnow, mock_stat): + def test_notify_file(self): """Test the notify file output without timestamp.""" - self._test_notify_file(False, mock_utcnow, mock_stat) + self._test_notify_file(False) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file_timestamp(self, mock_utcnow, mock_stat): + def test_notify_file_timestamp(self): """Test the notify file output with timestamp.""" - self._test_notify_file(True, mock_utcnow, mock_stat) + self._test_notify_file(True) diff --git a/tests/test_loader.py b/tests/test_loader.py index e8a79c6501f..c97e94a7ce1 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -120,3 +120,7 @@ async def test_custom_component_name(hass): comp = loader.get_component(hass, 'light.test') assert comp.__name__ == 'custom_components.light.test' assert comp.__package__ == 'custom_components.light' + + # Test custom components is mounted + from custom_components.test_package import TEST + assert TEST == 5 diff --git a/tests/testing_config/custom_components/image_processing/test.py b/tests/testing_config/custom_components/image_processing/test.py index 29d362699f5..b50050ed68e 100644 --- a/tests/testing_config/custom_components/image_processing/test.py +++ b/tests/testing_config/custom_components/image_processing/test.py @@ -3,9 +3,11 @@ from homeassistant.components.image_processing import ImageProcessingEntity -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Set up the test image_processing platform.""" - add_devices([TestImageProcessing('camera.demo_camera', "Test")]) + async_add_devices_callback([ + TestImageProcessing('camera.demo_camera', "Test")]) class TestImageProcessing(ImageProcessingEntity): diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index 71625dfdf93..fbf79f9e770 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Return mock devices.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/switch/test.py b/tests/testing_config/custom_components/switch/test.py index 2819f2f2951..79126b7b52a 100644 --- a/tests/testing_config/custom_components/switch/test.py +++ b/tests/testing_config/custom_components/switch/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Find and return test switches.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index ee669c6c9b5..85e78a7f9d6 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,4 +1,7 @@ """Provide a mock package component.""" +from .const import TEST # noqa + + DOMAIN = 'test_package' diff --git a/tests/testing_config/custom_components/test_package/const.py b/tests/testing_config/custom_components/test_package/const.py new file mode 100644 index 00000000000..7e13e04cb47 --- /dev/null +++ b/tests/testing_config/custom_components/test_package/const.py @@ -0,0 +1,2 @@ +"""Constants for test_package custom component.""" +TEST = 5 From a2b8ad50f25fdfe21814556452c8e7b1d1262ec1 Mon Sep 17 00:00:00 2001 From: Javier Gonel Date: Mon, 7 May 2018 16:52:33 +0300 Subject: [PATCH 629/924] fix(hbmqtt): partial packets breaking hbmqtt (#14329) This issue was fixed in hbmqtt/issues#95 that was released in hbmqtt 0.9.2 --- homeassistant/components/mqtt/server.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index db251ab4180..8a012928792 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.1'] +REQUIREMENTS = ['hbmqtt==0.9.2'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config diff --git a/requirements_all.txt b/requirements_all.txt index 79c7e38efbb..342bbebb08f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ ha-philipsjs==0.0.3 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 939e4314718..976f4d87280 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ ha-ffmpeg==1.9 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.binary_sensor.workday holidays==0.9.5 From 6318178a8b211666b1ac00c93747af6640fa5915 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 10:00:54 -0400 Subject: [PATCH 630/924] Update netdisco to 1.4.1 --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 46ac58d43b1..68cf293ce48 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.4.0'] +REQUIREMENTS = ['netdisco==1.4.1'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index 342bbebb08f..66b011e0440 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.4.0 +netdisco==1.4.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From 5c88e897af38c69fc188ef2dfbfd62372f522088 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 10:00:54 -0400 Subject: [PATCH 631/924] Update netdisco to 1.4.1 --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 07eb5aaab82..65d0a1c76f3 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.4.0'] +REQUIREMENTS = ['netdisco==1.4.1'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index e74070cb98c..735f39a8013 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.4.0 +netdisco==1.4.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From ab621808bd3ddeae5055545c906ebf593dbf6789 Mon Sep 17 00:00:00 2001 From: Justin Loutsenhizer Date: Sun, 6 May 2018 13:18:26 -0400 Subject: [PATCH 632/924] Add missing 'sensor' to ABODE_PLATFORMS (#14313) This fixes missing light, humidity, temperature sensors from abode component. --- homeassistant/components/abode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 2f56bb7c2b5..6d5feb87dc2 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({ ABODE_PLATFORMS = [ 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light' + 'camera', 'light', 'sensor' ] From c48986a4674466d437cc7a1f672ac465c0189ce6 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 7 May 2018 02:55:38 +0200 Subject: [PATCH 633/924] Add debounce to move_cover (#14314) * Add debounce to move_cover * Fix spelling mistake --- .../components/homekit/type_covers.py | 4 ++- .../homekit/type_security_systems.py | 2 +- tests/components/homekit/test_type_covers.py | 34 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b30109f711d..3de87cf63e8 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, @@ -80,6 +80,7 @@ class WindowCovering(HomeAccessory): self.char_target_position = serv_cover.configure_char( CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) @@ -122,6 +123,7 @@ class WindowCoveringBasic(HomeAccessory): self.char_position_state = serv_cover.configure_char( CHAR_POSITION_STATE, value=2) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index e32860d1fef..ab16f921e99 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -67,7 +67,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - # SecuritySystemTargetSTate does not support triggered + # SecuritySystemTargetState does not support triggered if not self.flag_target_state and \ hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 2dcb48a4d4c..313d58e78fd 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,19 +4,35 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) -from homeassistant.components.homekit.type_covers import ( - GarageDoorOpener, WindowCovering, WindowCoveringBasic) from homeassistant.const import ( STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, ATTR_SUPPORTED_FEATURES) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitSensors(unittest.TestCase): +class TestHomekitCovers(unittest.TestCase): """Test class for all accessory types regarding covers.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + cls.garage_cls = _import.GarageDoorOpener + cls.window_cls = _import.WindowCovering + cls.window_basic_cls = _import.WindowCoveringBasic + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +53,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" garage_door = 'cover.garage_door' - acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None) + acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -95,7 +111,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None) + acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -146,8 +162,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -214,8 +230,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() # Set from HomeKit From c4ec2e3434e3981476de6581bb578cf88dd01693 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 May 2018 20:54:56 -0400 Subject: [PATCH 634/924] Fix module names for custom components (#14317) * Fix module names for custom components * Also set __package__ correctly * bla * Remove print --- homeassistant/loader.py | 23 +++++++++++++++---- tests/test_loader.py | 18 +++++++++++++-- .../test_package/__init__.py | 2 +- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 322870952f2..b6dabb1d883 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -73,13 +73,15 @@ def get_component(hass, comp_or_platform): # Try custom component module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - comp_or_platform) + PATH_CUSTOM_COMPONENTS, comp_or_platform) if module is None: try: module = importlib.import_module( '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) + _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) except ImportError: + _LOGGER.warning('Unable to find %s', comp_or_platform) module = None cache = hass.data.get(DATA_KEY) @@ -102,18 +104,20 @@ def _find_spec(path, name): return None -def _load_module(path, name): +def _load_module(path, base_module, name): """Load a module based on a folder and a name.""" + mod_name = "{}.{}".format(base_module, name) spec = _find_spec([path], name) # Special handling if loading platforms and the folder is a namespace # (namespace is a folder without __init__.py) if spec is None and '.' in name: - parent_spec = _find_spec([path], name.split('.')[0]) + mod_parent_name = name.split('.')[0] + parent_spec = _find_spec([path], mod_parent_name) if (parent_spec is None or parent_spec.submodule_search_locations is None): return None - spec = _find_spec(parent_spec.submodule_search_locations, name) + spec = _find_spec(parent_spec.submodule_search_locations, mod_name) # Not found if spec is None: @@ -123,8 +127,19 @@ def _load_module(path, name): if spec.loader is None: return None + _LOGGER.debug('Loaded %s (%s)', name, base_module) + module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + # A hack, I know. Don't currently know how to work around it. + if not module.__name__.startswith(base_module): + module.__name__ = "{}.{}".format(base_module, name) + + if not module.__package__: + module.__package__ = base_module + elif not module.__package__.startswith(base_module): + module.__package__ = "{}.{}".format(base_module, name) + return module diff --git a/tests/test_loader.py b/tests/test_loader.py index 646526e94ea..e8a79c6501f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -30,8 +30,7 @@ class TestLoader(unittest.TestCase): comp = object() loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(comp, - loader.get_component(self.hass, 'switch.test_set')) + assert loader.get_component(self.hass, 'switch.test_set') is comp def test_get_component(self): """Test if get_component works.""" @@ -106,3 +105,18 @@ def test_helpers_wrapper(hass): yield from hass.async_block_till_done() assert result == ['hello'] + + +async def test_custom_component_name(hass): + """Test the name attribte of custom components.""" + comp = loader.get_component(hass, 'test_standalone') + assert comp.__name__ == 'custom_components.test_standalone' + assert comp.__package__ == 'custom_components' + + comp = loader.get_component(hass, 'test_package') + assert comp.__name__ == 'custom_components.test_package' + assert comp.__package__ == 'custom_components.test_package' + + comp = loader.get_component(hass, 'light.test') + assert comp.__name__ == 'custom_components.light.test' + assert comp.__package__ == 'custom_components.light' diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 528f056948b..ee669c6c9b5 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -2,6 +2,6 @@ DOMAIN = 'test_package' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True From 6a74fa344d693b12d9839b229f178dd87cf8e2af Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 05:25:48 -0400 Subject: [PATCH 635/924] Revert custom component loading logic (#14327) * Revert custom component loading logic * Lint * Fix tests * Guard for infinite inserts into sys.path --- homeassistant/loader.py | 114 +++++++----------- tests/components/notify/test_file.py | 46 ++++--- tests/test_loader.py | 4 + .../image_processing/test.py | 6 +- .../custom_components/light/test.py | 5 +- .../custom_components/switch/test.py | 5 +- .../test_package/__init__.py | 3 + .../custom_components/test_package/const.py | 2 + 8 files changed, 84 insertions(+), 101 deletions(-) create mode 100644 tests/testing_config/custom_components/test_package/const.py diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b6dabb1d883..e94fb2d6833 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -31,12 +31,6 @@ PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) -# List of available components -AVAILABLE_COMPONENTS = [] # type: List[str] - -# Dict of loaded components mapped name => module -_COMPONENT_CACHE = {} # type: Dict[str, ModuleType] - _LOGGER = logging.getLogger(__name__) @@ -64,85 +58,63 @@ def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(hass, comp_or_platform): - """Load a module from either custom component or built-in.""" +def get_component(hass, comp_or_platform) -> Optional[ModuleType]: + """Try to load specified component. + + Looks in config dir first, then built-in components. + Only returns it if also found to be valid. + Async friendly. + """ try: return hass.data[DATA_KEY][comp_or_platform] except KeyError: pass - # Try custom component - module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - PATH_CUSTOM_COMPONENTS, comp_or_platform) - - if module is None: - try: - module = importlib.import_module( - '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) - _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) - except ImportError: - _LOGGER.warning('Unable to find %s', comp_or_platform) - module = None - cache = hass.data.get(DATA_KEY) if cache is None: + # Only insert if it's not there (happens during tests) + if sys.path[0] != hass.config.config_dir: + sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - cache[comp_or_platform] = module - return module + # First check custom, then built-in + potential_paths = ['custom_components.{}'.format(comp_or_platform), + 'homeassistant.components.{}'.format(comp_or_platform)] - -def _find_spec(path, name): - for finder in sys.meta_path: + for path in potential_paths: try: - spec = finder.find_spec(name, path=path) - if spec is not None: - return spec - except AttributeError: - # Not all finders have the find_spec method - pass + module = importlib.import_module(path) + + # In Python 3 you can import files from directories that do not + # contain the file __init__.py. A directory is a valid module if + # it contains a file with the .py extension. In this case Python + # will succeed in importing the directory as a module and call it + # a namespace. We do not care about namespaces. + # This prevents that when only + # custom_components/switch/some_platform.py exists, + # the import custom_components.switch would succeed. + if module.__spec__.origin == 'namespace': + continue + + _LOGGER.info("Loaded %s from %s", comp_or_platform, path) + + cache[comp_or_platform] = module + + return module + + except ImportError as err: + # This error happens if for example custom_components/switch + # exists and we try to load switch.demo. + if str(err) != "No module named '{}'".format(path): + _LOGGER.exception( + ("Error loading %s. Make sure all " + "dependencies are installed"), path) + + _LOGGER.error("Unable to find component %s", comp_or_platform) + return None -def _load_module(path, base_module, name): - """Load a module based on a folder and a name.""" - mod_name = "{}.{}".format(base_module, name) - spec = _find_spec([path], name) - - # Special handling if loading platforms and the folder is a namespace - # (namespace is a folder without __init__.py) - if spec is None and '.' in name: - mod_parent_name = name.split('.')[0] - parent_spec = _find_spec([path], mod_parent_name) - if (parent_spec is None or - parent_spec.submodule_search_locations is None): - return None - spec = _find_spec(parent_spec.submodule_search_locations, mod_name) - - # Not found - if spec is None: - return None - - # This is a namespace - if spec.loader is None: - return None - - _LOGGER.debug('Loaded %s (%s)', name, base_module) - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - # A hack, I know. Don't currently know how to work around it. - if not module.__name__.startswith(base_module): - module.__name__ = "{}.{}".format(base_module, name) - - if not module.__package__: - module.__package__ = base_module - elif not module.__package__.startswith(base_module): - module.__package__ = "{}.{}".format(base_module, name) - - return module - - class Components: """Helper to load components.""" diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 42b9eb9d82d..c5064fca851 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -35,28 +35,30 @@ class TestNotifyFile(unittest.TestCase): assert setup_component(self.hass, notify.DOMAIN, config) assert not handle_config[notify.DOMAIN] - def _test_notify_file(self, timestamp, mock_utcnow, mock_stat): + def _test_notify_file(self, timestamp): """Test the notify file output.""" - mock_utcnow.return_value = dt_util.as_utc(dt_util.now()) - mock_stat.return_value.st_size = 0 + filename = 'mock_file' + message = 'one, two, testing, testing' + with assert_setup_component(1) as handle_config: + self.assertTrue(setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'file', + 'filename': filename, + 'timestamp': timestamp, + } + })) + assert handle_config[notify.DOMAIN] m_open = mock_open() with patch( 'homeassistant.components.notify.file.open', m_open, create=True - ): - filename = 'mock_file' - message = 'one, two, testing, testing' - with assert_setup_component(1) as handle_config: - self.assertTrue(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - 'filename': filename, - 'timestamp': timestamp, - } - })) - assert handle_config[notify.DOMAIN] + ), patch('homeassistant.components.notify.file.os.stat') as mock_st, \ + patch('homeassistant.util.dt.utcnow', + return_value=dt_util.utcnow()): + + mock_st.return_value.st_size = 0 title = '{} notifications (Log started: {})\n{}\n'.format( ATTR_TITLE_DEFAULT, dt_util.utcnow().isoformat(), @@ -82,14 +84,10 @@ class TestNotifyFile(unittest.TestCase): dt_util.utcnow().isoformat(), message))] ) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file(self, mock_utcnow, mock_stat): + def test_notify_file(self): """Test the notify file output without timestamp.""" - self._test_notify_file(False, mock_utcnow, mock_stat) + self._test_notify_file(False) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file_timestamp(self, mock_utcnow, mock_stat): + def test_notify_file_timestamp(self): """Test the notify file output with timestamp.""" - self._test_notify_file(True, mock_utcnow, mock_stat) + self._test_notify_file(True) diff --git a/tests/test_loader.py b/tests/test_loader.py index e8a79c6501f..c97e94a7ce1 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -120,3 +120,7 @@ async def test_custom_component_name(hass): comp = loader.get_component(hass, 'light.test') assert comp.__name__ == 'custom_components.light.test' assert comp.__package__ == 'custom_components.light' + + # Test custom components is mounted + from custom_components.test_package import TEST + assert TEST == 5 diff --git a/tests/testing_config/custom_components/image_processing/test.py b/tests/testing_config/custom_components/image_processing/test.py index 29d362699f5..b50050ed68e 100644 --- a/tests/testing_config/custom_components/image_processing/test.py +++ b/tests/testing_config/custom_components/image_processing/test.py @@ -3,9 +3,11 @@ from homeassistant.components.image_processing import ImageProcessingEntity -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Set up the test image_processing platform.""" - add_devices([TestImageProcessing('camera.demo_camera', "Test")]) + async_add_devices_callback([ + TestImageProcessing('camera.demo_camera', "Test")]) class TestImageProcessing(ImageProcessingEntity): diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index 71625dfdf93..fbf79f9e770 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Return mock devices.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/switch/test.py b/tests/testing_config/custom_components/switch/test.py index 2819f2f2951..79126b7b52a 100644 --- a/tests/testing_config/custom_components/switch/test.py +++ b/tests/testing_config/custom_components/switch/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Find and return test switches.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index ee669c6c9b5..85e78a7f9d6 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,4 +1,7 @@ """Provide a mock package component.""" +from .const import TEST # noqa + + DOMAIN = 'test_package' diff --git a/tests/testing_config/custom_components/test_package/const.py b/tests/testing_config/custom_components/test_package/const.py new file mode 100644 index 00000000000..7e13e04cb47 --- /dev/null +++ b/tests/testing_config/custom_components/test_package/const.py @@ -0,0 +1,2 @@ +"""Constants for test_package custom component.""" +TEST = 5 From 3e5d76efb277501a9fc8b14647fe7fa351f4c073 Mon Sep 17 00:00:00 2001 From: Javier Gonel Date: Mon, 7 May 2018 16:52:33 +0300 Subject: [PATCH 636/924] fix(hbmqtt): partial packets breaking hbmqtt (#14329) This issue was fixed in hbmqtt/issues#95 that was released in hbmqtt 0.9.2 --- homeassistant/components/mqtt/server.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index db251ab4180..8a012928792 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.1'] +REQUIREMENTS = ['hbmqtt==0.9.2'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config diff --git a/requirements_all.txt b/requirements_all.txt index 735f39a8013..6ddf0f81f61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ ha-philipsjs==0.0.3 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 939e4314718..976f4d87280 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ ha-ffmpeg==1.9 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.binary_sensor.workday holidays==0.9.5 From a4e1615127e5e4fde91a07ca855475aab556552a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 10:05:34 -0400 Subject: [PATCH 637/924] Version bump to 0.69.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7462710ea23..a1c19d41efe 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c7166241f73a969c7ce01fb49a4ae534b16a92b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 13:12:12 -0400 Subject: [PATCH 638/924] Ignore more loading errors (#14331) --- homeassistant/loader.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e94fb2d6833..67647a323c9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -105,7 +105,16 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: except ImportError as err: # This error happens if for example custom_components/switch # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): + # Ignore errors for custom_components, custom_components.switch + # and custom_components.switch.demo. + white_listed_errors = [] + parts = [] + for part in path.split('.'): + parts.append(part) + white_listed_errors.append( + "No module named '{}'".format('.'.join(parts))) + + if str(err) not in white_listed_errors: _LOGGER.exception( ("Error loading %s. Make sure all " "dependencies are installed"), path) From b1eb35ee119c07db2e85ebf44ad5599413d104e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 13:12:12 -0400 Subject: [PATCH 639/924] Ignore more loading errors (#14331) --- homeassistant/loader.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e94fb2d6833..67647a323c9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -105,7 +105,16 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: except ImportError as err: # This error happens if for example custom_components/switch # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): + # Ignore errors for custom_components, custom_components.switch + # and custom_components.switch.demo. + white_listed_errors = [] + parts = [] + for part in path.split('.'): + parts.append(part) + white_listed_errors.append( + "No module named '{}'".format('.'.join(parts))) + + if str(err) not in white_listed_errors: _LOGGER.exception( ("Error loading %s. Make sure all " "dependencies are installed"), path) From 8d24541ffe3d20bf4b714656a3505f0958aeeff5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 13:12:54 -0400 Subject: [PATCH 640/924] Version bump to 0.69.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a1c19d41efe..7d54bf6356d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e7c7b9b2a947e177eb8d40331983ab3bfdf48359 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 May 2018 11:18:51 -0600 Subject: [PATCH 641/924] Adds unique ID to Roku for entity registry inclusion (#14325) * Adds unique ID to Roku for entity registry inclusion * Owner-requested changes --- homeassistant/components/media_player/roku.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 87129f30db5..a46e781de59 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -146,6 +146,11 @@ class RokuDevice(MediaPlayerDevice): """Flag media player features that are supported.""" return SUPPORT_ROKU + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.device_info.sernum + @property def media_content_type(self): """Content type of current playing media.""" From 48b13cc86501e09ddd6694faa08193b048b55609 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 8 May 2018 01:26:46 -0600 Subject: [PATCH 642/924] Update hitron_coda.py to fix login for Shaw modems (#14306) I have a Hitron modem provided by Shaw communications rather than from Rogers as the Docs specify for this device_tracker but it seems like the api/code is all the same except that the login failed due to the password being passed as "pws" instead of "pwd". Making that one character change allowed HASS to read the connected device details from my Hitron modem. If this difference is actually one that stands between the Rogers-provided Hitron modems and the Shaw-provided variant, I am happy to create another device-tracker file for the Shaw modem. I just figured I would go with the simplest solution first. --- .../components/device_tracker/hitron_coda.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py index aa437eeef86..c9cd30cdb25 100644 --- a/homeassistant/components/device_tracker/hitron_coda.py +++ b/homeassistant/components/device_tracker/hitron_coda.py @@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE ) _LOGGER = logging.getLogger(__name__) +DEFAULT_TYPE = "rogers" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string, }) @@ -49,6 +52,11 @@ class HitronCODADeviceScanner(DeviceScanner): self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) + if config.get(CONF_TYPE) == "shaw": + self._type = 'pwd' + else: + self.type = 'pws' + self._userid = None self.success_init = self._update_info() @@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner): try: data = [ ('user', self._username), - ('pws', self._password), + (self._type, self._password), ] res = requests.post(self._loginurl, data=data, timeout=10) except requests.exceptions.Timeout: From ba7333e804210487f92e8df1e34cf28228812970 Mon Sep 17 00:00:00 2001 From: Gerard Date: Tue, 8 May 2018 09:52:21 +0200 Subject: [PATCH 643/924] Add sensors for BMW electric cars (#14293) * Add sensors for electric cars * Updates based on review of @MartinHjelmare * Fix Travis error * Another fix for Travis --- .../binary_sensor/bmw_connected_drive.py | 75 +++++++++++++++++-- .../bmw_connected_drive/__init__.py | 2 +- .../components/sensor/bmw_connected_drive.py | 69 ++++++++++------- requirements_all.txt | 2 +- 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0abf6eb1064..af3ebd53b80 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'lids': ['Doors', 'opening'], 'windows': ['Windows', 'opening'], - 'door_lock_state': ['Door lock state', 'safety'] + 'door_lock_state': ['Door lock state', 'safety'], + 'lights_parking': ['Parking lights', 'light'], + 'condition_based_services': ['Condition based services', 'problem'], + 'check_control_messages': ['Control messages', 'problem'] } +SENSOR_TYPES_ELEC = { + 'charging_status': ['Charging status', 'power'], + 'connection_status': ['Connection status', 'plug'] +} + +SENSOR_TYPES_ELEC.update(SENSOR_TYPES) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) - devices.append(device) + if vehicle.has_hv_battery: + _LOGGER.debug('BMW with a high voltage battery') + for key, value in sorted(SENSOR_TYPES_ELEC.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) + elif vehicle.has_internal_combustion_engine: + _LOGGER.debug('BMW with an internal combustion engine') + for key, value in sorted(SENSOR_TYPES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) add_devices(devices, True) @@ -92,12 +110,41 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result[window.name] = window.state.value elif self._attribute == 'door_lock_state': result['door_lock_state'] = vehicle_state.door_lock_state.value + result['last_update_reason'] = vehicle_state.last_update_reason + elif self._attribute == 'lights_parking': + result['lights_parking'] = vehicle_state.parking_lights.value + elif self._attribute == 'condition_based_services': + for report in vehicle_state.condition_based_services: + service_type = report.service_type.lower().replace('_', ' ') + result['{} status'.format(service_type)] = report.state.value + if report.due_date is not None: + result['{} date'.format(service_type)] = \ + report.due_date.strftime('%Y-%m-%d') + if report.due_distance is not None: + result['{} distance'.format(service_type)] = \ + '{} km'.format(report.due_distance) + elif self._attribute == 'check_control_messages': + check_control_messages = vehicle_state.check_control_messages + if not check_control_messages: + result['check_control_messages'] = 'OK' + else: + result['check_control_messages'] = check_control_messages + elif self._attribute == 'charging_status': + result['charging_status'] = vehicle_state.charging_status.value + # pylint: disable=W0212 + result['last_charging_end_result'] = \ + vehicle_state._attributes['lastChargingEndResult'] + if self._attribute == 'connection_status': + # pylint: disable=W0212 + result['connection_status'] = \ + vehicle_state._attributes['connectionStatus'] return result def update(self): """Read new state data from the library.""" from bimmer_connected.state import LockState + from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed @@ -111,6 +158,24 @@ class BMWConnectedDriveSensor(BinarySensorDevice): # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED self._state = vehicle_state.door_lock_state not in \ [LockState.LOCKED, LockState.SECURED] + # device class light: On means light detected, Off means no light + if self._attribute == 'lights_parking': + self._state = vehicle_state.are_parking_lights_on + # device class problem: On means problem detected, Off means no problem + if self._attribute == 'condition_based_services': + self._state = not vehicle_state.are_all_cbs_ok + if self._attribute == 'check_control_messages': + self._state = vehicle_state.has_check_control_messages + # device class power: On means power detected, Off means no power + if self._attribute == 'charging_status': + self._state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + # device class plug: On means device is plugged in, + # Off means device is unplugged + if self._attribute == 'connection_status': + # pylint: disable=W0212 + self._state = (vehicle_state._attributes['connectionStatus'] == + 'CONNECTED') def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 347bab6f529..a7ed262ac2c 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['bimmer_connected==0.5.0'] +REQUIREMENTS = ['bimmer_connected==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index ed75520c179..8e06836b102 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -9,22 +9,12 @@ import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -LENGTH_ATTRIBUTES = { - 'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'], - 'mileage': ['Mileage', 'mdi:speedometer'] -} - -VALID_ATTRIBUTES = { - 'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station'] -} - -VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -34,27 +24,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for key, value in sorted(VALID_ATTRIBUTES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) + for attribute_name in vehicle.drive_train_attributes: + device = BMWConnectedDriveSensor(account, vehicle, + attribute_name) devices.append(device) + device = BMWConnectedDriveSensor(account, vehicle, 'mileage') + devices.append(device) add_devices(devices, True) class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, icon): + def __init__(self, account, vehicle, attribute: str): """Constructor.""" self._vehicle = vehicle self._account = account self._attribute = attribute self._state = None - self._unit_of_measurement = None self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) - self._sensor_name = sensor_name - self._icon = icon @property def should_poll(self) -> bool: @@ -74,7 +63,27 @@ class BMWConnectedDriveSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return self._icon + from bimmer_connected.state import ChargingState + vehicle_state = self._vehicle.state + charging_state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + + if self._attribute == 'mileage': + return 'mdi:speedometer' + elif self._attribute in ( + 'remaining_range_total', 'remaining_range_electric', + 'remaining_range_fuel', 'max_range_electric'): + return 'mdi:ruler' + elif self._attribute == 'remaining_fuel': + return 'mdi:gas-station' + elif self._attribute == 'charging_time_remaining': + return 'mdi:update' + elif self._attribute == 'charging_status': + return 'mdi:battery-charging' + elif self._attribute == 'charging_level_hv': + return icon_for_battery_level( + battery_level=vehicle_state.charging_level_hv, + charging=charging_state) @property def state(self): @@ -88,7 +97,17 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - return self._unit_of_measurement + if self._attribute in ( + 'mileage', 'remaining_range_total', 'remaining_range_electric', + 'remaining_range_fuel', 'max_range_electric'): + return 'km' + elif self._attribute == 'remaining_fuel': + return 'l' + elif self._attribute == 'charging_time_remaining': + return 'h' + elif self._attribute == 'charging_level_hv': + return '%' + return None @property def device_state_attributes(self): @@ -101,14 +120,10 @@ class BMWConnectedDriveSensor(Entity): """Read new state data from the library.""" _LOGGER.debug('Updating %s', self._vehicle.name) vehicle_state = self._vehicle.state - self._state = getattr(vehicle_state, self._attribute) - - if self._attribute in LENGTH_ATTRIBUTES: - self._unit_of_measurement = 'km' - elif self._attribute == 'remaining_fuel': - self._unit_of_measurement = 'l' + if self._attribute == 'charging_status': + self._state = getattr(vehicle_state, self._attribute).value else: - self._unit_of_measurement = None + self._state = getattr(vehicle_state, self._attribute) def update_callback(self): """Schedule a state update.""" diff --git a/requirements_all.txt b/requirements_all.txt index 66b011e0440..2db7a66d7b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -149,7 +149,7 @@ beautifulsoup4==4.6.0 bellows==0.6.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.0 +bimmer_connected==0.5.1 # homeassistant.components.blink blinkpy==0.6.0 From 230bd3929c716aeed1e9dbca0bf1154e0821fade Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Tue, 8 May 2018 09:57:51 +0200 Subject: [PATCH 644/924] Add more homematicip cloud components (#14084) * Add support for shutter contact and motion detector device * Add support for power switch devices * Add support for light switch device * Cleanup binary_switch and light platform * Update comment --- .../binary_sensor/homematicip_cloud.py | 85 +++++++++++++++++++ homeassistant/components/homematicip_cloud.py | 5 +- .../components/light/homematicip_cloud.py | 76 +++++++++++++++++ .../components/switch/homematicip_cloud.py | 84 ++++++++++++++++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/binary_sensor/homematicip_cloud.py create mode 100644 homeassistant/components/light/homematicip_cloud.py create mode 100644 homeassistant/components/switch/homematicip_cloud.py diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py new file mode 100644 index 00000000000..40ffe498402 --- /dev/null +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -0,0 +1,85 @@ +""" +Support for HomematicIP binary sensor. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_WINDOW_STATE = 'window_state' +ATTR_EVENT_DELAY = 'event_delay' +ATTR_MOTION_DETECTED = 'motion_detected' +ATTR_ILLUMINATION = 'illumination' + +HMIP_OPEN = 'open' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP binary sensor devices.""" + from homematicip.device import (ShutterContact, MotionDetectorIndoor) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, ShutterContact): + devices.append(HomematicipShutterContact(home, device)) + elif isinstance(device, MotionDetectorIndoor): + devices.append(HomematicipMotionDetector(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): + """HomematicIP shutter contact.""" + + def __init__(self, home, device): + """Initialize the shutter contact.""" + super().__init__(home, device) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'door' + + @property + def is_on(self): + """Return true if the shutter contact is on/open.""" + if self._device.sabotage: + return True + if self._device.windowState is None: + return None + return self._device.windowState.lower() == HMIP_OPEN + + +class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): + """MomematicIP motion detector.""" + + def __init__(self, home, device): + """Initialize the shutter contact.""" + super().__init__(home, device) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'motion' + + @property + def is_on(self): + """Return true if motion is detected.""" + if self._device.sabotage: + return True + return self._device.motionDetected diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 0b15d7a3dfe..d85d867d8f8 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -24,7 +24,10 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' COMPONENTS = [ - 'sensor' + 'sensor', + 'binary_sensor', + 'switch', + 'light' ] CONF_NAME = 'name' diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py new file mode 100644 index 00000000000..e433da44ae7 --- /dev/null +++ b/homeassistant/components/light/homematicip_cloud.py @@ -0,0 +1,76 @@ +""" +Support for HomematicIP light. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.light import Light +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGIE_COUNTER = 'energie_counter' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP light devices.""" + from homematicip.device import ( + BrandSwitchMeasuring) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, BrandSwitchMeasuring): + devices.append(HomematicipLightMeasuring(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipLight(HomematicipGenericDevice, Light): + """MomematicIP light device.""" + + def __init__(self, home, device): + """Initialize the light device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipLightMeasuring(HomematicipLight): + """MomematicIP measuring light device.""" + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.currentPowerConsumption + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if self._device.energyCounter is None: + return 0 + return round(self._device.energyCounter) diff --git a/homeassistant/components/switch/homematicip_cloud.py b/homeassistant/components/switch/homematicip_cloud.py new file mode 100644 index 00000000000..9123d46c87b --- /dev/null +++ b/homeassistant/components/switch/homematicip_cloud.py @@ -0,0 +1,84 @@ +""" +Support for HomematicIP switch. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGIE_COUNTER = 'energie_counter' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP switch devices.""" + from homematicip.device import ( + PlugableSwitch, PlugableSwitchMeasuring, + BrandSwitchMeasuring) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, BrandSwitchMeasuring): + # BrandSwitchMeasuring inherits PlugableSwitchMeasuring + # This device is implemented in the light platform and will + # not be added in the switch platform + pass + elif isinstance(device, PlugableSwitchMeasuring): + devices.append(HomematicipSwitchMeasuring(home, device)) + elif isinstance(device, PlugableSwitch): + devices.append(HomematicipSwitch(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): + """MomematicIP switch device.""" + + def __init__(self, home, device): + """Initialize the switch device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipSwitchMeasuring(HomematicipSwitch): + """MomematicIP measuring switch device.""" + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.currentPowerConsumption + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if self._device.energyCounter is None: + return 0 + return round(self._device.energyCounter) From 434365974243e8671b8d8944de02c91b95f40abc Mon Sep 17 00:00:00 2001 From: m4dmin <39031482+m4dmin@users.noreply.github.com> Date: Tue, 8 May 2018 13:43:07 +0200 Subject: [PATCH 645/924] add 2 devices (#14321) * add 2 devices io:RollerShutterUnoIOComponent io:ExteriorVenetianBlindIOComponent * add 2 devices * Update tahoma.py * Fix hounci-bot violation * Fixed Travis CI build failure ./homeassistant/components/cover/tahoma.py:83:13: E125 continuation line with same indent as next logical line * Fixed Travis CI build failure E125 continuation line with same indent as next logical line * Fixed Travis CI build failure E127 continuation line over-indented for visual indent * Fix indent * Change check --- homeassistant/components/cover/tahoma.py | 4 +++- homeassistant/components/tahoma.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 20625143daf..cf8b7dfad48 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -79,7 +79,9 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.tahoma_device.type == \ 'io:RollerShutterWithLowSpeedManagementIOComponent': self.apply_action('setPosition', 'secured') - elif self.tahoma_device.type == 'rts:BlindRTSComponent': + elif self.tahoma_device.type in \ + ('rts:BlindRTSComponent', + 'io:ExteriorVenetianBlindIOComponent'): self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 9848d20094c..84edd9afd40 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -40,6 +40,8 @@ TAHOMA_TYPES = { 'rts:CurtainRTSComponent': 'cover', 'rts:BlindRTSComponent': 'cover', 'rts:VenetianBlindRTSComponent': 'cover', + 'io:ExteriorVenetianBlindIOComponent': 'cover', + 'io:RollerShutterUnoIOComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', 'io:RollerShutterVeluxIOComponent': 'cover', 'io:RollerShutterGenericIOComponent': 'cover', From eb551a6d5afa5581d26ae739a79787cc363c87d1 Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Wed, 9 May 2018 01:42:18 +1000 Subject: [PATCH 646/924] Gogogate2 0.1.1 (#14294) * Gogogate2 - bump version Uses latest version of library which ensures commands to device are idempotent * Update requirements_all * Expose sensor temperature * update version * import attribute * Set temperature * Remove temperature attribute Removed temperature attribute until it can be re-implemented as a separate sensor. * Update ordering * Fix copy-&-paste issue --- homeassistant/components/cover/gogogate2.py | 13 +++++++------ requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index 688df62ca6a..2b91591e71b 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -1,5 +1,5 @@ """ -Support for Gogogate2 Garage Doors. +Support for Gogogate2 garage Doors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.gogogate2/ @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygogogate2==0.0.7'] +REQUIREMENTS = ['pygogogate2==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -25,9 +25,9 @@ NOTIFICATION_ID = 'gogogate2_notification' NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' COVER_SCHEMA = vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -36,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Gogogate2 component.""" from pygogogate2 import Gogogate2API as pygogogate2 - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) ip_address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) + password = config.get(CONF_PASSWORD) + username = config.get(CONF_USERNAME) + mygogogate2 = pygogogate2(username, password, ip_address) try: diff --git a/requirements_all.txt b/requirements_all.txt index 2db7a66d7b2..0dcfdbcce70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,7 +784,7 @@ pyfritzhome==0.3.7 pyfttt==0.3 # homeassistant.components.cover.gogogate2 -pygogogate2==0.0.7 +pygogogate2==0.1.1 # homeassistant.components.remote.harmony pyharmony==1.0.20 From c664c20165ebeb248b98716cf61e865f274a2dac Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Tue, 8 May 2018 11:43:31 -0400 Subject: [PATCH 647/924] Snips: Added slot values for siteId and probability (#14315) * Added solt values for siteId and probability * Update snips.py * Update test_snips.py --- homeassistant/components/snips.py | 2 ++ tests/components/test_snips.py | 54 ++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 812906e7be9..4f50c6beaaa 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -131,6 +131,8 @@ async def async_setup(hass, config): slots = {} for slot in request.get('slots', []): slots[slot['slotName']] = {'value': resolve_slot_values(slot)} + slots['site_id'] = {'value': request.get('siteId')} + slots['probability'] = {'value': request['intent']['probability']} try: intent_response = await intent.async_handle( diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index 2342e897708..d9238336768 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -118,7 +118,9 @@ async def test_snips_intent(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'Lights' - assert intent.slots == {'light_color': {'value': 'green'}} + assert intent.slots == {'light_color': {'value': 'green'}, + 'probability': {'value': 1}, + 'site_id': {'value': None}} assert intent.text_input == 'turn the lights green' @@ -169,7 +171,9 @@ async def test_snips_intent_with_duration(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'SetTimer' - assert intent.slots == {'timer_duration': {'value': 300}} + assert intent.slots == {'probability': {'value': 1}, + 'site_id': {'value': None}, + 'timer_duration': {'value': 300}} async def test_intent_speech_response(hass, mqtt_mock): @@ -318,11 +322,51 @@ async def test_snips_low_probability(hass, mqtt_mock, caplog): assert 'Intent below probaility threshold 0.49 < 0.5' in caplog.text +async def test_intent_special_slots(hass, mqtt_mock): + """Test intent special slot values via Snips.""" + calls = async_mock_service(hass, 'light', 'turn_on') + result = await async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + result = await async_setup_component(hass, "intent_script", { + "intent_script": { + "Lights": { + "action": { + "service": "light.turn_on", + "data_template": { + "probability": "{{ probability }}", + "site_id": "{{ site_id }}" + } + } + } + } + }) + assert result + payload = """ + { + "input": "turn the light on", + "intent": { + "intentName": "Lights", + "probability": 0.85 + }, + "siteId": "default", + "slots": [] + } + """ + async_fire_mqtt_message(hass, 'hermes/intent/Lights', payload) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'light' + assert calls[0].service == 'turn_on' + assert calls[0].data['probability'] == '0.85' + assert calls[0].data['site_id'] == 'default' + + async def test_snips_say(hass, caplog): """Test snips say with invalid config.""" - calls = async_mock_service(hass, 'snips', 'say', - snips.SERVICE_SCHEMA_SAY) - + calls = async_mock_service(hass, 'snips', 'say', snips.SERVICE_SCHEMA_SAY) data = {'text': 'Hello'} await hass.services.async_call('snips', 'say', data) await hass.async_block_till_done() From 6199e50e8051992ba0fc661babc66c3ac9069e5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 11:55:04 -0400 Subject: [PATCH 648/924] Fix Insteon PLM coverage --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 9030cc9a097..1f2a8f8d233 100644 --- a/.coveragerc +++ b/.coveragerc @@ -127,7 +127,7 @@ omit = homeassistant/components/insteon_local.py homeassistant/components/*/insteon_local.py - homeassistant/components/insteon_plm.py + homeassistant/components/insteon_plm/* homeassistant/components/*/insteon_plm.py homeassistant/components/ios.py From ff01aa40c93faf4659ffb1ec8729180cb1dabeeb Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Tue, 8 May 2018 19:24:27 +0200 Subject: [PATCH 649/924] Add help for conversation/process service (#14323) * Add help for conversation/process service * Add logging to debug text received when service is called * Move conversation to specific folder --- .../{conversation.py => conversation/__init__.py} | 1 + homeassistant/components/conversation/services.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) rename homeassistant/components/{conversation.py => conversation/__init__.py} (99%) create mode 100644 homeassistant/components/conversation/services.yaml diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation/__init__.py similarity index 99% rename from homeassistant/components/conversation.py rename to homeassistant/components/conversation/__init__.py index ddd96c99177..9cb00a84583 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation/__init__.py @@ -96,6 +96,7 @@ async def async_setup(hass, config): async def process(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] + _LOGGER.debug('Processing: <%s>', text) try: await _process(hass, text) except intent.IntentHandleError as err: diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml new file mode 100644 index 00000000000..a1b980d8e05 --- /dev/null +++ b/homeassistant/components/conversation/services.yaml @@ -0,0 +1,10 @@ +# Describes the format for available component services + +process: + description: Launch a conversation from a transcribed text. + fields: + text: + description: Transcribed text + example: Turn all lights on + + From e12994a0cdc4855173ad8ee9055bdfd3205ed799 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Wed, 9 May 2018 03:35:55 +1000 Subject: [PATCH 650/924] Fix BOM weather '-' value (#14042) --- homeassistant/components/sensor/bom.py | 44 +++++++---- homeassistant/components/weather/bom.py | 14 ++-- tests/components/sensor/test_bom.py | 97 +++++++++++++++++++++++++ tests/fixtures/bom_weather.json | 42 +++++++++++ 4 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 tests/components/sensor/test_bom.py create mode 100644 tests/fixtures/bom_weather.json diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 128f532e459..d6764e5e994 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -19,8 +19,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, STATE_UNKNOWN, CONF_NAME, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE) + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, CONF_NAME, ATTR_ATTRIBUTION, + CONF_LATITUDE, CONF_LONGITUDE) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -145,21 +145,18 @@ class BOMCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.bom_data.data and self._condition in self.bom_data.data: - return self.bom_data.data[self._condition] - - return STATE_UNKNOWN + return self.bom_data.get_reading(self._condition) @property def device_state_attributes(self): """Return the state attributes of the device.""" attr = {} attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.bom_data.data['history_product'] - attr['Station Id'] = self.bom_data.data['wmo'] - attr['Station Name'] = self.bom_data.data['name'] + attr['Zone Id'] = self.bom_data.latest_data['history_product'] + attr['Station Id'] = self.bom_data.latest_data['wmo'] + attr['Station Name'] = self.bom_data.latest_data['name'] attr['Last Update'] = datetime.datetime.strptime(str( - self.bom_data.data['local_date_time_full']), '%Y%m%d%H%M%S') + self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr @@ -180,22 +177,43 @@ class BOMCurrentData(object): """Initialize the data object.""" self._hass = hass self._zone_id, self._wmo_id = station_id.split('.') - self.data = None + self._data = None def _build_url(self): url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) _LOGGER.info("BOM URL %s", url) return url + @property + def latest_data(self): + """Return the latest data object.""" + if self._data: + return self._data[0] + return None + + def get_reading(self, condition): + """Return the value for the given condition. + + BOM weather publishes condition readings for weather (and a few other + conditions) at intervals throughout the day. To avoid a `-` value in + the frontend for these conditions, we traverse the historical data + for the latest value that is not `-`. + + Iterators are used in this method to avoid iterating needlessly + iterating through the entire BOM provided dataset + """ + condition_readings = (entry[condition] for entry in self._data) + return next((x for x in condition_readings if x != '-'), None) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from BOM.""" try: result = requests.get(self._build_url(), timeout=10).json() - self.data = result['observations']['data'][0] + self._data = result['observations']['data'] except ValueError as err: _LOGGER.error("Check BOM %s", err.args) - self.data = None + self._data = None raise diff --git a/homeassistant/components/weather/bom.py b/homeassistant/components/weather/bom.py index 236aeb2fa2e..ad74bb4fb77 100644 --- a/homeassistant/components/weather/bom.py +++ b/homeassistant/components/weather/bom.py @@ -48,7 +48,7 @@ class BOMWeather(WeatherEntity): def __init__(self, bom_data, stationname=None): """Initialise the platform with a data instance and station name.""" self.bom_data = bom_data - self.stationname = stationname or self.bom_data.data.get('name') + self.stationname = stationname or self.bom_data.latest_data.get('name') def update(self): """Update current conditions.""" @@ -62,14 +62,14 @@ class BOMWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self.bom_data.data.get('weather') + return self.bom_data.get_reading('weather') # Now implement the WeatherEntity interface @property def temperature(self): """Return the platform temperature.""" - return self.bom_data.data.get('air_temp') + return self.bom_data.get_reading('air_temp') @property def temperature_unit(self): @@ -79,17 +79,17 @@ class BOMWeather(WeatherEntity): @property def pressure(self): """Return the mean sea-level pressure.""" - return self.bom_data.data.get('press_msl') + return self.bom_data.get_reading('press_msl') @property def humidity(self): """Return the relative humidity.""" - return self.bom_data.data.get('rel_hum') + return self.bom_data.get_reading('rel_hum') @property def wind_speed(self): """Return the wind speed.""" - return self.bom_data.data.get('wind_spd_kmh') + return self.bom_data.get_reading('wind_spd_kmh') @property def wind_bearing(self): @@ -99,7 +99,7 @@ class BOMWeather(WeatherEntity): 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] wind = {name: idx * 360 / 16 for idx, name in enumerate(directions)} - return wind.get(self.bom_data.data.get('wind_dir')) + return wind.get(self.bom_data.get_reading('wind_dir')) @property def attribution(self): diff --git a/tests/components/sensor/test_bom.py b/tests/components/sensor/test_bom.py new file mode 100644 index 00000000000..06a7089e052 --- /dev/null +++ b/tests/components/sensor/test_bom.py @@ -0,0 +1,97 @@ +"""The tests for the BOM Weather sensor platform.""" +import re +import unittest +import json +import requests +from unittest.mock import patch +from urllib.parse import urlparse + +from homeassistant.setup import setup_component +from homeassistant.components import sensor + +from tests.common import ( + get_test_home_assistant, assert_setup_component, load_fixture) + +VALID_CONFIG = { + 'platform': 'bom', + 'station': 'IDN60901.94767', + 'name': 'Fake', + 'monitored_conditions': [ + 'apparent_t', + 'press', + 'weather' + ] +} + + +def mocked_requests(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + @property + def content(self): + """Return the content of the response.""" + return self.json() + + def raise_for_status(self): + """Raise an HTTPError if status is not 200.""" + if self.status_code != 200: + raise requests.HTTPError(self.status_code) + + url = urlparse(args[0]) + if re.match(r'^/fwo/[\w]+/[\w.]+\.json', url.path): + return MockResponse(json.loads(load_fixture('bom_weather.json')), 200) + + raise NotImplementedError('Unknown route {}'.format(url.path)) + + +class TestBOMWeatherSensor(unittest.TestCase): + """Test the BOM Weather sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('requests.get', side_effect=mocked_requests) + def test_setup(self, mock_get): + """Test the setup with custom settings.""" + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + 'sensor': VALID_CONFIG})) + + fake_entities = [ + 'bom_fake_feels_like_c', + 'bom_fake_pressure_mb', + 'bom_fake_weather'] + + for entity_id in fake_entities: + state = self.hass.states.get('sensor.{}'.format(entity_id)) + self.assertIsNotNone(state) + + @patch('requests.get', side_effect=mocked_requests) + def test_sensor_values(self, mock_get): + """Test retrieval of sensor values.""" + self.assertTrue(setup_component( + self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) + + self.assertEqual('Fine', self.hass.states.get( + 'sensor.bom_fake_weather').state) + self.assertEqual('1021.7', self.hass.states.get( + 'sensor.bom_fake_pressure_mb').state) + self.assertEqual('25.0', self.hass.states.get( + 'sensor.bom_fake_feels_like_c').state) diff --git a/tests/fixtures/bom_weather.json b/tests/fixtures/bom_weather.json new file mode 100644 index 00000000000..d40ea6fb21a --- /dev/null +++ b/tests/fixtures/bom_weather.json @@ -0,0 +1,42 @@ +{ + "observations": { + "data": [ + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 25.0, + "press": 1021.7, + "weather": "-" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 22.0, + "press": 1019.7, + "weather": "-" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 20.0, + "press": 1011.7, + "weather": "Fine" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 18.0, + "press": 1010.0, + "weather": "-" + } + ] + } +} From 10505d542ad49fbb4643d4f0cb3ae8b13477f2e2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 8 May 2018 22:30:28 +0300 Subject: [PATCH 651/924] Make sure zwave nodes/entities enter the registry is proper state. (#14251) * When zwave node's info is parsed remove it and re-add back. * Delay value entity if not ready * If node is ready consider it parsed even if manufacturer/product are missing. * Add annotations --- homeassistant/components/zwave/__init__.py | 81 ++++++++++++------- homeassistant/components/zwave/node_entity.py | 16 +++- homeassistant/components/zwave/util.py | 23 ++++++ tests/components/zwave/test_init.py | 2 +- 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 01b17023c12..7562ac0ff14 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,7 +11,7 @@ from pprint import pprint import voluptuous as vol -from homeassistant.core import CoreState +from homeassistant.core import callback, CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id @@ -31,7 +31,8 @@ from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS -from .util import check_node_schema, check_value_schema, node_name +from .util import (check_node_schema, check_value_schema, node_name, + check_has_unique_id, is_node_parsed) REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] @@ -313,30 +314,22 @@ def setup(hass, config): _add_node_to_component() return - async def _check_node_ready(): - """Wait for node to be parsed.""" - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow()-start_time).total_seconds()) - - if entity.unique_id: - _LOGGER.info("Z-Wave node %d ready after %d seconds", - entity.node_id, waited) - break - elif waited >= const.NODE_READY_WAIT_SECS: - # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave - # node to be ready. - _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " - "continuing anyway", - entity.node_id, waited) - break - else: - await asyncio.sleep(1, loop=hass.loop) - + @callback + def _on_ready(sec): + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, sec) hass.async_add_job(_add_node_to_component) - hass.add_job(_check_node_ready) + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, sec) + hass.async_add_job(_add_node_to_component) + + hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout, + hass.loop) def network_ready(): """Handle the query of all awake nodes.""" @@ -839,13 +832,35 @@ class ZWaveDeviceEntityValues(): dict_id = id(self) + @callback + def _on_ready(sec): + _LOGGER.info( + "Z-Wave entity %s (node_id: %d) ready after %d seconds", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave entity %s (node_id: %d) not ready after %d seconds, " + "continuing anyway", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) - self._hass.add_job(discover_device, component, device, dict_id) + + if device.unique_id: + self._hass.add_job(discover_device, component, device, dict_id) + else: + self._hass.add_job(check_has_unique_id, device, _on_ready, + _on_timeout, self._hass.loop) class ZWaveDeviceEntity(ZWaveBaseEntity): @@ -862,8 +877,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = self._compute_unique_id() self._update_attributes() dispatcher.connect( @@ -894,6 +908,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def _update_attributes(self): """Update the node attributes. May only be used inside callback.""" self.node_id = self.node.node_id + self._name = _value_name(self.values.primary) + if not self._unique_id: + self._unique_id = self._compute_unique_id() + if self._unique_id: + self.try_remove_and_add() if self.values.power: self.power_consumption = round( @@ -940,3 +959,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): for value in self.values: if value is not None: self.node.refresh_value(value.value_id) + + def _compute_unique_id(self): + if (is_node_parsed(self.node) and + self.values.primary.label != "Unknown") or \ + self.node.is_ready: + return "{}-{}".format(self.node.node_id, + self.values.primary.object_id) + return None diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index bcddcb0b800..2c6d26802bd 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -9,7 +9,7 @@ from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE) -from .util import node_name +from .util import node_name, is_node_parsed _LOGGER = logging.getLogger(__name__) @@ -65,6 +65,15 @@ class ZWaveBaseEntity(Entity): self._update_scheduled = True self.hass.loop.call_later(0.1, do_update) + def try_remove_and_add(self): + """Remove this entity and add it back.""" + async def _async_remove_and_add(): + await self.async_remove() + self.entity_id = None + await self.platform.async_add_entities([self]) + if self.hass and self.platform: + self.hass.add_job(_async_remove_and_add) + class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" @@ -151,6 +160,9 @@ class ZWaveNodeEntity(ZWaveBaseEntity): if not self._unique_id: self._unique_id = self._compute_unique_id() + if self._unique_id: + # Node info parsed. Remove and re-add + self.try_remove_and_add() self.maybe_schedule_update() @@ -243,6 +255,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return attrs def _compute_unique_id(self): - if self._manufacturer_name and self._product_name: + if is_node_parsed(self.node) or self.node.is_ready: return 'node-{}'.format(self.node_id) return None diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 8c74b731ad6..1c0bb14f7e5 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -1,6 +1,9 @@ """Zwave util methods.""" +import asyncio import logging +import homeassistant.util.dt as dt_util + from . import const _LOGGER = logging.getLogger(__name__) @@ -67,3 +70,23 @@ def node_name(node): """Return the name of the node.""" return node.name or '{} {}'.format( node.manufacturer_name, node.product_name) + + +async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): + """Wait for entity to have unique_id.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + if entity.unique_id: + ready_callback(waited) + return + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. + timeout_callback(waited) + return + await asyncio.sleep(1, loop=loop) + + +def is_node_parsed(node): + """Check whether the node has been parsed or still waiting to be parsed.""" + return node.manufacturer_name and node.product_name diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index faa7357bd8a..0eba19f03a4 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -237,7 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None) + node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) sleeps = [] From 9c7523d7b0b38895b4d405d6e5d1f922d66d0e8c Mon Sep 17 00:00:00 2001 From: Evgeniy <592652+evgeniy-khatko@users.noreply.github.com> Date: Tue, 8 May 2018 14:42:57 -0700 Subject: [PATCH 652/924] Improving icloud device tracker (#14078) * Improving icloud device tracker * Adding config validations for new values * Adding config validations for new values * Moving icloud specific setup to platform schema. Setting default in platform schema. --- .../components/device_tracker/icloud.py | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 5d40f5d533a..8ea81e88440 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyicloud==0.9.1'] -CONF_IGNORED_DEVICES = 'ignored_devices' CONF_ACCOUNTNAME = 'account_name' +CONF_MAX_INTERVAL = 'max_interval' +CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold' # entity attributes ATTR_ACCOUNTNAME = 'account_name' @@ -64,13 +65,15 @@ DEVICESTATUSCODES = { SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), vol.Optional(ATTR_DEVICENAME): cv.slugify, - vol.Optional(ATTR_INTERVAL): cv.positive_int, + vol.Optional(ATTR_INTERVAL): cv.positive_int }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, + vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int, + vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int }) @@ -79,8 +82,11 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0])) + max_interval = config.get(CONF_MAX_INTERVAL) + gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD) - icloudaccount = Icloud(hass, username, password, account, see) + icloudaccount = Icloud(hass, username, password, account, max_interval, + gps_accuracy_threshold, see) if icloudaccount.api is not None: ICLOUDTRACKERS[account] = icloudaccount @@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].lost_iphone(devicename) + hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone, schema=SERVICE_SCHEMA) @@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].update_icloud(devicename) + hass.services.register(DOMAIN, 'icloud_update', update_icloud, schema=SERVICE_SCHEMA) @@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].reset_account_icloud() + hass.services.register(DOMAIN, 'icloud_reset_account', reset_account_icloud, schema=SERVICE_SCHEMA) @@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): class Icloud(DeviceScanner): """Representation of an iCloud account.""" - def __init__(self, hass, username, password, name, see): + def __init__(self, hass, username, password, name, max_interval, + gps_accuracy_threshold, see): """Initialize an iCloud account.""" self.hass = hass self.username = username @@ -148,6 +158,8 @@ class Icloud(DeviceScanner): self.seen_devices = {} self._overridestates = {} self._intervals = {} + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold self.see = see self._trusted_device = None @@ -348,7 +360,7 @@ class Icloud(DeviceScanner): self._overridestates[devicename] = None if currentzone is not None: - self._intervals[devicename] = 30 + self._intervals[devicename] = self._max_interval return if mindistance is None: @@ -363,7 +375,6 @@ class Icloud(DeviceScanner): if interval > 180: # Three hour drive? This is far enough that they might be flying - # home - check every half hour interval = 30 if battery is not None and battery <= 33 and mindistance > 3: @@ -403,22 +414,24 @@ class Icloud(DeviceScanner): status = device.status(DEVICESTATUSSET) battery = status.get('batteryLevel', 0) * 100 location = status['location'] - if location: - self.determine_interval( - devicename, location['latitude'], - location['longitude'], battery) - interval = self._intervals.get(devicename, 1) - attrs[ATTR_INTERVAL] = interval - accuracy = location['horizontalAccuracy'] - kwargs['dev_id'] = dev_id - kwargs['host_name'] = status['name'] - kwargs['gps'] = (location['latitude'], - location['longitude']) - kwargs['battery'] = battery - kwargs['gps_accuracy'] = accuracy - kwargs[ATTR_ATTRIBUTES] = attrs - self.see(**kwargs) - self.seen_devices[devicename] = True + if location and location['horizontalAccuracy']: + horizontal_accuracy = int(location['horizontalAccuracy']) + if horizontal_accuracy < self._gps_accuracy_threshold: + self.determine_interval( + devicename, location['latitude'], + location['longitude'], battery) + interval = self._intervals.get(devicename, 1) + attrs[ATTR_INTERVAL] = interval + accuracy = location['horizontalAccuracy'] + kwargs['dev_id'] = dev_id + kwargs['host_name'] = status['name'] + kwargs['gps'] = (location['latitude'], + location['longitude']) + kwargs['battery'] = battery + kwargs['gps_accuracy'] = accuracy + kwargs[ATTR_ATTRIBUTES] = attrs + self.see(**kwargs) + self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") @@ -434,7 +447,7 @@ class Icloud(DeviceScanner): device.play_sound() def update_icloud(self, devicename=None): - """Authenticate against iCloud and scan for devices.""" + """Request device information from iCloud and update device_tracker.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: @@ -443,13 +456,13 @@ class Icloud(DeviceScanner): try: if devicename is not None: if devicename in self.devices: - self.devices[devicename].location() + self.update_device(devicename) else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: - self.devices[device].location() + self.update_device(device) except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") From f516cc7dc62134e5952529779e6dd5821a4ee6f9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 8 May 2018 16:10:03 -0600 Subject: [PATCH 653/924] Adds useful attributes to RainMachine programs and zones (#14087) * Starting to add attributes * All attributes added to programs * Basic zone attributes in place * Added advanced properties for zones * Working to move common logic into component + dispatcher * We shouldn't calculate the MAC with every entity * Small fixes * Small adjustments * Owner-requested changes * Restart * Restart part 2 * Added ID attribute to each switch * Collaborator-requested changes --- homeassistant/components/rainmachine.py | 57 +++- .../components/switch/rainmachine.py | 287 ++++++++++++------ 2 files changed, 247 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py index 99cec53c2ed..f2d5893d60b 100644 --- a/homeassistant/components/rainmachine.py +++ b/homeassistant/components/rainmachine.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/rainmachine/ """ import logging -from datetime import timedelta import voluptuous as vol -from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES) + ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['regenmaschine==0.4.1'] @@ -26,11 +27,11 @@ NOTIFICATION_TITLE = 'RainMachine Component Setup' CONF_ZONE_RUN_TIME = 'zone_run_time' DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' DEFAULT_PORT = 8080 DEFAULT_SSL = True -MIN_SCAN_TIME = timedelta(seconds=1) -MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) SWITCH_SCHEMA = vol.Schema({ vol.Optional(CONF_ZONE_RUN_TIME): @@ -68,8 +69,7 @@ def setup(hass, config): auth = Authenticator.create_local( ip_address, password, port=port, https=ssl) client = Client(auth) - mac = client.provision.wifi()['macAddress'] - hass.data[DATA_RAINMACHINE] = (client, mac) + hass.data[DATA_RAINMACHINE] = RainMachine(client) except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: _LOGGER.error('An error occurred: %s', str(exc_info)) hass.components.persistent_notification.create( @@ -87,3 +87,46 @@ def setup(hass, config): _LOGGER.debug('Setup complete') return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, + rainmachine, + rainmachine_type, + rainmachine_entity_id, + icon=DEFAULT_ICON): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._icon = icon + self._rainmachine_type = rainmachine_type + self._rainmachine_entity_id = rainmachine_entity_id + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace( + ':', ''), self._rainmachine_type, + self._rainmachine_entity_id) diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 8306b323330..beb00eeca44 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,26 +1,118 @@ -"""Implements a RainMachine sprinkler controller for Home Assistant.""" +""" +This component provides support for RainMachine programs and zones. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.rainmachine/ +""" from logging import getLogger from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, - MIN_SCAN_TIME_FORCED) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, + RainMachineEntity) +from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.util import Throttle +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) DEPENDENCIES = ['rainmachine'] _LOGGER = getLogger(__name__) +ATTR_AREA = 'area' +ATTR_CS_ON = 'cs_on' +ATTR_CURRENT_CYCLE = 'current_cycle' ATTR_CYCLES = 'cycles' -ATTR_TOTAL_DURATION = 'total_duration' +ATTR_DELAY = 'delay' +ATTR_DELAY_ON = 'delay_on' +ATTR_FIELD_CAPACITY = 'field_capacity' +ATTR_NO_CYCLES = 'number_of_cycles' +ATTR_PRECIP_RATE = 'sprinkler_head_precipitation_rate' +ATTR_RESTRICTIONS = 'restrictions' +ATTR_SLOPE = 'slope' +ATTR_SOAK = 'soak' +ATTR_SOIL_TYPE = 'soil_type' +ATTR_SPRINKLER_TYPE = 'sprinkler_head_type' +ATTR_STATUS = 'status' +ATTR_SUN_EXPOSURE = 'sun_exposure' +ATTR_VEGETATION_TYPE = 'vegetation_type' +ATTR_ZONES = 'zones' DEFAULT_ZONE_RUN = 60 * 10 +DAYS = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday' +] + +PROGRAM_STATUS_MAP = { + 0: 'Not Running', + 1: 'Running', + 2: 'Queued' +} + +SOIL_TYPE_MAP = { + 0: 'Not Set', + 1: 'Clay Loam', + 2: 'Silty Clay', + 3: 'Clay', + 4: 'Loam', + 5: 'Sandy Loam', + 6: 'Loamy Sand', + 7: 'Sand', + 8: 'Sandy Clay', + 9: 'Silt Loam', + 10: 'Silt', + 99: 'Other' +} + +SLOPE_TYPE_MAP = { + 0: 'Not Set', + 1: 'Flat', + 2: 'Moderate', + 3: 'High', + 4: 'Very High', + 99: 'Other' +} + +SPRINKLER_TYPE_MAP = { + 0: 'Not Set', + 1: 'Popup Spray', + 2: 'Rotors', + 3: 'Surface Drip', + 4: 'Bubblers', + 99: 'Other' +} + +SUN_EXPOSURE_MAP = { + 0: 'Not Set', + 1: 'Full Sun', + 2: 'Partial Shade', + 3: 'Full Shade' +} + +VEGETATION_MAP = { + 0: 'Not Set', + 1: 'Not Set', + 2: 'Grass', + 3: 'Fruit Trees', + 4: 'Flowers', + 5: 'Vegetables', + 6: 'Citrus', + 7: 'Bushes', + 8: 'Xeriscape', + 99: 'Other' +} + def setup_platform(hass, config, add_devices, discovery_info=None): - """Set this component up under its platform.""" + """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -28,181 +120,196 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) - client, device_mac = hass.data.get(DATA_RAINMACHINE) + rainmachine = hass.data[DATA_RAINMACHINE] entities = [] - for program in client.programs.all().get('programs', {}): + for program in rainmachine.client.programs.all().get('programs', {}): if not program.get('active'): continue _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_mac, program)) + entities.append(RainMachineProgram(rainmachine, program)) - for zone in client.zones.all().get('zones', {}): + for zone in rainmachine.client.zones.all().get('zones', {}): if not zone.get('active'): continue _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_mac, zone, - zone_run_time)) + entities.append(RainMachineZone(rainmachine, zone, zone_run_time)) add_devices(entities, True) -class RainMachineEntity(SwitchDevice): +class RainMachineSwitch(RainMachineEntity, SwitchDevice): """A class to represent a generic RainMachine entity.""" - def __init__(self, client, device_mac, entity_json): + def __init__(self, rainmachine, rainmachine_type, obj): """Initialize a generic RainMachine entity.""" - self._api_type = 'remote' if client.auth.using_remote_api else 'local' - self._client = client - self._entity_json = entity_json + self._obj = obj + self._type = rainmachine_type - self.device_mac = device_mac - - self._attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION - } - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return 'mdi:water' + super().__init__(rainmachine, rainmachine_type, obj.get('uid')) @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" - return self._entity_json.get('active') - - @property - def rainmachine_entity_id(self) -> int: - """Return the RainMachine ID for this entity.""" - return self._entity_json.get('uid') + return self._obj.get('active') -class RainMachineProgram(RainMachineEntity): +class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" + def __init__(self, rainmachine, obj): + """Initialize.""" + super().__init__(rainmachine, 'program', obj) + @property def is_on(self) -> bool: """Return whether the program is running.""" - return bool(self._entity_json.get('status')) + return bool(self._obj.get('status')) @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {0}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._obj.get('name')) @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_program_{1}'.format( - self.device_mac.replace(':', ''), self.rainmachine_entity_id) + def zones(self) -> list: + """Return a list of active zones associated with this program.""" + return [z for z in self._obj['wateringTimes'] if z['active']] def turn_off(self, **kwargs) -> None: """Turn the program off.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.programs.stop(self.rainmachine_entity_id) - except exceptions.BrokenAPICall: - _LOGGER.error('programs.stop currently broken in remote API') - except exceptions.HTTPError as exc_info: + self.rainmachine.client.programs.stop(self._rainmachine_entity_id) + dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except HTTPError as exc_info: _LOGGER.error('Unable to turn off program "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the program on.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.programs.start(self.rainmachine_entity_id) - except exceptions.BrokenAPICall: - _LOGGER.error('programs.start currently broken in remote API') - except exceptions.HTTPError as exc_info: + self.rainmachine.client.programs.start(self._rainmachine_entity_id) + dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except HTTPError as exc_info: _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) - @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) def update(self) -> None: """Update info for the program.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._entity_json = self._client.programs.get( - self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self._obj = self.rainmachine.client.programs.get( + self._rainmachine_entity_id) + + self._attrs.update({ + ATTR_ID: self._obj['uid'], + ATTR_CS_ON: self._obj.get('cs_on'), + ATTR_CYCLES: self._obj.get('cycles'), + ATTR_DELAY: self._obj.get('delay'), + ATTR_DELAY_ON: self._obj.get('delay_on'), + ATTR_SOAK: self._obj.get('soak'), + ATTR_STATUS: + PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_ZONES: ', '.join(z['name'] for z in self.zones) + }) + except HTTPError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) -class RainMachineZone(RainMachineEntity): +class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" - def __init__(self, client, device_mac, zone_json, - zone_run_time): + def __init__(self, rainmachine, obj, zone_run_time): """Initialize a RainMachine zone.""" - super().__init__(client, device_mac, zone_json) + super().__init__(rainmachine, 'zone', obj) + + self._properties_json = {} self._run_time = zone_run_time - self._attrs.update({ - ATTR_CYCLES: self._entity_json.get('noOfCycles'), - ATTR_TOTAL_DURATION: self._entity_json.get('userDuration') - }) @property def is_on(self) -> bool: """Return whether the zone is running.""" - return bool(self._entity_json.get('state')) + return bool(self._obj.get('state')) @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {0}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._obj.get('name')) - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_zone_{1}'.format( - self.device_mac.replace(':', ''), self.rainmachine_entity_id) + @callback + def _program_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC, + self._program_updated) def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.zones.stop(self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self.rainmachine.client.zones.stop(self._rainmachine_entity_id) + except HTTPError as exc_info: _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the zone on.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.zones.start(self.rainmachine_entity_id, - self._run_time) - except exceptions.HTTPError as exc_info: + self.rainmachine.client.zones.start(self._rainmachine_entity_id, + self._run_time) + except HTTPError as exc_info: _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) - @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) def update(self) -> None: """Update info for the zone.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._entity_json = self._client.zones.get( - self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self._obj = self.rainmachine.client.zones.get( + self._rainmachine_entity_id) + + self._properties_json = self.rainmachine.client.zones.get( + self._rainmachine_entity_id, properties=True) + + self._attrs.update({ + ATTR_ID: self._obj['uid'], + ATTR_AREA: self._properties_json.get('waterSense').get('area'), + ATTR_CURRENT_CYCLE: self._obj.get('cycle'), + ATTR_FIELD_CAPACITY: + self._properties_json.get( + 'waterSense').get('fieldCapacity'), + ATTR_NO_CYCLES: self._obj.get('noOfCycles'), + ATTR_PRECIP_RATE: + self._properties_json.get( + 'waterSense').get('precipitationRate'), + ATTR_RESTRICTIONS: self._obj.get('restriction'), + ATTR_SLOPE: SLOPE_TYPE_MAP[self._properties_json.get('slope')], + ATTR_SOIL_TYPE: + SOIL_TYPE_MAP[self._properties_json.get('sun')], + ATTR_SPRINKLER_TYPE: + SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')], + ATTR_SUN_EXPOSURE: + SUN_EXPOSURE_MAP[self._properties_json.get('sun')], + ATTR_VEGETATION_TYPE: + VEGETATION_MAP[self._obj.get('type')], + }) + except HTTPError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) From 62313946149154362189c1979720e5e58dc59ed6 Mon Sep 17 00:00:00 2001 From: Mario Di Raimondo Date: Wed, 9 May 2018 00:35:03 +0200 Subject: [PATCH 654/924] Waze Travel Time: optional inclusive/exclusive filters (#14000) * Waze Travel Time: optional inclusive/exclusive filters Added optional `inc_filter` and `excl_filter' params that allow to refine the reported routes: the first is not always the best/desired. A simple case-insensitive filtering (no regular expression) is used. * fix line lenght * fix spaces * Rename var * Fix typo * Fix missing var --- .../components/sensor/waze_travel_time.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 47589f33530..dbcfcb9cc27 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -26,6 +26,8 @@ ATTR_ROUTE = 'route' CONF_ATTRIBUTION = "Data provided by the Waze.com" CONF_DESTINATION = 'destination' CONF_ORIGIN = 'origin' +CONF_INCL_FILTER = 'incl_filter' +CONF_EXCL_FILTER = 'excl_filter' DEFAULT_NAME = 'Waze Travel Time' @@ -40,6 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_REGION): vol.In(REGIONS), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_INCL_FILTER): cv.string, + vol.Optional(CONF_EXCL_FILTER): cv.string, }) @@ -49,9 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) origin = config.get(CONF_ORIGIN) region = config.get(CONF_REGION) + incl_filter = config.get(CONF_INCL_FILTER) + excl_filter = config.get(CONF_EXCL_FILTER) try: - waze_data = WazeRouteData(origin, destination, region) + waze_data = WazeRouteData( + origin, destination, region, incl_filter, excl_filter) except requests.exceptions.HTTPError as error: _LOGGER.error("%s", error) return @@ -109,11 +116,13 @@ class WazeTravelTime(Entity): class WazeRouteData(object): """Get data from Waze.""" - def __init__(self, origin, destination, region): + def __init__(self, origin, destination, region, incl_filter, excl_filter): """Initialize the data object.""" self._destination = destination self._origin = origin self._region = region + self._incl_filter = incl_filter + self._excl_filter = excl_filter self.data = {} @Throttle(SCAN_INTERVAL) @@ -125,6 +134,12 @@ class WazeRouteData(object): params = WazeRouteCalculator.WazeRouteCalculator( self._origin, self._destination, self._region, None) results = params.calc_all_routes_info() + if self._incl_filter is not None: + results = {k: v for k, v in results.items() if + self._incl_filter.lower() in k.lower()} + if self._excl_filter is not None: + results = {k: v for k, v in results.items() if + self._excl_filter.lower() not in k.lower()} best_route = next(iter(results)) (duration, distance) = results[best_route] best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') From 50cea778879e64a9b52072ea641c5273ccaf916f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 20:48:46 -0400 Subject: [PATCH 655/924] Bump frontend to 20180509.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b4eb6df07e1..0d267077991 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180505.0'] +REQUIREMENTS = ['home-assistant-frontend==20180509.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0dcfdbcce70..5e7df874f17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 976f4d87280..a25f36a8195 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d43e6a28883dc3a513b9dc99002de4f15703c58d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 9 May 2018 02:54:38 +0200 Subject: [PATCH 656/924] Ignore NaN values for influxdb (#14347) * Ignore NaN values for influxdb * Catch TypeError --- homeassistant/components/influxdb.py | 10 +++++++--- tests/components/test_influxdb.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 1f7f9f6262f..6d54324542a 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -9,6 +9,7 @@ import re import queue import threading import time +import math import requests.exceptions import voluptuous as vol @@ -220,9 +221,12 @@ def setup(hass, config): json['fields'][key] = float( RE_DECIMAL.sub('', new_value)) - # Infinity is not a valid float in InfluxDB - if (key, float("inf")) in json['fields'].items(): - del json['fields'][key] + # Infinity and NaN are not valid floats in InfluxDB + try: + if not math.isfinite(json['fields'][key]): + del json['fields'][key] + except (KeyError, TypeError): + pass json['tags'].update(tags) diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index c909a8488be..e2323aca855 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -217,7 +217,7 @@ class TestInfluxDB(unittest.TestCase): """Test the event listener for missing units.""" self._setup() - attrs = {'bignumstring': "9" * 999} + attrs = {'bignumstring': '9' * 999, 'nonumstring': 'nan'} state = mock.MagicMock( state=8, domain='fake', entity_id='fake.entity-id', object_id='entity', attributes=attrs) From 01ec4a7afdf616d2ff37a266c48e33804b16bab6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 20:48:46 -0400 Subject: [PATCH 657/924] Bump frontend to 20180509.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b4eb6df07e1..0d267077991 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180505.0'] +REQUIREMENTS = ['home-assistant-frontend==20180509.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6ddf0f81f61..6bcd267e456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 976f4d87280..a25f36a8195 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 2d0e3c14021d51ce7c780720d61ec303718dce90 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 9 May 2018 02:54:38 +0200 Subject: [PATCH 658/924] Ignore NaN values for influxdb (#14347) * Ignore NaN values for influxdb * Catch TypeError --- homeassistant/components/influxdb.py | 10 +++++++--- tests/components/test_influxdb.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 1f7f9f6262f..6d54324542a 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -9,6 +9,7 @@ import re import queue import threading import time +import math import requests.exceptions import voluptuous as vol @@ -220,9 +221,12 @@ def setup(hass, config): json['fields'][key] = float( RE_DECIMAL.sub('', new_value)) - # Infinity is not a valid float in InfluxDB - if (key, float("inf")) in json['fields'].items(): - del json['fields'][key] + # Infinity and NaN are not valid floats in InfluxDB + try: + if not math.isfinite(json['fields'][key]): + del json['fields'][key] + except (KeyError, TypeError): + pass json['tags'].update(tags) diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index c909a8488be..e2323aca855 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -217,7 +217,7 @@ class TestInfluxDB(unittest.TestCase): """Test the event listener for missing units.""" self._setup() - attrs = {'bignumstring': "9" * 999} + attrs = {'bignumstring': '9' * 999, 'nonumstring': 'nan'} state = mock.MagicMock( state=8, domain='fake', entity_id='fake.entity-id', object_id='entity', attributes=attrs) From f406fd57ac52eea2f2fc0ab12e8b9c914b64f741 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 20:55:35 -0400 Subject: [PATCH 659/924] Version bump to 0.69.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d54bf6356d..60bc6a78213 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From a91c1bc668e6a5ebee49ee2239d90a0a2feb4ea8 Mon Sep 17 00:00:00 2001 From: Mal Curtis Date: Wed, 9 May 2018 14:33:38 +1200 Subject: [PATCH 660/924] Add zone 3 for Onkyo media player (#14295) * Add zone 3 for Onkyo media player * CR Updates * Fix travis lint errors --- .../components/media_player/onkyo.py | 89 ++++++++++++++----- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 39c278ff95d..71b74868544 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' CONF_MAX_VOLUME = 'max_volume' -CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' SUPPORTED_MAX_VOLUME = 80 @@ -47,9 +46,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, - vol.Optional(CONF_ZONE2, default=False): cv.boolean, }) +TIMEOUT_MESSAGE = 'Timeout waiting for response.' + + +def determine_zones(receiver): + """Determine what zones are available for the receiver.""" + out = { + "zone2": False, + "zone3": False, + } + try: + _LOGGER.debug("Checking for zone 2 capability") + receiver.raw("ZPW") + out["zone2"] = True + except ValueError as error: + if str(error) != TIMEOUT_MESSAGE: + raise error + _LOGGER.debug("Zone 2 timed out, assuming no functionality") + try: + _LOGGER.debug("Checking for zone 3 capability") + receiver.raw("PW3") + out["zone3"] = True + except ValueError as error: + if str(error) != TIMEOUT_MESSAGE: + raise error + _LOGGER.debug("Zone 3 timed out, assuming no functionality") + + return out + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Onkyo platform.""" @@ -61,20 +87,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if CONF_HOST in config and host not in KNOWN_HOSTS: try: + receiver = eiscp.eISCP(host) hosts.append(OnkyoDevice( - eiscp.eISCP(host), config.get(CONF_SOURCES), + receiver, + config.get(CONF_SOURCES), name=config.get(CONF_NAME), max_volume=config.get(CONF_MAX_VOLUME), )) KNOWN_HOSTS.append(host) - # Add Zone2 if configured - if config.get(CONF_ZONE2): + zones = determine_zones(receiver) + + # Add Zone2 if available + if zones["zone2"]: _LOGGER.debug("Setting up zone 2") - hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host), - config.get(CONF_SOURCES), - name=config.get(CONF_NAME) + - " Zone 2")) + hosts.append(OnkyoDeviceZone( + "2", receiver, + config.get(CONF_SOURCES), + name="{} Zone 2".format(config[CONF_NAME]))) + # Add Zone3 if available + if zones["zone3"]: + _LOGGER.debug("Setting up zone 3") + hosts.append(OnkyoDeviceZone( + "3", receiver, + config.get(CONF_SOURCES), + name="{} Zone 3".format(config[CONF_NAME]))) except OSError: _LOGGER.error("Unable to connect to receiver at %s", host) else: @@ -227,12 +264,17 @@ class OnkyoDevice(MediaPlayerDevice): self.command('input-selector {}'.format(source)) -class OnkyoDeviceZone2(OnkyoDevice): - """Representation of an Onkyo device's zone 2.""" +class OnkyoDeviceZone(OnkyoDevice): + """Representation of an Onkyo device's extra zone.""" + + def __init__(self, zone, receiver, sources, name=None): + """Initialize the Zone with the zone identifier.""" + self._zone = zone + super().__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" - status = self.command('zone2.power=query') + status = self.command('zone{}.power=query'.format(self._zone)) if not status: return @@ -242,9 +284,10 @@ class OnkyoDeviceZone2(OnkyoDevice): self._pwstate = STATE_OFF return - volume_raw = self.command('zone2.volume=query') - mute_raw = self.command('zone2.muting=query') - current_source_raw = self.command('zone2.selector=query') + volume_raw = self.command('zone{}.volume=query'.format(self._zone)) + mute_raw = self.command('zone{}.muting=query'.format(self._zone)) + current_source_raw = self.command( + 'zone{}.selector=query'.format(self._zone)) if not (volume_raw and mute_raw and current_source_raw): return @@ -268,33 +311,33 @@ class OnkyoDeviceZone2(OnkyoDevice): def turn_off(self): """Turn the media player off.""" - self.command('zone2.power=standby') + self.command('zone{}.power=standby'.format(self._zone)) def set_volume_level(self, volume): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command('zone2.volume={}'.format(int(volume*80))) + self.command('zone{}.volume={}'.format(self._zone, int(volume*80))) def volume_up(self): """Increase volume by 1 step.""" - self.command('zone2.volume=level-up') + self.command('zone{}.volume=level-up'.format(self._zone)) def volume_down(self): """Decrease volume by 1 step.""" - self.command('zone2.volume=level-down') + self.command('zone{}.volume=level-down'.format(self._zone)) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: - self.command('zone2.muting=on') + self.command('zone{}.muting=on'.format(self._zone)) else: - self.command('zone2.muting=off') + self.command('zone{}.muting=off'.format(self._zone)) def turn_on(self): """Turn the media player on.""" - self.command('zone2.power=on') + self.command('zone{}.power=on'.format(self._zone)) def select_source(self, source): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command('zone2.selector={}'.format(source)) + self.command('zone{}.selector={}'.format(self._zone, source)) From cf8562a030ba79d9962848f0254d362408bc3956 Mon Sep 17 00:00:00 2001 From: Nash Kaminski Date: Wed, 9 May 2018 04:26:29 -0500 Subject: [PATCH 661/924] Support control of away mode and hold mode in Venstar component. Correctly detect humidifiers. (#14256) * Implement support for away mode and hold mode in Venstar component * Fix Venstar humidifier capability detection * Add option to configure humidifier control in Venstar component * style fix: add missing space and resolve pylint issues * Remove quotes --- homeassistant/components/climate/venstar.py | 69 ++++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 6e63cc4092b..c2b82e1cc84 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -11,9 +11,11 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, + SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, + SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + ClimateDevice) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, @@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__) ATTR_FAN_STATE = 'fan_state' ATTR_HVAC_STATE = 'hvac_state' +CONF_HUMIDIFIER = 'humidifier' + DEFAULT_SSL = False VALID_FAN_STATES = [STATE_ON, STATE_AUTO] VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO] +HOLD_MODE_OFF = 'off' +HOLD_MODE_TEMPERATURE = 'temperature' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HUMIDIFIER, default=True): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_TIMEOUT, default=5): vol.All(vol.Coerce(int), vol.Range(min=1)), @@ -50,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) timeout = config.get(CONF_TIMEOUT) + humidifier = config.get(CONF_HUMIDIFIER) if config.get(CONF_SSL): proto = 'https' @@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): addr=host, timeout=timeout, user=username, password=password, proto=proto) - add_devices([VenstarThermostat(client)], True) + add_devices([VenstarThermostat(client, humidifier)], True) class VenstarThermostat(ClimateDevice): """Representation of a Venstar thermostat.""" - def __init__(self, client): + def __init__(self, client, humidifier): """Initialize the thermostat.""" self._client = client + self._humidifier = humidifier def update(self): """Update the data from the thermostat.""" @@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice): def supported_features(self): """Return the list of supported features.""" features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE) + SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE | + SUPPORT_HOLD_MODE) if self._client.mode == self._client.MODE_AUTO: features |= (SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW) - if self._client.hum_active == 1: - features |= SUPPORT_TARGET_HUMIDITY + if (self._humidifier and + hasattr(self._client, 'hum_active')): + features |= (SUPPORT_TARGET_HUMIDITY | + SUPPORT_TARGET_HUMIDITY_HIGH | + SUPPORT_TARGET_HUMIDITY_LOW) return features @@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice): """Return the maximum humidity. Hardcoded to 60 in API.""" return 60 + @property + def is_away_mode_on(self): + """Return the status of away mode.""" + return self._client.away == self._client.AWAY_AWAY + + @property + def current_hold_mode(self): + """Return the status of hold mode.""" + if self._client.schedule == 0: + return HOLD_MODE_TEMPERATURE + return HOLD_MODE_OFF + def _set_operation_mode(self, operation_mode): """Change the operation mode (internal).""" if operation_mode == STATE_HEAT: @@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice): if not success: _LOGGER.error("Failed to change the target humidity level") + + def set_hold_mode(self, hold_mode): + """Set the hold mode.""" + if hold_mode == HOLD_MODE_TEMPERATURE: + success = self._client.set_schedule(0) + elif hold_mode == HOLD_MODE_OFF: + success = self._client.set_schedule(1) + else: + _LOGGER.error("Unknown hold mode: %s", hold_mode) + success = False + + if not success: + _LOGGER.error("Failed to change the schedule/hold state") + + def turn_away_mode_on(self): + """Activate away mode.""" + success = self._client.set_away(self._client.AWAY_AWAY) + + if not success: + _LOGGER.error("Failed to activate away mode") + + def turn_away_mode_off(self): + """Deactivate away mode.""" + success = self._client.set_away(self._client.AWAY_HOME) + + if not success: + _LOGGER.error("Failed to deactivate away mode") From 2c566072f5094f1e0c0c97f2ccb00c91fa833181 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 9 May 2018 11:31:18 +0200 Subject: [PATCH 662/924] Upgrade keyring to 12.2.0 and keyrings.alt to 3.1 (#14355) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 82a57c90263..11e337a76b5 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.0.0', 'keyrings.alt==3.0'] +REQUIREMENTS = ['keyring==12.2.0', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 5e7df874f17..1400237a683 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -459,10 +459,10 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.0.0 +keyring==12.2.0 # homeassistant.scripts.keyring -keyrings.alt==3.0 +keyrings.alt==3.1 # homeassistant.components.eufy lakeside==0.5 From 0f3ec94fbaf13d590837037ed54d62764643632d Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 9 May 2018 13:44:42 +0200 Subject: [PATCH 663/924] debug++ for multiple volume controls (#14349) Be less noisy for those who have more volume controls than one, mentioned in #13022. --- homeassistant/components/media_player/songpal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index 955456f2465..5d0962775f0 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -151,8 +151,8 @@ class SongpalDevice(MediaPlayerDevice): return if len(volumes) > 1: - _LOGGER.warning("Got %s volume controls, using the first one", - volumes) + _LOGGER.debug("Got %s volume controls, using the first one", + volumes) volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) From 5ec7fc7ddb23c83f6718ef8c544fff5265073112 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 04:38:11 -0400 Subject: [PATCH 664/924] Backend tweaks to make authorization work (#14339) * Backend tweaks to make authorization work * Lint * Add test * Validate redirect uris * Fix tests * Fix tests * Lint --- homeassistant/auth.py | 19 ++++++-- homeassistant/components/api.py | 3 +- homeassistant/components/auth/__init__.py | 21 ++++++--- homeassistant/components/auth/client.py | 38 ++++++++++----- homeassistant/components/frontend/__init__.py | 22 +++++++-- homeassistant/components/http/auth.py | 7 ++- homeassistant/components/http/view.py | 6 --- homeassistant/components/websocket_api.py | 18 +++++--- homeassistant/helpers/data_entry_flow.py | 2 +- tests/components/auth/__init__.py | 4 +- tests/components/auth/test_client.py | 6 +-- tests/components/auth/test_init.py | 5 +- tests/components/auth/test_init_link_user.py | 8 ++-- tests/components/auth/test_init_login_flow.py | 5 +- tests/components/conftest.py | 16 +++++++ tests/components/test_api.py | 8 ++-- tests/components/test_websocket_api.py | 46 +++++++++++++++++++ 17 files changed, 176 insertions(+), 58 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 55de9309954..5c9d437e067 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -210,6 +210,7 @@ class Client: name = attr.ib(type=str) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) secret = attr.ib(type=str, default=attr.Factory(generate_secret)) + redirect_uris = attr.ib(type=list, default=attr.Factory(list)) async def load_auth_provider_module(hass, provider): @@ -340,9 +341,11 @@ class AuthManager: """Get an access token.""" return self.access_tokens.get(token) - async def async_create_client(self, name): + async def async_create_client(self, name, *, redirect_uris=None, + no_secret=False): """Create a new client.""" - return await self._store.async_create_client(name) + return await self._store.async_create_client( + name, redirect_uris, no_secret) async def async_get_client(self, client_id): """Get a client.""" @@ -477,12 +480,20 @@ class AuthStore: return None - async def async_create_client(self, name): + async def async_create_client(self, name, redirect_uris, no_secret): """Create a new client.""" if self.clients is None: await self.async_load() - client = Client(name) + kwargs = { + 'name': name, + 'redirect_uris': redirect_uris + } + + if no_secret: + kwargs['secret'] = None + + client = Client(**kwargs) self.clients[client.id] = client await self.async_save() return client diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 83e05dae641..dc34006ad03 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -356,7 +356,8 @@ class APIErrorLog(HomeAssistantView): async def get(self, request): """Retrieve API error log.""" - return await self.file(request, request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse( + request.app['hass'].data[DATA_LOGGING]) async def async_services_json(hass): diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d4b4b0f4591..0f7295a41e0 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -144,7 +144,7 @@ class AuthProvidersView(HomeAssistantView): requires_auth = False @verify_client - async def get(self, request, client_id): + async def get(self, request, client): """Get available auth providers.""" return self.json([{ 'name': provider.name, @@ -166,8 +166,15 @@ class LoginFlowIndexView(FlowManagerIndexView): # pylint: disable=arguments-differ @verify_client - async def post(self, request, client_id): + @RequestDataValidator(vol.Schema({ + vol.Required('handler'): vol.Any(str, list), + vol.Required('redirect_uri'): str, + })) + async def post(self, request, client, data): """Create a new login flow.""" + if data['redirect_uri'] not in client.redirect_uris: + return self.json_message('invalid redirect uri', ) + # pylint: disable=no-value-for-parameter return await super().post(request) @@ -192,7 +199,7 @@ class LoginFlowResourceView(FlowManagerResourceView): # pylint: disable=arguments-differ @verify_client @RequestDataValidator(vol.Schema(dict), allow_empty=True) - async def post(self, request, client_id, flow_id, data): + async def post(self, request, client, flow_id, data): """Handle progressing a login flow request.""" try: result = await self._flow_mgr.async_configure(flow_id, data) @@ -205,7 +212,7 @@ class LoginFlowResourceView(FlowManagerResourceView): return self.json(self._prepare_result_json(result)) result.pop('data') - result['result'] = self._store_credentials(client_id, result['result']) + result['result'] = self._store_credentials(client.id, result['result']) return self.json(result) @@ -222,7 +229,7 @@ class GrantTokenView(HomeAssistantView): self._retrieve_credentials = retrieve_credentials @verify_client - async def post(self, request, client_id): + async def post(self, request, client): """Grant a token.""" hass = request.app['hass'] data = await request.post() @@ -230,11 +237,11 @@ class GrantTokenView(HomeAssistantView): if grant_type == 'authorization_code': return await self._async_handle_auth_code( - hass, client_id, data) + hass, client.id, data) elif grant_type == 'refresh_token': return await self._async_handle_refresh_token( - hass, client_id, data) + hass, client.id, data) return self.json({ 'error': 'unsupported_grant_type', diff --git a/homeassistant/components/auth/client.py b/homeassistant/components/auth/client.py index 28d72aefe0f..122c3032188 100644 --- a/homeassistant/components/auth/client.py +++ b/homeassistant/components/auth/client.py @@ -11,15 +11,15 @@ def verify_client(method): @wraps(method) async def wrapper(view, request, *args, **kwargs): """Verify client id/secret before doing request.""" - client_id = await _verify_client(request) + client = await _verify_client(request) - if client_id is None: + if client is None: return view.json({ 'error': 'invalid_client', }, status_code=401) return await method( - view, request, *args, client_id=client_id, **kwargs) + view, request, *args, **kwargs, client=client) return wrapper @@ -46,18 +46,34 @@ async def _verify_client(request): client_id, client_secret = decoded.split(':', 1) except ValueError: # If no ':' in decoded - return None + client_id, client_secret = decoded, None - client = await request.app['hass'].auth.async_get_client(client_id) + return await async_secure_get_client( + request.app['hass'], client_id, client_secret) + + +async def async_secure_get_client(hass, client_id, client_secret): + """Get a client id/secret in consistent time.""" + client = await hass.auth.async_get_client(client_id) if client is None: - # Still do a compare so we run same time as if a client was found. - hmac.compare_digest(client_secret.encode('utf-8'), - client_secret.encode('utf-8')) + if client_secret is not None: + # Still do a compare so we run same time as if a client was found. + hmac.compare_digest(client_secret.encode('utf-8'), + client_secret.encode('utf-8')) return None - if hmac.compare_digest(client_secret.encode('utf-8'), - client.secret.encode('utf-8')): - return client_id + if client.secret is None: + return client + + elif client_secret is None: + # Still do a compare so we run same time as if a secret was passed. + hmac.compare_digest(client.secret.encode('utf-8'), + client.secret.encode('utf-8')) + return None + + elif hmac.compare_digest(client_secret.encode('utf-8'), + client.secret.encode('utf-8')): + return client return None diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0d267077991..c30e0dfb69f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -296,6 +296,15 @@ def add_manifest_json_key(key, val): @asyncio.coroutine def async_setup(hass, config): """Set up the serving of the frontend.""" + if list(hass.auth.async_auth_providers): + client = yield from hass.auth.async_create_client( + 'Home Assistant Frontend', + redirect_uris=['/'], + no_secret=True, + ) + else: + client = None + hass.components.websocket_api.async_register_command( WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) hass.http.register_view(ManifestJSONView) @@ -353,7 +362,7 @@ def async_setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) - index_view = IndexView(repo_path, js_version) + index_view = IndexView(repo_path, js_version, client) hass.http.register_view(index_view) @asyncio.coroutine @@ -451,10 +460,11 @@ class IndexView(HomeAssistantView): requires_auth = False extra_urls = ['/states', '/states/{extra}'] - def __init__(self, repo_path, js_option): + def __init__(self, repo_path, js_option, client): """Initialize the frontend view.""" self.repo_path = repo_path self.js_option = js_option + self.client = client self._template_cache = {} def get_template(self, latest): @@ -508,7 +518,7 @@ class IndexView(HomeAssistantView): extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5 - resp = template.render( + template_params = dict( no_auth=no_auth, panel_url=panel_url, panels=hass.data[DATA_PANELS], @@ -516,7 +526,11 @@ class IndexView(HomeAssistantView): extra_urls=hass.data[extra_key], ) - return web.Response(text=resp, content_type='text/html') + if self.client is not None: + template_params['client_id'] = self.client.id + + return web.Response(text=template.render(**template_params), + content_type='text/html') class ManifestJSONView(HomeAssistantView): diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 5558063c5c4..c4723abccee 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -81,7 +81,12 @@ async def async_validate_auth_header(api_password, request): if hdrs.AUTHORIZATION not in request.headers: return False - auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + try: + auth_type, auth_val = \ + request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + except ValueError: + # If no space in authorization header + return False if auth_type == 'Basic': decoded = base64.b64decode(auth_val).decode('utf-8') diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 81c6ea4bcfb..3de276564eb 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -51,12 +51,6 @@ class HomeAssistantView(object): data['code'] = message_code return self.json(data, status_code, headers=headers) - # pylint: disable=no-self-use - async def file(self, request, fil): - """Return a file.""" - assert isinstance(fil, str), 'only string paths allowed' - return web.FileResponse(fil) - def register(self, router): """Register the view with a router.""" assert self.url is not None, 'No url set for view' diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 4989f4f0db2..11094acd3e2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -60,7 +60,8 @@ JSON_DUMP = partial(json.dumps, cls=JSONEncoder) AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('type'): TYPE_AUTH, - vol.Required('api_password'): str, + vol.Exclusive('api_password', 'auth'): str, + vol.Exclusive('access_token', 'auth'): str, }) # Minimal requirements of a message @@ -318,15 +319,18 @@ class ActiveConnection: msg = await wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if validate_password(request, msg['api_password']): - authenticated = True + if 'api_password' in msg: + authenticated = validate_password( + request, msg['api_password']) - else: - self.debug("Invalid password") - await self.wsock.send_json( - auth_invalid_message('Invalid password')) + elif 'access_token' in msg: + authenticated = \ + msg['access_token'] in self.hass.auth.access_tokens if not authenticated: + self.debug("Invalid password") + await self.wsock.send_json( + auth_invalid_message('Invalid password')) await process_wrong_login(request) return wsock diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 913e90a859d..5a0b2ca56ea 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -44,7 +44,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema({ vol.Required('handler'): vol.Any(str, list), - })) + }, extra=vol.ALLOW_EXTRA)) async def post(self, request, data): """Handle a POST request.""" if isinstance(data['handler'], list): diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 3e5a59e8386..f0b205ff5ce 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -19,6 +19,7 @@ BASE_CONFIG = [{ CLIENT_ID = 'test-id' CLIENT_SECRET = 'test-secret' CLIENT_AUTH = BasicAuth(CLIENT_ID, CLIENT_SECRET) +CLIENT_REDIRECT_URI = 'http://example.com/callback' async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, @@ -31,7 +32,8 @@ async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, 'api_password': 'bla' } }) - client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET) + client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET, + redirect_uris=[CLIENT_REDIRECT_URI]) hass.auth._store.clients[client.id] = client if setup_api: await async_setup_component(hass, 'api', {}) diff --git a/tests/components/auth/test_client.py b/tests/components/auth/test_client.py index 2995a6ac81a..65ad22efae2 100644 --- a/tests/components/auth/test_client.py +++ b/tests/components/auth/test_client.py @@ -21,9 +21,9 @@ def mock_view(hass): name = 'bla' @verify_client - async def get(self, request, client_id): + async def get(self, request, client): """Handle GET request.""" - clients.append(client_id) + clients.append(client) hass.http.register_view(ClientView) return clients @@ -36,7 +36,7 @@ async def test_verify_client(hass, aiohttp_client, mock_view): resp = await http_client.get('/', auth=BasicAuth(client.id, client.secret)) assert resp.status == 200 - assert mock_view == [client.id] + assert mock_view[0] is client async def test_verify_client_no_auth_header(hass, aiohttp_client, mock_view): diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 5d9bf6b98cc..7cff04327b8 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,12 +1,13 @@ """Integration tests for the auth component.""" -from . import async_setup_auth, CLIENT_AUTH +from . import async_setup_auth, CLIENT_AUTH, CLIENT_REDIRECT_URI async def test_login_new_user_and_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" client = await async_setup_auth(hass, aiohttp_client, setup_api=True) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 44695bce202..853c002ba46 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,5 +1,5 @@ """Tests for the link user flow.""" -from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID +from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID, CLIENT_REDIRECT_URI async def async_get_code(hass, aiohttp_client): @@ -25,7 +25,8 @@ async def async_get_code(hass, aiohttp_client): client = await async_setup_auth(hass, aiohttp_client, config) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() @@ -56,7 +57,8 @@ async def async_get_code(hass, aiohttp_client): # Now authenticate with the 2nd flow resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', '2nd auth'] + 'handler': ['insecure_example', '2nd auth'], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_init_login_flow.py index 96fece6506b..ad39fba3997 100644 --- a/tests/components/auth/test_init_login_flow.py +++ b/tests/components/auth/test_init_login_flow.py @@ -1,7 +1,7 @@ """Tests for the login flow.""" from aiohttp.helpers import BasicAuth -from . import async_setup_auth, CLIENT_AUTH +from . import async_setup_auth, CLIENT_AUTH, CLIENT_REDIRECT_URI async def test_fetch_auth_providers(hass, aiohttp_client): @@ -34,7 +34,8 @@ async def test_invalid_username_password(hass, aiohttp_client): """Test we cannot get flows in progress.""" client = await async_setup_auth(hass, aiohttp_client) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 53caeb80783..8a1b934ab76 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,6 +3,8 @@ import pytest from homeassistant.setup import async_setup_component +from tests.common import MockUser + @pytest.fixture def hass_ws_client(aiohttp_client): @@ -20,3 +22,17 @@ def hass_ws_client(aiohttp_client): return websocket return create_client + + +@pytest.fixture +def hass_access_token(hass): + """Return an access token to access Home Assistant.""" + user = MockUser().add_to_hass(hass) + client = hass.loop.run_until_complete(hass.auth.async_create_client( + 'Access Token Fixture', + redirect_uris=['/'], + no_secret=True, + )) + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(user, client.id)) + yield hass.auth.async_create_access_token(refresh_token) diff --git a/tests/components/test_api.py b/tests/components/test_api.py index c9dae27d14c..f53010ef27f 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -12,8 +12,6 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.common import mock_coro - @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -420,14 +418,14 @@ async def test_api_error_log(hass, aiohttp_client): assert resp.status == 401 with patch( - 'homeassistant.components.http.view.HomeAssistantView.file', - return_value=mock_coro(web.Response(status=200, text='Hello')) + 'aiohttp.web.FileResponse', + return_value=web.Response(status=200, text='Hello') ) as mock_file: resp = await client.get(const.URL_API_ERROR_LOG, headers={ 'x-ha-access': 'yolo' }) assert len(mock_file.mock_calls) == 1 - assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING] + assert mock_file.mock_calls[0][1][0] == hass.data[DATA_LOGGING] assert resp.status == 200 assert await resp.text() == 'Hello' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 0a130e507d4..cff103142b0 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -313,3 +313,49 @@ def test_unknown_command(websocket_client): msg = yield from websocket_client.receive() assert msg.type == WSMsgType.close + + +async def test_auth_with_token(hass, aiohttp_client, hass_access_token): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + +async def test_auth_with_invalid_token(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': 'incorrect' + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID From 8d017b7678d654da25bcb171f9c15cfaa7a3cf80 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 10 May 2018 12:47:04 +0200 Subject: [PATCH 665/924] script/lint: Ensure there are files to test with pylint (#14363) --- script/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/script/lint b/script/lint index dc6884f4882..8ba14d8939e 100755 --- a/script/lint +++ b/script/lint @@ -8,7 +8,7 @@ echo '=================================================' echo '= FILES CHANGED =' echo '=================================================' if [ -z "$files" ] ; then - echo "No python file changed. Rather use: tox -e lint" + echo "No python file changed. Rather use: tox -e lint\n" exit fi printf "%s\n" $files @@ -19,5 +19,10 @@ flake8 --doctests $files echo "================" echo "LINT with pylint" echo "================" -pylint $(echo "$files" | grep -v '^tests.*') +pylint_files=$(echo "$files" | grep -v '^tests.*') +if [ -z "$pylint_files" ] ; then + echo "Only test files changed. Skipping\n" + exit +fi +pylint $pylint_files echo From eb2671f4bbbcb270784597ae8ddf166d7bc65119 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 May 2018 13:18:13 +0200 Subject: [PATCH 666/924] Update .coveragerc (#14368) --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1f2a8f8d233..28fe39430f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,8 @@ source = homeassistant omit = homeassistant/__main__.py homeassistant/scripts/*.py + homeassistant/util/async.py + homeassistant/monkey_patch.py homeassistant/helpers/typing.py homeassistant/helpers/signal.py From 6e831138b407776de549b29907c9efb04fe0a0ea Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 10 May 2018 19:59:23 +0200 Subject: [PATCH 667/924] Fix binary_sensor async_update (#14376) --- homeassistant/components/binary_sensor/zha.py | 3 ++- homeassistant/components/zha/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 756323f41d9..be61a9e9ba4 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -133,7 +133,8 @@ class BinarySensor(zha.Entity, BinarySensorDevice): from bellows.types.basic import uint16_t result = await zha.safe_read(self._endpoint.ias_zone, - ['zone_status']) + ['zone_status'], + allow_cache=False) state = result.get('zone_status', self._state) if isinstance(state, (int, uint16_t)): self._state = result.get('zone_status', self._state) & 3 diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 9d7556fc334..d293d4d07cd 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -410,7 +410,7 @@ def get_discovery_info(hass, discovery_info): return all_discovery_info.get(discovery_key, None) -async def safe_read(cluster, attributes): +async def safe_read(cluster, attributes, allow_cache=True): """Swallow all exceptions from network read. If we throw during initialization, setup fails. Rather have an entity that @@ -420,7 +420,7 @@ async def safe_read(cluster, attributes): try: result, _ = await cluster.read_attributes( attributes, - allow_cache=True, + allow_cache=allow_cache, ) return result except Exception: # pylint: disable=broad-except From ea01b127c277d3539a2b4d2362d73afa9b26b9b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 14:09:22 -0400 Subject: [PATCH 668/924] Add local auth provider (#14365) * Add local auth provider * Lint * Docstring --- homeassistant/auth.py | 22 +-- homeassistant/auth_providers/homeassistant.py | 181 ++++++++++++++++++ .../auth_providers/insecure_example.py | 14 +- homeassistant/scripts/auth.py | 78 ++++++++ tests/auth_providers/test_homeassistant.py | 124 ++++++++++++ tests/auth_providers/test_insecure_example.py | 18 +- tests/scripts/test_auth.py | 100 ++++++++++ 7 files changed, 501 insertions(+), 36 deletions(-) create mode 100644 homeassistant/auth_providers/homeassistant.py create mode 100644 homeassistant/scripts/auth.py create mode 100644 tests/auth_providers/test_homeassistant.py create mode 100644 tests/scripts/test_auth.py diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 5c9d437e067..2c6c95f9b42 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -15,7 +15,6 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.core import callback from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID -from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry from homeassistant.util import dt as dt_util @@ -36,22 +35,6 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) DATA_REQS = 'auth_reqs_processed' -class AuthError(HomeAssistantError): - """Generic authentication error.""" - - -class InvalidUser(AuthError): - """Raised when an invalid user has been specified.""" - - -class InvalidPassword(AuthError): - """Raised when an invalid password has been supplied.""" - - -class UnknownError(AuthError): - """When an unknown error occurs.""" - - def generate_secret(entropy=32): """Generate a secret. @@ -69,8 +52,9 @@ class AuthProvider: initialized = False - def __init__(self, store, config): + def __init__(self, hass, store, config): """Initialize an auth provider.""" + self.hass = hass self.store = store self.config = config @@ -284,7 +268,7 @@ async def _auth_provider_from_config(hass, store, config): provider_name, humanize_error(config, err)) return None - return AUTH_PROVIDERS[provider_name](store, config) + return AUTH_PROVIDERS[provider_name](hass, store, config) class AuthManager: diff --git a/homeassistant/auth_providers/homeassistant.py b/homeassistant/auth_providers/homeassistant.py new file mode 100644 index 00000000000..c2db193ce1a --- /dev/null +++ b/homeassistant/auth_providers/homeassistant.py @@ -0,0 +1,181 @@ +"""Home Assistant auth provider.""" +import base64 +from collections import OrderedDict +import hashlib +import hmac + +import voluptuous as vol + +from homeassistant import auth, data_entry_flow +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import json + + +PATH_DATA = '.users.json' + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ +}, extra=vol.PREVENT_EXTRA) + + +class InvalidAuth(HomeAssistantError): + """Raised when we encounter invalid authentication.""" + + +class InvalidUser(HomeAssistantError): + """Raised when invalid user is specified. + + Will not be raised when validating authentication. + """ + + +class Data: + """Hold the user data.""" + + def __init__(self, path, data): + """Initialize the user data store.""" + self.path = path + if data is None: + data = { + 'salt': auth.generate_secret(), + 'users': [] + } + self._data = data + + @property + def users(self): + """Return users.""" + return self._data['users'] + + def validate_login(self, username, password): + """Validate a username and password. + + Raises InvalidAuth if auth invalid. + """ + password = self.hash_password(password) + + found = None + + # Compare all users to avoid timing attacks. + for user in self._data['users']: + if username == user['username']: + found = user + + if found is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password, password) + raise InvalidAuth + + if not hmac.compare_digest(password, + base64.b64decode(found['password'])): + raise InvalidAuth + + def hash_password(self, password, for_storage=False): + """Encode a password.""" + hashed = hashlib.pbkdf2_hmac( + 'sha512', password.encode(), self._data['salt'].encode(), 100000) + if for_storage: + hashed = base64.b64encode(hashed).decode() + return hashed + + def add_user(self, username, password): + """Add a user.""" + if any(user['username'] == username for user in self.users): + raise InvalidUser + + self.users.append({ + 'username': username, + 'password': self.hash_password(password, True), + }) + + def change_password(self, username, new_password): + """Update the password of a user. + + Raises InvalidUser if user cannot be found. + """ + for user in self.users: + if user['username'] == username: + user['password'] = self.hash_password(new_password, True) + break + else: + raise InvalidUser + + def save(self): + """Save data.""" + json.save_json(self.path, self._data) + + +def load_data(path): + """Load auth data.""" + return Data(path, json.load_json(path, None)) + + +@auth.AUTH_PROVIDERS.register('homeassistant') +class HassAuthProvider(auth.AuthProvider): + """Auth provider based on a local storage of users in HASS config dir.""" + + DEFAULT_TITLE = 'Home Assistant Local' + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + async def async_validate_login(self, username, password): + """Helper to validate a username and password.""" + def validate(): + """Validate creds.""" + data = self._auth_data() + data.validate_login(username, password) + + await self.hass.async_add_job(validate) + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + username = flow_result['username'] + + for credential in await self.async_credentials(): + if credential.data['username'] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + 'username': username + }) + + def _auth_data(self): + """Return the auth provider data.""" + return load_data(self.hass.config.path(PATH_DATA)) + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + await self._auth_provider.async_validate_login( + user_input['username'], user_input['password']) + except InvalidAuth: + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data=user_input + ) + + schema = OrderedDict() + schema['username'] = str + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/auth_providers/insecure_example.py b/homeassistant/auth_providers/insecure_example.py index 8538e8c2f3e..a8e8cd0cb0e 100644 --- a/homeassistant/auth_providers/insecure_example.py +++ b/homeassistant/auth_providers/insecure_example.py @@ -4,6 +4,7 @@ import hmac import voluptuous as vol +from homeassistant.exceptions import HomeAssistantError from homeassistant import auth, data_entry_flow from homeassistant.core import callback @@ -20,6 +21,10 @@ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + @auth.AUTH_PROVIDERS.register('insecure_example') class ExampleAuthProvider(auth.AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" @@ -43,18 +48,15 @@ class ExampleAuthProvider(auth.AuthProvider): # Do one more compare to make timing the same as if user was found. hmac.compare_digest(password.encode('utf-8'), password.encode('utf-8')) - raise auth.InvalidUser + raise InvalidAuthError if not hmac.compare_digest(user['password'].encode('utf-8'), password.encode('utf-8')): - raise auth.InvalidPassword + raise InvalidAuthError async def async_get_or_create_credentials(self, flow_result): """Get credentials based on the flow result.""" username = flow_result['username'] - password = flow_result['password'] - - self.async_validate_login(username, password) for credential in await self.async_credentials(): if credential.data['username'] == username: @@ -96,7 +98,7 @@ class LoginFlow(data_entry_flow.FlowHandler): try: self._auth_provider.async_validate_login( user_input['username'], user_input['password']) - except (auth.InvalidUser, auth.InvalidPassword): + except InvalidAuthError: errors['base'] = 'invalid_auth' if not errors: diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py new file mode 100644 index 00000000000..b4f1ddd2f11 --- /dev/null +++ b/homeassistant/scripts/auth.py @@ -0,0 +1,78 @@ +"""Script to manage users for the Home Assistant auth provider.""" +import argparse +import os + +from homeassistant.config import get_default_config_dir +from homeassistant.auth_providers import homeassistant as hass_auth + + +def run(args): + """Handle Home Assistant auth provider script.""" + parser = argparse.ArgumentParser( + description=("Manage Home Assistant users")) + parser.add_argument( + '--script', choices=['auth']) + parser.add_argument( + '-c', '--config', + default=get_default_config_dir(), + help="Directory that contains the Home Assistant configuration") + + subparsers = parser.add_subparsers() + parser_list = subparsers.add_parser('list') + parser_list.set_defaults(func=list_users) + + parser_add = subparsers.add_parser('add') + parser_add.add_argument('username', type=str) + parser_add.add_argument('password', type=str) + parser_add.set_defaults(func=add_user) + + parser_validate_login = subparsers.add_parser('validate') + parser_validate_login.add_argument('username', type=str) + parser_validate_login.add_argument('password', type=str) + parser_validate_login.set_defaults(func=validate_login) + + parser_change_pw = subparsers.add_parser('change_password') + parser_change_pw.add_argument('username', type=str) + parser_change_pw.add_argument('new_password', type=str) + parser_change_pw.set_defaults(func=change_password) + + args = parser.parse_args(args) + path = os.path.join(os.getcwd(), args.config, hass_auth.PATH_DATA) + args.func(hass_auth.load_data(path), args) + + +def list_users(data, args): + """List the users.""" + count = 0 + for user in data.users: + count += 1 + print(user['username']) + + print() + print("Total users:", count) + + +def add_user(data, args): + """Create a user.""" + data.add_user(args.username, args.password) + data.save() + print("User created") + + +def validate_login(data, args): + """Validate a login.""" + try: + data.validate_login(args.username, args.password) + print("Auth valid") + except hass_auth.InvalidAuth: + print("Auth invalid") + + +def change_password(data, args): + """Change password.""" + try: + data.change_password(args.username, args.new_password) + data.save() + print("Password changed") + except hass_auth.InvalidUser: + print("User not found") diff --git a/tests/auth_providers/test_homeassistant.py b/tests/auth_providers/test_homeassistant.py new file mode 100644 index 00000000000..8b12e682865 --- /dev/null +++ b/tests/auth_providers/test_homeassistant.py @@ -0,0 +1,124 @@ +"""Test the Home Assistant local auth provider.""" +from unittest.mock import patch, mock_open + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.auth_providers import homeassistant as hass_auth + + +MOCK_PATH = '/bla/users.json' +JSON__OPEN_PATH = 'homeassistant.util.json.open' + + +def test_initialize_empty_config_file_not_found(): + """Test that we initialize an empty config.""" + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + data = hass_auth.load_data(MOCK_PATH) + + assert data is not None + + +def test_adding_user(): + """Test adding a user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + data.validate_login('test-user', 'test-pass') + + +def test_adding_user_duplicate_username(): + """Test adding a user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + with pytest.raises(hass_auth.InvalidUser): + data.add_user('test-user', 'other-pass') + + +def test_validating_password_invalid_user(): + """Test validating an invalid user.""" + data = hass_auth.Data(MOCK_PATH, None) + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('non-existing', 'pw') + + +def test_validating_password_invalid_password(): + """Test validating an invalid user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'invalid-pass') + + +def test_changing_password(): + """Test adding a user.""" + user = 'test-user' + data = hass_auth.Data(MOCK_PATH, None) + data.add_user(user, 'test-pass') + data.change_password(user, 'new-pass') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login(user, 'test-pass') + + data.validate_login(user, 'new-pass') + + +def test_changing_password_raises_invalid_user(): + """Test that we initialize an empty config.""" + data = hass_auth.Data(MOCK_PATH, None) + + with pytest.raises(hass_auth.InvalidUser): + data.change_password('non-existing', 'pw') + + +async def test_login_flow_validates(hass): + """Test login flow.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + provider = hass_auth.HassAuthProvider(hass, None, {}) + flow = hass_auth.LoginFlow(provider) + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + with patch.object(provider, '_auth_data', return_value=data): + result = await flow.async_step_init({ + 'username': 'incorrect-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' + + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'incorrect-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' + + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_saving_loading(hass): + """Test saving and loading JSON.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + data.add_user('second-user', 'second-pass') + + with patch(JSON__OPEN_PATH, mock_open(), create=True) as mock_write: + await hass.async_add_job(data.save) + + # Mock open calls are: open file, context enter, write, context leave + written = mock_write.mock_calls[2][1][0] + + with patch('os.path.isfile', return_value=True), \ + patch(JSON__OPEN_PATH, mock_open(read_data=written), create=True): + await hass.async_add_job(hass_auth.load_data, MOCK_PATH) + + data.validate_login('test-user', 'test-pass') + data.validate_login('second-user', 'second-pass') diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py index 92fc2974e27..0b481f93099 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -19,7 +19,7 @@ def store(): @pytest.fixture def provider(store): """Mock provider.""" - return insecure_example.ExampleAuthProvider(store, { + return insecure_example.ExampleAuthProvider(None, store, { 'type': 'insecure_example', 'users': [ { @@ -64,20 +64,16 @@ async def test_match_existing_credentials(store, provider): async def test_verify_username(provider): """Test we raise if incorrect user specified.""" - with pytest.raises(auth.InvalidUser): - await provider.async_get_or_create_credentials({ - 'username': 'non-existing-user', - 'password': 'password-test', - }) + with pytest.raises(insecure_example.InvalidAuthError): + await provider.async_validate_login( + 'non-existing-user', 'password-test') async def test_verify_password(provider): """Test we raise if incorrect user specified.""" - with pytest.raises(auth.InvalidPassword): - await provider.async_get_or_create_credentials({ - 'username': 'user-test', - 'password': 'incorrect-password', - }) + with pytest.raises(insecure_example.InvalidAuthError): + await provider.async_validate_login( + 'user-test', 'incorrect-password') async def test_utf_8_username_password(provider): diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py new file mode 100644 index 00000000000..2e837b06b58 --- /dev/null +++ b/tests/scripts/test_auth.py @@ -0,0 +1,100 @@ +"""Test the auth script to manage local users.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.scripts import auth as script_auth +from homeassistant.auth_providers import homeassistant as hass_auth + +MOCK_PATH = '/bla/users.json' + + +def test_list_user(capsys): + """Test we can list users.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + data.add_user('second-user', 'second-pass') + + script_auth.list_users(data, None) + + captured = capsys.readouterr() + + assert captured.out == '\n'.join([ + 'test-user', + 'second-user', + '', + 'Total users: 2', + '' + ]) + + +def test_add_user(capsys): + """Test we can add a user.""" + data = hass_auth.Data(MOCK_PATH, None) + + with patch.object(data, 'save') as mock_save: + script_auth.add_user( + data, Mock(username='paulus', password='test-pass')) + + assert len(mock_save.mock_calls) == 1 + + captured = capsys.readouterr() + assert captured.out == 'User created\n' + + assert len(data.users) == 1 + data.validate_login('paulus', 'test-pass') + + +def test_validate_login(capsys): + """Test we can validate a user login.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + script_auth.validate_login( + data, Mock(username='test-user', password='test-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth valid\n' + + script_auth.validate_login( + data, Mock(username='test-user', password='invalid-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth invalid\n' + + script_auth.validate_login( + data, Mock(username='invalid-user', password='test-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth invalid\n' + + +def test_change_password(capsys): + """Test we can change a password.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + with patch.object(data, 'save') as mock_save: + script_auth.change_password( + data, Mock(username='test-user', new_password='new-pass')) + + assert len(mock_save.mock_calls) == 1 + captured = capsys.readouterr() + assert captured.out == 'Password changed\n' + data.validate_login('test-user', 'new-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'test-pass') + + +def test_change_password_invalid_user(capsys): + """Test changing password of non-existing user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + with patch.object(data, 'save') as mock_save: + script_auth.change_password( + data, Mock(username='invalid-user', new_password='new-pass')) + + assert len(mock_save.mock_calls) == 0 + captured = capsys.readouterr() + assert captured.out == 'User not found\n' + data.validate_login('test-user', 'test-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('invalid-user', 'new-pass') From f168226be9880feef1506a5018557836c080f7ef Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 10 May 2018 22:11:02 +0300 Subject: [PATCH 669/924] Update to sensibo 1.0.3 with better error reporting (#14380) --- homeassistant/components/climate/sensibo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index e2a455aefc7..2b92d050d3b 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.temperature import convert as convert_temperature -REQUIREMENTS = ['pysensibo==1.0.2'] +REQUIREMENTS = ['pysensibo==1.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1400237a683..3f301589264 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -913,7 +913,7 @@ pyrainbird==0.1.3 pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo -pysensibo==1.0.2 +pysensibo==1.0.3 # homeassistant.components.sensor.serial pyserial-asyncio==0.4 From db31cdf075f17a338b01a2a5499a3c82df66c6ab Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 10 May 2018 21:28:57 +0200 Subject: [PATCH 670/924] Fix binary_sensor device_state_attributes (#14375) --- homeassistant/components/binary_sensor/zha.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index be61a9e9ba4..4f3f824c8f9 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -219,7 +219,10 @@ class Switch(zha.Entity, BinarySensorDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - return {'level': self._state and self._level or 0} + self._device_state_attributes.update({ + 'level': self._state and self._level or 0 + }) + return self._device_state_attributes def move_level(self, change): """Increment the level, setting state if appropriate.""" From f192ef8219cdbd20423357c1814b1e7174437f8d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 17:13:00 -0400 Subject: [PATCH 671/924] Remove domain expiry sensor (#14381) --- .../components/sensor/domain_expiry.py | 76 ------------------- requirements_all.txt | 3 - 2 files changed, 79 deletions(-) delete mode 100644 homeassistant/components/sensor/domain_expiry.py diff --git a/homeassistant/components/sensor/domain_expiry.py b/homeassistant/components/sensor/domain_expiry.py deleted file mode 100644 index 9364ce041f2..00000000000 --- a/homeassistant/components/sensor/domain_expiry.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Counter for the days till domain will expire. - -For more details about this sensor please refer to the documentation at -https://home-assistant.io/components/sensor.domain_expiry/ -""" -import logging -from datetime import datetime, timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_DOMAIN) -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['python-whois==0.6.9'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Domain Expiry' - -SCAN_INTERVAL = timedelta(hours=24) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up domain expiry sensor.""" - server_name = config.get(CONF_DOMAIN) - sensor_name = config.get(CONF_NAME) - - add_devices([DomainExpiry(sensor_name, server_name)], True) - - -class DomainExpiry(Entity): - """Implementation of the domain expiry sensor.""" - - def __init__(self, sensor_name, server_name): - """Initialize the sensor.""" - self.server_name = server_name - self._name = sensor_name - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return 'days' - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return 'mdi:earth' - - def update(self): - """Fetch the domain information.""" - import whois - domain = whois.whois(self.server_name) - if isinstance(domain.expiration_date, datetime): - expiry = domain.expiration_date - datetime.today() - self._state = expiry.days - else: - _LOGGER.error("Cannot get expiry date for %s", self.server_name) diff --git a/requirements_all.txt b/requirements_all.txt index 3f301589264..bfaeb72ae7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,9 +1041,6 @@ python-velbus==2.0.11 # homeassistant.components.media_player.vlc python-vlc==1.1.2 -# homeassistant.components.sensor.domain_expiry -python-whois==0.6.9 - # homeassistant.components.wink python-wink==1.7.3 From bc664c276c2050544b9ff96196a79b8ed1338669 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 17:33:10 -0400 Subject: [PATCH 672/924] Bump frontend to 20180510.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c30e0dfb69f..f60d095a682 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180509.0'] +REQUIREMENTS = ['home-assistant-frontend==20180510.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index bfaeb72ae7e..cbc5cce0590 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180509.0 +home-assistant-frontend==20180510.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a25f36a8195..630ed06580c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180509.0 +home-assistant-frontend==20180510.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e963fc5acf610333410b33c58d63f2b6e343f258 Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 10 May 2018 23:55:32 +0200 Subject: [PATCH 673/924] Add support for pressure sensors (#14361) --- homeassistant/components/sensor/zha.py | 21 ++++++++++++++++++++- homeassistant/components/zha/const.py | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index d856ed1a17e..41dab282997 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -32,13 +32,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def make_sensor(discovery_info): """Create ZHA sensors factory.""" from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement + RelativeHumidity, TemperatureMeasurement, PressureMeasurement ) in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) elif TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) + elif PressureMeasurement.cluster_id in in_clusters: + sensor = PressureSensor(**discovery_info) else: sensor = Sensor(**discovery_info) @@ -111,3 +113,20 @@ class RelativeHumiditySensor(Sensor): return 'unknown' return round(float(self._state) / 100, 1) + + +class PressureSensor(Sensor): + """ZHA pressure sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'hPa' + + @property + def state(self): + """Return the state of the entity.""" + if self._state == 'unknown': + return 'unknown' + + return round(float(self._state)) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 36eb4d55c97..1c083c3ca93 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -47,6 +47,7 @@ def populate_data(): zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', + zcl.clusters.measurement.PressureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) From 6843893d9f334467d82d89a9a218114e91d082f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 10 May 2018 23:22:02 +0100 Subject: [PATCH 674/924] Add "framerate" parameter to generic camera (#14079) * add "framerate" parameter to generic camera * fix lint --- homeassistant/components/camera/__init__.py | 15 +++++++++------ homeassistant/components/camera/generic.py | 8 ++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c1f92965198..60f8979bb16 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -256,6 +256,11 @@ class Camera(Entity): """Return the camera model.""" return None + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return 0.5 + def camera_image(self): """Return bytes of camera image.""" raise NotImplementedError() @@ -272,10 +277,6 @@ class Camera(Entity): This method must be run in the event loop. """ - if interval < MIN_STREAM_INTERVAL: - raise ValueError("Stream interval must be be > {}" - .format(MIN_STREAM_INTERVAL)) - response = web.StreamResponse() response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--frameboundary') @@ -325,8 +326,7 @@ class Camera(Entity): a direct stream from the camera. This method must be run in the event loop. """ - await self.handle_async_still_stream(request, - FALLBACK_STREAM_INTERVAL) + await self.handle_async_still_stream(request, self.frame_interval) @property def state(self): @@ -448,6 +448,9 @@ class CameraMjpegStream(CameraView): try: # Compose camera stream from stills interval = float(request.query.get('interval')) + if interval < MIN_STREAM_INTERVAL: + raise ValueError("Stream interval must be be > {}" + .format(MIN_STREAM_INTERVAL)) await camera.handle_async_still_stream(request, interval) return except ValueError: diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 2f5d8d28979..e11bd599e45 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) CONF_CONTENT_TYPE = 'content_type' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_STILL_IMAGE_URL = 'still_image_url' +CONF_FRAMERATE = 'framerate' DEFAULT_NAME = 'Generic Camera' @@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, + vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, }) @@ -62,6 +64,7 @@ class GenericCamera(Camera): self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] + self._frame_interval = 1 / device_info[CONF_FRAMERATE] self.content_type = device_info[CONF_CONTENT_TYPE] username = device_info.get(CONF_USERNAME) @@ -78,6 +81,11 @@ class GenericCamera(Camera): self._last_url = None self._last_image = None + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return self._frame_interval + def camera_image(self): """Return bytes of camera image.""" return run_coroutine_threadsafe( From 8fcf085829def67fa4e79b0591f037d822da5703 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 May 2018 01:21:59 +0200 Subject: [PATCH 675/924] Rewritten HomeKit tests (#14377) * Use pytest fixtures and parametrize * Use async --- tests/components/homekit/test_accessories.py | 220 +++---- .../homekit/test_get_accessories.py | 253 +++----- tests/components/homekit/test_homekit.py | 308 +++++---- tests/components/homekit/test_type_covers.py | 396 ++++++------ tests/components/homekit/test_type_lights.py | 280 ++++---- tests/components/homekit/test_type_locks.py | 102 ++- .../homekit/test_type_security_systems.py | 188 +++--- tests/components/homekit/test_type_sensors.py | 298 +++++---- .../components/homekit/test_type_switches.py | 121 +--- .../homekit/test_type_thermostats.py | 607 +++++++++--------- tests/components/homekit/test_util.py | 73 +-- 11 files changed, 1255 insertions(+), 1591 deletions(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index faa982f62f3..48c6357c28d 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,8 +3,7 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -import unittest -from unittest.mock import call, patch, Mock +from unittest.mock import patch, Mock from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) @@ -15,8 +14,6 @@ from homeassistant.components.homekit.const import ( from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant - def patch_debounce(): """Return patch for debounce method.""" @@ -24,141 +21,122 @@ def patch_debounce(): lambda f: lambda *args, **kwargs: f(*args, **kwargs)) -class TestAccessories(unittest.TestCase): - """Test pyhap adapter methods.""" +async def test_debounce(hass): + """Test add_timeout decorator function.""" + def demo_func(*args): + nonlocal arguments, counter + counter += 1 + arguments = args - def test_debounce(self): - """Test add_timeout decorator function.""" - def demo_func(*args): - nonlocal arguments, counter - counter += 1 - arguments = args + arguments = None + counter = 0 + mock = Mock(hass=hass) - arguments = None - counter = 0 - hass = get_test_home_assistant() - mock = Mock(hass=hass) + debounce_demo = debounce(demo_func) + assert debounce_demo.__name__ == 'demo_func' + now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) - debounce_demo = debounce(demo_func) - self.assertEqual(debounce_demo.__name__, 'demo_func') - now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): + await hass.async_add_job(debounce_demo, mock, 'value') + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + await hass.async_block_till_done() + assert counter == 1 + assert len(arguments) == 2 - with patch('homeassistant.util.dt.utcnow', return_value=now): - debounce_demo(mock, 'value') - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) - hass.block_till_done() - assert counter == 1 - assert len(arguments) == 2 + with patch('homeassistant.util.dt.utcnow', return_value=now): + await hass.async_add_job(debounce_demo, mock, 'value') + await hass.async_add_job(debounce_demo, mock, 'value') - with patch('homeassistant.util.dt.utcnow', return_value=now): - debounce_demo(mock, 'value') - debounce_demo(mock, 'value') + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + await hass.async_block_till_done() + assert counter == 2 - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) - hass.block_till_done() - assert counter == 2 - hass.stop() +async def test_home_accessory(hass): + """Test HomeAccessory class.""" + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) + assert acc.hass == hass + assert acc.display_name == 'Home Accessory' + assert acc.category == 1 # Category.OTHER + assert len(acc.services) == 1 + serv = acc.services[0] # SERV_ACCESSORY_INFO + assert serv.display_name == SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == 'Home Accessory' + assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER + assert serv.get_characteristic(CHAR_MODEL).value == 'Homekit' + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ + 'homekit.accessory' - def test_home_accessory(self): - """Test HomeAccessory class.""" - hass = get_test_home_assistant() + hass.states.async_set('homekit.accessory', 'on') + await hass.async_block_till_done() + await hass.async_add_job(acc.run) + hass.states.async_set('homekit.accessory', 'off') + await hass.async_block_till_done() - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) - self.assertEqual(acc.hass, hass) - self.assertEqual(acc.display_name, 'Home Accessory') - self.assertEqual(acc.category, 1) # Category.OTHER - self.assertEqual(len(acc.services), 1) - serv = acc.services[0] # SERV_ACCESSORY_INFO - self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) - self.assertEqual( - serv.get_characteristic(CHAR_NAME).value, 'Home Accessory') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'Homekit') - self.assertEqual(serv.get_characteristic(CHAR_SERIAL_NUMBER).value, - 'homekit.accessory') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) + assert acc.display_name == 'test_name' + assert acc.aid == 2 + assert len(acc.services) == 1 + serv = acc.services[0] # SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' - hass.states.set('homekit.accessory', 'on') - hass.block_till_done() - acc.run() - hass.states.set('homekit.accessory', 'off') - hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) - self.assertEqual(acc.display_name, 'test_name') - self.assertEqual(acc.aid, 2) - self.assertEqual(len(acc.services), 1) - serv = acc.services[0] # SERV_ACCESSORY_INFO - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'Test Model') +def test_home_bridge(): + """Test HomeBridge class.""" + bridge = HomeBridge('hass') + assert bridge.hass == 'hass' + assert bridge.display_name == BRIDGE_NAME + assert bridge.category == 2 # Category.BRIDGE + assert len(bridge.services) == 1 + serv = bridge.services[0] # SERV_ACCESSORY_INFO + assert serv.display_name == SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER + assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ + BRIDGE_SERIAL_NUMBER - hass.stop() + bridge = HomeBridge('hass', 'test_name') + assert bridge.display_name == 'test_name' + assert len(bridge.services) == 1 + serv = bridge.services[0] # SERV_ACCESSORY_INFO - def test_home_bridge(self): - """Test HomeBridge class.""" - bridge = HomeBridge('hass') - self.assertEqual(bridge.hass, 'hass') - self.assertEqual(bridge.display_name, BRIDGE_NAME) - self.assertEqual(bridge.category, 2) # Category.BRIDGE - self.assertEqual(len(bridge.services), 1) - serv = bridge.services[0] # SERV_ACCESSORY_INFO - self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) - self.assertEqual( - serv.get_characteristic(CHAR_NAME).value, BRIDGE_NAME) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, __version__) - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, - BRIDGE_SERIAL_NUMBER) + # setup_message + bridge.setup_message() - bridge = HomeBridge('hass', 'test_name') - self.assertEqual(bridge.display_name, 'test_name') - self.assertEqual(len(bridge.services), 1) - serv = bridge.services[0] # SERV_ACCESSORY_INFO + # add_paired_client + with patch('pyhap.accessory.Accessory.add_paired_client') \ + as mock_add_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + bridge.add_paired_client('client_uuid', 'client_public') - # setup_message - bridge.setup_message() + mock_add_paired_client.assert_called_with('client_uuid', 'client_public') + mock_dissmiss_msg.assert_called_with('hass') - # add_paired_client - with patch('pyhap.accessory.Accessory.add_paired_client') \ - as mock_add_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'dismiss_setup_message') as mock_dissmiss_msg: - bridge.add_paired_client('client_uuid', 'client_public') + # remove_paired_client + with patch('pyhap.accessory.Accessory.remove_paired_client') \ + as mock_remove_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + bridge.remove_paired_client('client_uuid') - self.assertEqual(mock_add_paired_client.call_args, - call('client_uuid', 'client_public')) - self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) + mock_remove_paired_client.assert_called_with('client_uuid') + mock_show_msg.assert_called_with('hass', bridge) - # remove_paired_client - with patch('pyhap.accessory.Accessory.remove_paired_client') \ - as mock_remove_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'show_setup_message') as mock_show_msg: - bridge.remove_paired_client('client_uuid') - self.assertEqual( - mock_remove_paired_client.call_args, call('client_uuid')) - self.assertEqual(mock_show_msg.call_args, call('hass', bridge)) +def test_home_driver(): + """Test HomeDriver class.""" + bridge = HomeBridge('hass') + ip_address = '127.0.0.1' + port = 51826 + path = '.homekit.state' - def test_home_driver(self): - """Test HomeDriver class.""" - bridge = HomeBridge('hass') - ip_address = '127.0.0.1' - port = 51826 - path = '.homekit.state' + with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ + as mock_driver: + HomeDriver(bridge, ip_address, port, path) - with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ - as mock_driver: - HomeDriver(bridge, ip_address, port, path) - - self.assertEqual( - mock_driver.call_args, call(bridge, ip_address, port, path)) + mock_driver.assert_called_with(bridge, ip_address, port, path) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index cff52b2ff20..2ff591983c6 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,22 +1,20 @@ """Package to test the get_accessory method.""" import logging -import unittest from unittest.mock import patch, Mock +import pytest + from homeassistant.core import State -from homeassistant.components.cover import ( - SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( - ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS) + ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) _LOGGER = logging.getLogger(__name__) -CONFIG = {} - def test_get_accessory_invalid_aid(caplog): """Test with unsupported component.""" @@ -32,182 +30,93 @@ def test_not_supported(): is None -class TestGetAccessories(unittest.TestCase): - """Methods to test the get_accessory method.""" +@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Light', 'light.test', 'on', {}, None), + ('Lock', 'lock.test', 'locked', {}, None), - def setUp(self): - """Setup Mock type.""" - self.mock_type = Mock() + ('Thermostat', 'climate.test', 'auto', {}, None), + ('Thermostat', 'climate.test', 'auto', + {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH}, None), - def tearDown(self): - """Test if mock type was called.""" - self.assertTrue(self.mock_type.called) + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), +]) +def test_types(type_name, entity_id, state, attrs, config): + """Test if types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, config) + assert mock_type.called - def test_sensor_temperature(self): - """Test temperature sensor with device class temperature.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '23', - {ATTR_DEVICE_CLASS: 'temperature'}) - get_accessory(None, state, 2, {}) + if config: + assert mock_type.call_args[1]['config'] == config - def test_sensor_temperature_celsius(self): - """Test temperature sensor with Celsius as unit.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '23', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - get_accessory(None, state, 2, {}) - def test_sensor_temperature_fahrenheit(self): - """Test temperature sensor with Fahrenheit as unit.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '74', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('GarageDoorOpener', 'cover.garage_door', 'open', + {ATTR_DEVICE_CLASS: 'garage', + ATTR_SUPPORTED_FEATURES: SUPPORT_OPEN | SUPPORT_CLOSE}), + ('WindowCovering', 'cover.set_position', 'open', + {ATTR_SUPPORTED_FEATURES: 4}), + ('WindowCoveringBasic', 'cover.open_window', 'open', + {ATTR_SUPPORTED_FEATURES: 3}), +]) +def test_type_covers(type_name, entity_id, state, attrs): + """Test if cover types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, None) + assert mock_type.called - def test_sensor_humidity(self): - """Test humidity sensor with device class humidity.""" - with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): - state = State('sensor.humidity', '20', - {ATTR_DEVICE_CLASS: 'humidity', - ATTR_UNIT_OF_MEASUREMENT: '%'}) - get_accessory(None, state, 2, {}) - def test_air_quality_sensor(self): - """Test air quality sensor with pm25 class.""" - with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): - state = State('sensor.air_quality', '40', - {ATTR_DEVICE_CLASS: 'pm25'}) - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('BinarySensor', 'binary_sensor.opening', 'on', + {ATTR_DEVICE_CLASS: 'opening'}), + ('BinarySensor', 'device_tracker.someone', 'not_home', {}), - def test_air_quality_sensor_entity_id(self): - """Test air quality sensor with entity_id contains pm25.""" - with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): - state = State('sensor.air_quality_pm25', '40', {}) - get_accessory(None, state, 2, {}) + ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), + ('AirQualitySensor', 'sensor.air_quality', '40', + {ATTR_DEVICE_CLASS: 'pm25'}), - def test_co2_sensor(self): - """Test co2 sensor with device class co2.""" - with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): - state = State('sensor.airmeter', '500', - {ATTR_DEVICE_CLASS: 'co2'}) - get_accessory(None, state, 2, {}) + ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), + ('CarbonDioxideSensor', 'sensor.airmeter', '500', + {ATTR_DEVICE_CLASS: 'co2'}), - def test_co2_sensor_entity_id(self): - """Test co2 sensor with entity_id contains co2.""" - with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): - state = State('sensor.airmeter_co2', '500', {}) - get_accessory(None, state, 2, {}) + ('HumiditySensor', 'sensor.humidity', '20', + {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), - def test_light_sensor(self): - """Test light sensor with device class illuminance.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_DEVICE_CLASS: 'illuminance'}) - get_accessory(None, state, 2, {}) + ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), + ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), + ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), - def test_light_sensor_unit_lm(self): - """Test light sensor with lm as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) - get_accessory(None, state, 2, {}) + ('TemperatureSensor', 'sensor.temperature', '23', + {ATTR_DEVICE_CLASS: 'temperature'}), + ('TemperatureSensor', 'sensor.temperature', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}), + ('TemperatureSensor', 'sensor.temperature', '74', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}), +]) +def test_type_sensors(type_name, entity_id, state, attrs): + """Test if sensor types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, None) + assert mock_type.called - def test_light_sensor_unit_lx(self): - """Test light sensor with lx as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lx'}) - get_accessory(None, state, 2, {}) - def test_binary_sensor(self): - """Test binary sensor with opening class.""" - with patch.dict(TYPES, {'BinarySensor': self.mock_type}): - state = State('binary_sensor.opening', 'on', - {ATTR_DEVICE_CLASS: 'opening'}) - get_accessory(None, state, 2, {}) - - def test_device_tracker(self): - """Test binary sensor with opening class.""" - with patch.dict(TYPES, {'BinarySensor': self.mock_type}): - state = State('device_tracker.someone', 'not_home', {}) - get_accessory(None, state, 2, {}) - - def test_garage_door(self): - """Test cover with device_class: 'garage' and required features.""" - with patch.dict(TYPES, {'GarageDoorOpener': self.mock_type}): - state = State('cover.garage_door', 'open', { - ATTR_DEVICE_CLASS: 'garage', - ATTR_SUPPORTED_FEATURES: - SUPPORT_OPEN | SUPPORT_CLOSE}) - get_accessory(None, state, 2, {}) - - def test_cover_set_position(self): - """Test cover with support for set_cover_position.""" - with patch.dict(TYPES, {'WindowCovering': self.mock_type}): - state = State('cover.set_position', 'open', - {ATTR_SUPPORTED_FEATURES: 4}) - get_accessory(None, state, 2, {}) - - def test_cover_open_close(self): - """Test cover with support for open and close.""" - with patch.dict(TYPES, {'WindowCoveringBasic': self.mock_type}): - state = State('cover.open_window', 'open', - {ATTR_SUPPORTED_FEATURES: 3}) - get_accessory(None, state, 2, {}) - - def test_alarm_control_panel(self): - """Test alarm control panel.""" - config = {ATTR_CODE: '1234'} - with patch.dict(TYPES, {'SecuritySystem': self.mock_type}): - state = State('alarm_control_panel.test', 'armed') - get_accessory(None, state, 2, config) - - # pylint: disable=unsubscriptable-object - print(self.mock_type.call_args[1]) - self.assertEqual( - self.mock_type.call_args[1]['config'][ATTR_CODE], '1234') - - def test_climate(self): - """Test climate devices.""" - with patch.dict(TYPES, {'Thermostat': self.mock_type}): - state = State('climate.test', 'auto') - get_accessory(None, state, 2, {}) - - def test_light(self): - """Test light devices.""" - with patch.dict(TYPES, {'Light': self.mock_type}): - state = State('light.test', 'on') - get_accessory(None, state, 2, {}) - - def test_climate_support_auto(self): - """Test climate devices with support for auto mode.""" - with patch.dict(TYPES, {'Thermostat': self.mock_type}): - state = State('climate.test', 'auto', { - ATTR_SUPPORTED_FEATURES: - SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}) - get_accessory(None, state, 2, {}) - - def test_switch(self): - """Test switch.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('switch.test', 'on') - get_accessory(None, state, 2, {}) - - def test_remote(self): - """Test remote.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('remote.test', 'on') - get_accessory(None, state, 2, {}) - - def test_input_boolean(self): - """Test input_boolean.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('input_boolean.test', 'on') - get_accessory(None, state, 2, {}) - - def test_lock(self): - """Test lock.""" - with patch.dict(TYPES, {'Lock': self.mock_type}): - state = State('lock.test', 'locked') - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('Switch', 'switch.test', 'on', {}), + ('Switch', 'remote.test', 'on', {}), + ('Switch', 'input_boolean.test', 'on', {}), +]) +def test_type_switches(type_name, entity_id, state, attrs): + """Test if switch types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, None) + assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 082953038b5..23f117b15a0 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,6 +1,7 @@ """Tests for the HomeKit component.""" -import unittest -from unittest.mock import call, patch, ANY, Mock +from unittest.mock import patch, ANY, Mock + +import pytest from homeassistant import setup from homeassistant.core import State @@ -16,208 +17,193 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -class TestHomeKit(unittest.TestCase): - """Test setup of HomeKit component and HomeKit class.""" +@pytest.fixture('module') +def debounce_patcher(request): + """Patch debounce method.""" + patcher = patch_debounce() + patcher.start() + request.addfinalizer(patcher.stop) - @classmethod - def setUpClass(cls): - """Setup debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +def test_generate_aid(): + """Test generate aid method.""" + aid = generate_aid('demo.entity') + assert isinstance(aid, int) + assert aid >= 2 and aid <= 18446744073709551615 - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: + mock_adler32.side_effect = [0, 1] + assert generate_aid('demo.entity') is None - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_generate_aid(self): - """Test generate aid method.""" - aid = generate_aid('demo.entity') - self.assertIsInstance(aid, int) - self.assertTrue(aid >= 2 and aid <= 18446744073709551615) +async def test_setup_min(hass): + """Test async_setup with min config options.""" + with patch(PATH_HOMEKIT + '.HomeKit') as mock_homekit: + assert await setup.async_setup_component( + hass, DOMAIN, {DOMAIN: {}}) - with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: - mock_adler32.side_effect = [0, 1] - self.assertIsNone(generate_aid('demo.entity')) + mock_homekit.assert_any_call(hass, DEFAULT_PORT, None, ANY, {}) + assert mock_homekit().setup.called is True - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_min(self, mock_homekit): - """Test async_setup with min config options.""" - self.assertTrue(setup.setup_component( - self.hass, DOMAIN, {DOMAIN: {}})) + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() - self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, None, ANY, {}), - call().setup()]) + mock_homekit().start.assert_called_with(ANY) - # Test auto start enabled - mock_homekit.reset_mock() - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() - self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) +async def test_setup_auto_start_disabled(hass): + """Test async_setup with auto start disabled and test service calls.""" + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_auto_start_disabled(self, mock_homekit): - """Test async_setup with auto start disabled and test service calls.""" + with patch(PATH_HOMEKIT + '.HomeKit') as mock_homekit: mock_homekit.return_value = homekit = Mock() + assert await setup.async_setup_component( + hass, DOMAIN, config) - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, - CONF_IP_ADDRESS: '172.0.0.0'}} - self.assertTrue(setup.setup_component( - self.hass, DOMAIN, config)) - self.hass.block_till_done() + mock_homekit.assert_any_call(hass, 11111, '172.0.0.0', ANY, {}) + assert mock_homekit().setup.called is True - self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, '172.0.0.0', ANY, {}), - call().setup()]) + # Test auto_start disabled + homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert homekit.start.called is False - # Test auto_start disabled - homekit.reset_mock() - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() - self.assertEqual(homekit.mock_calls, []) + # Test start call with driver is ready + homekit.reset_mock() + homekit.status = STATUS_READY - # Test start call with driver is ready - homekit.reset_mock() - homekit.status = STATUS_READY + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + assert homekit.start.called is True - self.hass.services.call('homekit', 'start') - self.assertEqual(homekit.mock_calls, [call.start()]) + # Test start call with driver started + homekit.reset_mock() + homekit.status = STATUS_STOPPED - # Test start call with driver started - homekit.reset_mock() - homekit.status = STATUS_STOPPED + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + assert homekit.start.called is False - self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) - self.assertEqual(homekit.mock_calls, []) - def test_homekit_setup(self): - """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) +async def test_homekit_setup(hass): + """Test setup of bridge and driver.""" + homekit = HomeKit(hass, DEFAULT_PORT, None, {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ - patch('homeassistant.util.get_local_ip') as mock_ip: - mock_ip.return_value = IP_ADDRESS - homekit.setup() + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ + patch('homeassistant.util.get_local_ip') as mock_ip: + mock_ip.return_value = IP_ADDRESS + await hass.async_add_job(homekit.setup) - path = self.hass.config.path(HOMEKIT_FILE) - self.assertTrue(isinstance(homekit.bridge, HomeBridge)) - self.assertEqual(mock_driver.mock_calls, [ - call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) + path = hass.config.path(HOMEKIT_FILE) + assert isinstance(homekit.bridge, HomeBridge) + mock_driver.assert_called_with( + homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path) - # Test if stop listener is setup - self.assertEqual( - self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + # Test if stop listener is setup + assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 - def test_homekit_setup_ip_address(self): - """Test setup with given IP address.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: - homekit.setup() - mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) +async def test_homekit_setup_ip_address(hass): + """Test setup with given IP address.""" + homekit = HomeKit(hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - def test_homekit_add_accessory(self): - """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(self.hass) + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + await hass.async_add_job(homekit.setup) + mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - mock_get_acc.side_effect = [None, 'acc', None] - homekit.add_bridge_accessory(State('light.demo', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 363398124, {})) - self.assertFalse(mock_add_acc.called) - homekit.add_bridge_accessory(State('demo.test', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 294192020, {})) - self.assertTrue(mock_add_acc.called) - homekit.add_bridge_accessory(State('demo.test_2', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 429982757, {})) - self.assertEqual(mock_add_acc.mock_calls, [call('acc')]) - def test_homekit_entity_filter(self): - """Test the entity filter.""" - entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, None, entity_filter, {}) +async def test_homekit_add_accessory(hass): + """Add accessory if config exists and get_acc returns an accessory.""" + homekit = HomeKit(hass, None, None, lambda entity_id: True, {}) + homekit.bridge = HomeBridge(hass) - with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - mock_get_acc.return_value = None + with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ + as mock_add_acc, \ + patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - homekit.add_bridge_accessory(State('cover.test', 'open')) - self.assertTrue(mock_get_acc.called) - mock_get_acc.reset_mock() + mock_get_acc.side_effect = [None, 'acc', None] + homekit.add_bridge_accessory(State('light.demo', 'on')) + mock_get_acc.assert_called_with(hass, ANY, 363398124, {}) + assert mock_add_acc.called is False - homekit.add_bridge_accessory(State('demo.test', 'on')) - self.assertTrue(mock_get_acc.called) - mock_get_acc.reset_mock() + homekit.add_bridge_accessory(State('demo.test', 'on')) + mock_get_acc.assert_called_with(hass, ANY, 294192020, {}) + assert mock_add_acc.called is True - homekit.add_bridge_accessory(State('light.demo', 'light')) - self.assertFalse(mock_get_acc.called) + homekit.add_bridge_accessory(State('demo.test_2', 'on')) + mock_get_acc.assert_called_with(hass, ANY, 429982757, {}) + mock_add_acc.assert_called_with('acc') - @patch(PATH_HOMEKIT + '.show_setup_message') - @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') - def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): - """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(self.hass) - homekit.driver = Mock() - self.hass.states.set('light.demo', 'on') - state = self.hass.states.all()[0] +async def test_homekit_entity_filter(hass): + """Test the entity filter.""" + entity_filter = generate_filter(['cover'], ['demo.test'], [], []) + homekit = HomeKit(hass, None, None, entity_filter, {}) - homekit.start() - self.hass.block_till_done() + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.return_value = None - self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) - self.assertEqual(mock_show_setup_msg.mock_calls, [ - call(self.hass, homekit.bridge)]) - self.assertEqual(homekit.driver.mock_calls, [call.start()]) - self.assertEqual(homekit.status, STATUS_RUNNING) + homekit.add_bridge_accessory(State('cover.test', 'open')) + assert mock_get_acc.called is True + mock_get_acc.reset_mock() - # Test start() if already started - homekit.driver.reset_mock() - homekit.start() - self.hass.block_till_done() - self.assertEqual(homekit.driver.mock_calls, []) + homekit.add_bridge_accessory(State('demo.test', 'on')) + assert mock_get_acc.called is True + mock_get_acc.reset_mock() - def test_homekit_stop(self): - """Test HomeKit stop method.""" - homekit = HomeKit(self.hass, None, None, None, None) - homekit.driver = Mock() + homekit.add_bridge_accessory(State('light.demo', 'light')) + assert mock_get_acc.called is False - self.assertEqual(homekit.status, STATUS_READY) - homekit.stop() - self.hass.block_till_done() - homekit.status = STATUS_WAIT - homekit.stop() - self.hass.block_till_done() - homekit.status = STATUS_STOPPED - homekit.stop() - self.hass.block_till_done() - self.assertFalse(homekit.driver.stop.called) - # Test if driver is started - homekit.status = STATUS_RUNNING - homekit.stop() - self.hass.block_till_done() - self.assertTrue(homekit.driver.stop.called) +async def test_homekit_start(hass, debounce_patcher): + """Test HomeKit start method.""" + homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) + homekit.bridge = HomeBridge(hass) + homekit.driver = Mock() + + hass.states.async_set('light.demo', 'on') + state = hass.states.async_all()[0] + + with patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') as \ + mock_add_acc, \ + patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg: + await hass.async_add_job(homekit.start) + + mock_add_acc.assert_called_with(state) + mock_setup_msg.assert_called_with(hass, homekit.bridge) + assert homekit.driver.start.called is True + assert homekit.status == STATUS_RUNNING + + # Test start() if already started + homekit.driver.reset_mock() + await hass.async_add_job(homekit.start) + assert homekit.driver.start.called is False + + +async def test_homekit_stop(hass): + """Test HomeKit stop method.""" + homekit = HomeKit(hass, None, None, None, None) + homekit.driver = Mock() + + assert homekit.status == STATUS_READY + await hass.async_add_job(homekit.stop) + homekit.status = STATUS_WAIT + await hass.async_add_job(homekit.stop) + homekit.status = STATUS_STOPPED + await hass.async_add_job(homekit.stop) + assert homekit.driver.stop.called is False + + # Test if driver is started + homekit.status = STATUS_RUNNING + await hass.async_add_job(homekit.stop) + assert homekit.driver.stop.called is True diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 313d58e78fd..b833e1a03c9 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,265 +1,231 @@ """Test different accessory types: Covers.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) + DOMAIN, ATTR_CURRENT_POSITION, ATTR_POSITION, SUPPORT_STOP) from homeassistant.const import ( - STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, - ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitCovers(unittest.TestCase): - """Test class for all accessory types regarding covers.""" +@pytest.fixture(scope='module') +def cls(request): + """Patch debounce decorator during import of type_covers.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + request.addfinalizer(patcher.stop) + patcher_tuple = namedtuple('Cls', ['window', 'window_basic', 'garage']) + return patcher_tuple(window=_import.WindowCovering, + window_basic=_import.WindowCoveringBasic, + garage=_import.GarageDoorOpener) - @classmethod - def setUpClass(cls): - """Setup Light class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__('homeassistant.components.homekit.type_covers', - fromlist=['GarageDoorOpener', 'WindowCovering,', - 'WindowCoveringBasic']) - cls.garage_cls = _import.GarageDoorOpener - cls.window_cls = _import.WindowCovering - cls.window_basic_cls = _import.WindowCoveringBasic - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_garage_door_open_close(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.garage_door' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = cls.garage(hass, 'Garage Door', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 4 # GarageDoorOpener - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 1 - def test_garage_door_open_close(self): - """Test if accessory and HA are updated accordingly.""" - garage_door = 'cover.garage_door' + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 4) # GarageDoorOpener + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') - self.hass.states.set(garage_door, STATE_CLOSED) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_state.value == 2 + assert acc.char_target_state.value == 1 - self.assertEqual(acc.char_current_state.value, 1) - self.assertEqual(acc.char_target_state.value, 1) + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() - self.hass.states.set(garage_door, STATE_OPEN) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_open_cover + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) - self.hass.states.set(garage_door, STATE_UNAVAILABLE) - self.hass.block_till_done() +async def test_window_set_cover_position(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + acc = cls.window(hass, 'Cover', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.hass.states.set(garage_door, STATE_UNKNOWN) - self.hass.block_till_done() + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 - # Set closed from HomeKit - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_CURRENT_POSITION: None}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 - self.assertEqual(acc.char_current_state.value, 2) - self.assertEqual(acc.char_target_state.value, 1) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') + hass.states.async_set(entity_id, STATE_OPEN, + {ATTR_CURRENT_POSITION: 50}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 - self.hass.states.set(garage_door, STATE_CLOSED) - self.hass.block_till_done() + # Set from HomeKit + call_set_cover_position = async_mock_service(hass, DOMAIN, + 'set_cover_position') - # Set open from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_set_cover_position[0] + assert call_set_cover_position[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_cover_position[0].data[ATTR_POSITION] == 25 + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 25 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 0) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') + await hass.async_add_job(acc.char_target_position.client_update_value, 75) + await hass.async_block_till_done() + assert call_set_cover_position[1] + assert call_set_cover_position[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_cover_position[1].data[ATTR_POSITION] == 75 + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 75 - def test_window_set_cover_position(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) - acc.run() +async def test_window_open_close(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 14) # WindowCovering + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_CURRENT_POSITION: None}) - self.hass.block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.hass.states.set(window_cover, STATE_OPEN, - {ATTR_CURRENT_POSITION: 50}) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 50) + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_cover_position') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_POSITION], 25) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 25) + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - # Set from HomeKit - acc.char_target_position.client_update_value(75) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_cover_position') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75) + await hass.async_add_job(acc.char_target_position.client_update_value, 90) + await hass.async_block_till_done() + assert call_open_cover[0] + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 75) + await hass.async_add_job(acc.char_target_position.client_update_value, 55) + await hass.async_block_till_done() + assert call_open_cover[1] + assert call_open_cover[1].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - def test_window_open_close(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: 0}) - acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, - config=None) - acc.run() +async def test_window_open_close_stop(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 14) # WindowCovering + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') + call_stop_cover = async_mock_service(hass, DOMAIN, 'stop_cover') - self.hass.states.set(window_cover, STATE_UNKNOWN) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) + await hass.async_add_job(acc.char_target_position.client_update_value, 90) + await hass.async_block_till_done() + assert call_open_cover + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.hass.states.set(window_cover, STATE_OPEN) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - self.hass.states.set(window_cover, STATE_CLOSED) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(90) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(55) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - def test_window_open_close_stop(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, - config=None) - acc.run() - - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(90) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(55) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'stop_cover') - - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 50) - self.assertEqual(acc.char_position_state.value, 2) + await hass.async_add_job(acc.char_target_position.client_update_value, 55) + await hass.async_block_till_done() + assert call_stop_cover + assert call_stop_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 10bf469c08d..b4965fc5ab8 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,188 +1,174 @@ """Test different accessory types: Lights.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.light import ( DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, - ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, - SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_ON, STATE_OFF, STATE_UNKNOWN) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitLights(unittest.TestCase): - """Test class for all accessory types regarding lights.""" +@pytest.fixture(scope='module') +def cls(request): + """Patch debounce decorator during import of type_lights.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_lights', + fromlist=['Light']) + request.addfinalizer(patcher.stop) + patcher_tuple = namedtuple('Cls', ['light']) + return patcher_tuple(light=_import.Light) - @classmethod - def setUpClass(cls): - """Setup Light class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__('homeassistant.components.homekit.type_lights', - fromlist=['Light']) - cls.light_cls = _import.Light - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_light_basic(hass, cls): + """Test light with char state.""" + entity_id = 'light.demo' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 5 # Lightbulb + assert acc.char_on.value == 0 - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_on.value == 1 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - def test_light_basic(self): - """Test light with char state.""" - entity_id = 'light.demo' + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - self.hass.states.set(entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 5) # Lightbulb - self.assertEqual(acc.char_on.value, 0) + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 1) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 0) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 0) + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() - # Set from HomeKit - acc.char_on.client_update_value(1) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + await hass.async_add_job(acc.char_on.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - self.hass.states.set(entity_id, STATE_ON) - self.hass.block_till_done() - acc.char_on.client_update_value(0) - self.hass.block_till_done() - self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) +async def test_light_brightness(hass, cls): + """Test light with brightness.""" + entity_id = 'light.demo' - self.hass.states.set(entity_id, STATE_OFF) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) - # Remove entity - self.hass.states.remove(entity_id) - self.hass.block_till_done() + assert acc.char_brightness.value == 0 - def test_light_brightness(self): - """Test light with brightness.""" - entity_id = 'light.demo' + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_brightness.value, 0) + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_brightness.value, 100) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) - self.hass.block_till_done() - self.assertEqual(acc.char_brightness.value, 40) + await hass.async_add_job(acc.char_brightness.client_update_value, 20) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 - # Set from HomeKit - acc.char_brightness.client_update_value(20) - acc.char_on.client_update_value(1) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_add_job(acc.char_brightness.client_update_value, 40) + await hass.async_block_till_done() + assert call_turn_on[1] + assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 - acc.char_on.client_update_value(1) - acc.char_brightness.client_update_value(40) - self.hass.block_till_done() - self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_add_job(acc.char_brightness.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - acc.char_on.client_update_value(1) - acc.char_brightness.client_update_value(0) - self.hass.block_till_done() - self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) - def test_light_color_temperature(self): - """Test light with color temperature.""" - entity_id = 'light.demo' +async def test_light_color_temperature(hass, cls): + """Test light with color temperature.""" + entity_id = 'light.demo' - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, - ATTR_COLOR_TEMP: 190}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_color_temperature.value, 153) + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, + ATTR_COLOR_TEMP: 190}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_color_temperature.value, 190) + assert acc.char_color_temperature.value == 153 - # Set from HomeKit - acc.char_color_temperature.client_update_value(250) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 250}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_color_temperature.value == 190 - def test_light_rgb_color(self): - """Test light with rgb_color.""" - entity_id = 'light.demo' + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, - ATTR_HS_COLOR: (260, 90)}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_hue.value, 0) - self.assertEqual(acc.char_saturation.value, 75) + await hass.async_add_job( + acc.char_color_temperature.client_update_value, 250) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 260) - self.assertEqual(acc.char_saturation.value, 90) - # Set from HomeKit - acc.char_hue.client_update_value(145) - acc.char_saturation.client_update_value(75) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) +async def test_light_rgb_color(hass, cls): + """Test light with rgb_color.""" + entity_id = 'light.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, + ATTR_HS_COLOR: (260, 90)}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) + + assert acc.char_hue.value == 0 + assert acc.char_saturation.value == 75 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_hue.value == 260 + assert acc.char_saturation.value == 90 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + + await hass.async_add_job(acc.char_hue.client_update_value, 145) + await hass.async_add_job(acc.char_saturation.client_update_value, 75) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index b2053116060..3442c0da6c8 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,77 +1,57 @@ """Test different accessory types: Locks.""" -import unittest - -from homeassistant.core import callback from homeassistant.components.homekit.type_locks import Lock +from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED, - ATTR_SERVICE, EVENT_CALL_SERVICE) + ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding covers.""" +async def test_lock_unlock(hass): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'lock.kitchen_door' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = Lock(hass, 'Lock', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 6 # DoorLock - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 1 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_LOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 1 - def test_lock_unlock(self): - """Test if accessory and HA are updated accordingly.""" - kitchen_lock = 'lock.kitchen_door' + hass.states.async_set(entity_id, STATE_UNLOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - acc = Lock(self.hass, 'Lock', kitchen_lock, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 6) # DoorLock + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 1) + # Set from HomeKit + call_lock = async_mock_service(hass, DOMAIN, 'lock') + call_unlock = async_mock_service(hass, DOMAIN, 'unlock') - self.hass.states.set(kitchen_lock, STATE_LOCKED) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_lock + assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_state.value == 1 - self.assertEqual(acc.char_current_state.value, 1) - self.assertEqual(acc.char_target_state.value, 1) - - self.hass.states.set(kitchen_lock, STATE_UNLOCKED) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) - - self.hass.states.set(kitchen_lock, STATE_UNKNOWN) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 0) - - # Set from HomeKit - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'lock') - self.assertEqual(acc.char_target_state.value, 1) - - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'unlock') - self.assertEqual(acc.char_target_state.value, 0) - - self.hass.states.remove(kitchen_lock) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_unlock + assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_state.value == 0 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index baa461af772..8c3d9474f26 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,134 +1,110 @@ """Test different accessory types: Security Systems.""" -import unittest +import pytest -from homeassistant.core import callback +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.homekit.type_security_systems import ( SecuritySystem) from homeassistant.const import ( - ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - STATE_UNKNOWN) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSecuritySystems(unittest.TestCase): - """Test class for all accessory types regarding security systems.""" +async def test_switch_set_state(hass): + """Test if accessory and HA are updated accordingly.""" + code = '1234' + config = {ATTR_CODE: code} + entity_id = 'alarm_control_panel.test' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 11 # AlarmSystem - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 3 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.test' + hass.states.async_set(entity_id, STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert acc.char_target_state.value == 0 + assert acc.char_current_state.value == 0 - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={ATTR_CODE: '1234'}) - acc.run() + hass.states.async_set(entity_id, STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert acc.char_target_state.value == 2 + assert acc.char_current_state.value == 2 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 11) # AlarmSystem + hass.states.async_set(entity_id, STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 3 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 3) + hass.states.async_set(entity_id, STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 4 - self.hass.states.set(acp, STATE_ALARM_ARMED_AWAY) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 1) - self.assertEqual(acc.char_current_state.value, 1) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 4 - self.hass.states.set(acp, STATE_ALARM_ARMED_HOME) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 0) - self.assertEqual(acc.char_current_state.value, 0) + # Set from HomeKit + call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') + call_arm_away = async_mock_service(hass, DOMAIN, 'alarm_arm_away') + call_arm_night = async_mock_service(hass, DOMAIN, 'alarm_arm_night') + call_disarm = async_mock_service(hass, DOMAIN, 'alarm_disarm') - self.hass.states.set(acp, STATE_ALARM_ARMED_NIGHT) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 2) - self.assertEqual(acc.char_current_state.value, 2) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_arm_home + assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_home[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 0 - self.hass.states.set(acp, STATE_ALARM_DISARMED) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 3) + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_arm_away + assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_away[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 1 - self.hass.states.set(acp, STATE_ALARM_TRIGGERED) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 4) + await hass.async_add_job(acc.char_target_state.client_update_value, 2) + await hass.async_block_till_done() + assert call_arm_night + assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_night[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 2 - self.hass.states.set(acp, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 4) + await hass.async_add_job(acc.char_target_state.client_update_value, 3) + await hass.async_block_till_done() + assert call_disarm + assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id + assert call_disarm[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 3 - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 0) - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 1) +@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) +async def test_no_alarm_code(hass, config): + """Test accessory if security_system doesn't require a alarm_code.""" + entity_id = 'alarm_control_panel.test' - acc.char_target_state.client_update_value(2) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 2) + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) - acc.char_target_state.client_update_value(3) - self.hass.block_till_done() - self.assertEqual( - self.events[3].data[ATTR_SERVICE], 'alarm_disarm') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 3) + # Set from HomeKit + call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') - def test_no_alarm_code(self): - """Test accessory if security_system doesn't require a alarm_code.""" - acp = 'alarm_control_panel.test' - - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={ATTR_CODE: None}) - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) - self.assertEqual(acc.char_target_state.value, 0) - - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={}) - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) - self.assertEqual(acc.char_target_state.value, 0) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_arm_home + assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id + assert ATTR_CODE not in call_arm_home[0].data + assert acc.char_target_state.value == 0 diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 77bfc0c8901..39f48abd60e 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,6 +1,4 @@ """Test different accessory types: Sensors.""" -import unittest - from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, @@ -9,201 +7,191 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant + +async def test_temperature(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.temperature' + + acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, config=None) + await hass.async_add_job(acc.run) + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_temp.value == 0.0 + for key, value in PROP_CELSIUS.items(): + assert acc.char_temp.properties[key] == value + + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 0.0 + + hass.states.async_set(entity_id, '20', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 20 + + hass.states.async_set(entity_id, '75.2', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + await hass.async_block_till_done() + assert acc.char_temp.value == 24 -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding sensors.""" +async def test_humidity(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + acc = HumiditySensor(hass, 'Humidity', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + assert acc.aid == 2 + assert acc.category == 10 # Sensor - def test_temperature(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.temperature' + assert acc.char_humidity.value == 0 - acc = TemperatureSensor(self.hass, 'Temperature', entity_id, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_humidity.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, '20') + await hass.async_block_till_done() + assert acc.char_humidity.value == 20 - self.assertEqual(acc.char_temp.value, 0.0) - for key, value in PROP_CELSIUS.items(): - self.assertEqual(acc.char_temp.properties[key], value) - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 0.0) +async def test_air_quality(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.air_quality' - self.hass.states.set(entity_id, '20', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 20) + acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.hass.states.set(entity_id, '75.2', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 24) + assert acc.aid == 2 + assert acc.category == 10 # Sensor - def test_humidity(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.humidity' + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 - acc = HumiditySensor(self.hass, 'Humidity', entity_id, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, '34') + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 1 - self.assertEqual(acc.char_humidity.value, 0) + hass.states.async_set(entity_id, '200') + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 5 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_humidity.value, 0) - self.hass.states.set(entity_id, '20') - self.hass.block_till_done() - self.assertEqual(acc.char_humidity.value, 20) +async def test_co2(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.co2' - def test_air_quality(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.air_quality' + acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - acc = AirQualitySensor(self.hass, 'Air Quality', entity_id, - 2, config=None) - acc.run() + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + assert acc.char_co2.value == 0 + assert acc.char_peak.value == 0 + assert acc.char_detected.value == 0 - self.assertEqual(acc.char_density.value, 0) - self.assertEqual(acc.char_quality.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_co2.value == 0 + assert acc.char_peak.value == 0 + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 0) - self.assertEqual(acc.char_quality.value, 0) + hass.states.async_set(entity_id, '1100') + await hass.async_block_till_done() + assert acc.char_co2.value == 1100 + assert acc.char_peak.value == 1100 + assert acc.char_detected.value == 1 - self.hass.states.set(entity_id, '34') - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 34) - self.assertEqual(acc.char_quality.value, 1) + hass.states.async_set(entity_id, '800') + await hass.async_block_till_done() + assert acc.char_co2.value == 800 + assert acc.char_peak.value == 1100 + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, '200') - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 200) - self.assertEqual(acc.char_quality.value, 5) - def test_co2(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.co2' +async def test_light(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.light' - acc = CarbonDioxideSensor(self.hass, 'CO2', entity_id, 2, config=None) - acc.run() + acc = LightSensor(hass, 'Light', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.assertEqual(acc.char_co2.value, 0) - self.assertEqual(acc.char_peak.value, 0) - self.assertEqual(acc.char_detected.value, 0) + assert acc.char_light.value == 0.0001 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 0) - self.assertEqual(acc.char_peak.value, 0) - self.assertEqual(acc.char_detected.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_light.value == 0.0001 - self.hass.states.set(entity_id, '1100') - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 1100) - self.assertEqual(acc.char_peak.value, 1100) - self.assertEqual(acc.char_detected.value, 1) + hass.states.async_set(entity_id, '300') + await hass.async_block_till_done() + assert acc.char_light.value == 300 - self.hass.states.set(entity_id, '800') - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 800) - self.assertEqual(acc.char_peak.value, 1100) - self.assertEqual(acc.char_detected.value, 0) - def test_light(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.light' +async def test_binary(hass): + """Test if accessory is updated after state change.""" + entity_id = 'binary_sensor.opening' - acc = LightSensor(self.hass, 'Light', entity_id, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + acc = BinarySensor(hass, 'Window Opening', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_light.value, 0.0001) + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_light.value, 0.0001) + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, '300') - self.hass.block_till_done() - self.assertEqual(acc.char_light.value, 300) + hass.states.async_set(entity_id, STATE_ON, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 1 - def test_binary(self): - """Test if accessory is updated after state change.""" - entity_id = 'binary_sensor.opening' + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_HOME, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 1 - acc = BinarySensor(self.hass, 'Window Opening', entity_id, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_NOT_HOME, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.assertEqual(acc.char_detected.value, 0) - self.hass.states.set(entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 1) +async def test_binary_device_classes(hass): + """Test if services and characteristics are assigned correctly.""" + entity_id = 'binary_sensor.demo' - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 0) + for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: device_class}) + await hass.async_block_till_done() - self.hass.states.set(entity_id, STATE_HOME, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 1) - - self.hass.states.set(entity_id, STATE_NOT_HOME, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 0) - - self.hass.states.remove(entity_id) - self.hass.block_till_done() - - def test_binary_device_classes(self): - """Test if services and characteristics are assigned correctly.""" - entity_id = 'binary_sensor.demo' - - for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_DEVICE_CLASS: device_class}) - self.hass.block_till_done() - - acc = BinarySensor(self.hass, 'Binary Sensor', entity_id, - 2, config=None) - self.assertEqual(acc.get_service(service).display_name, service) - self.assertEqual(acc.char_detected.display_name, char) + acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, config=None) + assert acc.get_service(service).display_name == service + assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 65b107e24cd..7368179f232 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,104 +1,45 @@ """Test different accessory types: Switches.""" -import unittest +import pytest -from homeassistant.core import callback, split_entity_id +from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ( - ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, - SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSwitches(unittest.TestCase): - """Test class for all accessory types regarding switches.""" +@pytest.mark.parametrize('entity_id', [ + 'switch.test', 'remote.test', 'input_boolean.test']) +async def test_switch_set_state(hass, entity_id): + """Test if accessory and HA are updated accordingly.""" + domain = split_entity_id(entity_id)[0] - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = Switch(hass, 'Switch', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 8 # Switch - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_on.value is False - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value is True - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - entity_id = 'switch.test' - domain = split_entity_id(entity_id)[0] + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value is False - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() + # Set from HomeKit + call_turn_on = async_mock_service(hass, domain, 'turn_on') + call_turn_off = async_mock_service(hass, domain, 'turn_off') - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 8) # Switch + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - self.assertEqual(acc.char_on.value, False) - - self.hass.states.set(entity_id, STATE_ON) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, True) - - self.hass.states.set(entity_id, STATE_OFF) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - - acc.char_on.client_update_value(False) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) - - def test_remote_set_state(self): - """Test service call for remote as domain.""" - entity_id = 'remote.test' - domain = split_entity_id(entity_id)[0] - - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() - - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual(acc.char_on.value, True) - - def test_input_boolean_set_state(self): - """Test service call for remote as domain.""" - entity_id = 'input_boolean.test' - domain = split_entity_id(entity_id)[0] - - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() - - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual(acc.char_on.value, True) + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index fe2a7f6cd02..eea256c134d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,364 +1,347 @@ """Test different accessory types: Thermostats.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, + DOMAIN, ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitThermostats(unittest.TestCase): - """Test class for all accessory types regarding thermostats.""" +@pytest.fixture(scope='module') +def cls(request): + """Patch debounce decorator during import of type_thermostats.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_thermostats', + fromlist=['Thermostat']) + request.addfinalizer(patcher.stop) + patcher_tuple = namedtuple('Cls', ['thermostat']) + return patcher_tuple(thermostat=_import.Thermostat) - @classmethod - def setUpClass(cls): - """Setup Thermostat class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__( - 'homeassistant.components.homekit.type_thermostats', - fromlist=['Thermostat']) - cls.thermostat_cls = _import.Thermostat - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_default_thermostat(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 9 # Thermostat - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 + assert acc.char_current_temp.value == 21.0 + assert acc.char_target_temp.value == 21.0 + assert acc.char_display_units.value == 0 + assert acc.char_cooling_thresh_temp is None + assert acc.char_heating_thresh_temp is None - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - def test_default_thermostat(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 23.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 23.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 20.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 2 + assert acc.char_current_temp.value == 25.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 9) # Thermostat + hass.states.async_set(entity_id, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 19.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 20.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 2 + assert acc.char_current_temp.value == 19.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) - self.assertEqual(acc.char_current_temp.value, 21.0) - self.assertEqual(acc.char_target_temp.value, 21.0) - self.assertEqual(acc.char_display_units.value, 0) - self.assertEqual(acc.char_cooling_thresh_temp, None) - self.assertEqual(acc.char_heating_thresh_temp, None) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_HEAT, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 1) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_HEAT, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 23.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 1) - self.assertEqual(acc.char_current_temp.value, 23.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 25.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_COOL, - {ATTR_OPERATION_MODE: STATE_COOL, - ATTR_TEMPERATURE: 20.0, - ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 20.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 2) - self.assertEqual(acc.char_current_temp.value, 25.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 22.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 22.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_COOL, - {ATTR_OPERATION_MODE: STATE_COOL, - ATTR_TEMPERATURE: 20.0, - ATTR_CURRENT_TEMPERATURE: 19.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 20.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 2) - self.assertEqual(acc.char_current_temp.value, 19.0) - self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') + call_set_operation_mode = async_mock_service(hass, DOMAIN, + 'set_operation_mode') - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_OFF, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job(acc.char_target_temp.client_update_value, 19.0) + await hass.async_block_till_done() + assert call_set_temperature + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 + assert acc.char_target_temp.value == 19.0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_block_till_done() + assert call_set_operation_mode + assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT + assert acc.char_target_heat_cool.value == 1 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 25.0) - self.assertEqual(acc.char_display_units.value, 0) - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 22.0) - self.assertEqual(acc.char_display_units.value, 0) +async def test_auto_thermostat(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - # Set from HomeKit - acc.char_target_temp.client_update_value(19.0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0) - self.assertEqual(acc.char_target_temp.value, 19.0) + # support_auto = True + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - acc.char_target_heat_cool.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_operation_mode') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], - STATE_HEAT) - self.assertEqual(acc.char_target_heat_cool.value, 1) + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 - def test_auto_thermostat(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 22.0, + ATTR_TARGET_TEMP_LOW: 20.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - # support_auto = True - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 24.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 24.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 21.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 21.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 22.0, - ATTR_TARGET_TEMP_LOW: 20.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 23.0, - ATTR_TARGET_TEMP_LOW: 19.0, - ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 24.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job( + acc.char_heating_thresh_temp.client_update_value, 20.0) + await hass.async_block_till_done() + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 + assert acc.char_heating_thresh_temp.value == 20.0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 23.0, - ATTR_TARGET_TEMP_LOW: 19.0, - ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 21.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job( + acc.char_cooling_thresh_temp.client_update_value, 25.0) + await hass.async_block_till_done() + assert call_set_temperature[1] + assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0 + assert acc.char_cooling_thresh_temp.value == 25.0 - # Set from HomeKit - acc.char_heating_thresh_temp.client_update_value(20.0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0) - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - acc.char_cooling_thresh_temp.client_update_value(25.0) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], - 25.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) +async def test_power_state(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - def test_power_state(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + # SUPPORT_ON_OFF = True + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_SUPPORTED_FEATURES: 4096, + ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) + assert acc.support_power_state is True - # SUPPORT_ON_OFF = True - self.hass.states.set(climate, STATE_HEAT, - {ATTR_SUPPORTED_FEATURES: 4096, - ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() - self.assertTrue(acc.support_power_state) + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 1) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_OFF, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') + call_set_operation_mode = async_mock_service(hass, DOMAIN, + 'set_operation_mode') - # Set from HomeKit - acc.char_target_heat_cool.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'turn_on') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], - climate) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_operation_mode') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], - STATE_HEAT) - self.assertEqual(acc.char_target_heat_cool.value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode + assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT + assert acc.char_target_heat_cool.value == 1 - acc.char_target_heat_cool.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'turn_off') - self.assertEqual( - self.events[2].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], - climate) - self.assertEqual(acc.char_target_heat_cool.value, 0) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_heat_cool.value == 0 - def test_thermostat_fahrenheit(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' - # support_auto = True - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() +async def test_thermostat_fahrenheit(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 75.2, - ATTR_TARGET_TEMP_LOW: 68, - ATTR_TEMPERATURE: 71.6, - ATTR_CURRENT_TEMPERATURE: 73.4, - ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) - self.assertEqual(acc.char_current_temp.value, 23.0) - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_display_units.value, 1) + # support_auto = True + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - # Set from HomeKit - acc.char_cooling_thresh_temp.client_update_value(23) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) - self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 24.0 + assert acc.char_current_temp.value == 23.0 + assert acc.char_target_temp.value == 22.0 + assert acc.char_display_units.value == 1 - acc.char_heating_thresh_temp.client_update_value(22) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) - self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - acc.char_target_temp.client_update_value(24.0) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) + await hass.async_add_job( + acc.char_cooling_thresh_temp.client_update_value, 23) + await hass.async_block_till_done() + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 + + await hass.async_add_job( + acc.char_heating_thresh_temp.client_update_value, 22) + await hass.async_block_till_done() + assert call_set_temperature[1] + assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6 + + await hass.async_add_job(acc.char_target_temp.client_update_value, 24.0) + await hass.async_block_till_done() + assert call_set_temperature[2] + assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 4a9521384bd..2ec35975618 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -1,25 +1,20 @@ """Test HomeKit util module.""" -import unittest - -import voluptuous as vol import pytest +import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, ATTR_CODE, - density_to_air_quality) + temperature_to_homekit, temperature_to_states, density_to_air_quality) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) + DOMAIN, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service def test_validate_entity_config(): @@ -68,51 +63,27 @@ def test_density_to_air_quality(): assert density_to_air_quality(300) == 5 -class TestUtil(unittest.TestCase): - """Test all HomeKit util methods.""" +async def test_show_setup_msg(hass): + """Test show setup message as persistence notification.""" + bridge = HomeBridge(hass) - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + call_create_notification = async_mock_service(hass, DOMAIN, 'create') - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + await hass.async_add_job(show_setup_message, hass, bridge) + await hass.async_block_till_done() - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert call_create_notification + assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == \ + HOMEKIT_NOTIFY_ID - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_show_setup_msg(self): - """Test show setup message as persistence notification.""" - bridge = HomeBridge(self.hass) +async def test_dismiss_setup_msg(hass): + """Test dismiss setup message.""" + call_dismiss_notification = async_mock_service(hass, DOMAIN, 'dismiss') - show_setup_message(self.hass, bridge) - self.hass.block_till_done() + await hass.async_add_job(dismiss_setup_message, hass) + await hass.async_block_till_done() - data = self.events[0].data - self.assertEqual( - data.get(ATTR_DOMAIN, None), 'persistent_notification') - self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE) - self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) - self.assertEqual( - data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), - HOMEKIT_NOTIFY_ID) - - def test_dismiss_setup_msg(self): - """Test dismiss setup message.""" - dismiss_setup_message(self.hass) - self.hass.block_till_done() - - data = self.events[0].data - self.assertEqual( - data.get(ATTR_DOMAIN, None), 'persistent_notification') - self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS) - self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) - self.assertEqual( - data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), - HOMEKIT_NOTIFY_ID) + assert call_dismiss_notification + assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \ + HOMEKIT_NOTIFY_ID From 3b39ab5b94e0692df742c12478fb4f5e27ec5a56 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 17:13:00 -0400 Subject: [PATCH 676/924] Remove domain expiry sensor (#14381) --- .../components/sensor/domain_expiry.py | 76 ------------------- requirements_all.txt | 3 - 2 files changed, 79 deletions(-) delete mode 100644 homeassistant/components/sensor/domain_expiry.py diff --git a/homeassistant/components/sensor/domain_expiry.py b/homeassistant/components/sensor/domain_expiry.py deleted file mode 100644 index 9364ce041f2..00000000000 --- a/homeassistant/components/sensor/domain_expiry.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Counter for the days till domain will expire. - -For more details about this sensor please refer to the documentation at -https://home-assistant.io/components/sensor.domain_expiry/ -""" -import logging -from datetime import datetime, timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_DOMAIN) -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['python-whois==0.6.9'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Domain Expiry' - -SCAN_INTERVAL = timedelta(hours=24) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up domain expiry sensor.""" - server_name = config.get(CONF_DOMAIN) - sensor_name = config.get(CONF_NAME) - - add_devices([DomainExpiry(sensor_name, server_name)], True) - - -class DomainExpiry(Entity): - """Implementation of the domain expiry sensor.""" - - def __init__(self, sensor_name, server_name): - """Initialize the sensor.""" - self.server_name = server_name - self._name = sensor_name - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return 'days' - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return 'mdi:earth' - - def update(self): - """Fetch the domain information.""" - import whois - domain = whois.whois(self.server_name) - if isinstance(domain.expiration_date, datetime): - expiry = domain.expiration_date - datetime.today() - self._state = expiry.days - else: - _LOGGER.error("Cannot get expiry date for %s", self.server_name) diff --git a/requirements_all.txt b/requirements_all.txt index 6bcd267e456..0887ff98996 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,9 +1041,6 @@ python-velbus==2.0.11 # homeassistant.components.media_player.vlc python-vlc==1.1.2 -# homeassistant.components.sensor.domain_expiry -python-whois==0.6.9 - # homeassistant.components.wink python-wink==1.7.3 From 8c0b45af1e98e84d97d02ff6fe0657b0bb977c87 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 22:40:04 -0400 Subject: [PATCH 677/924] Version bump to 0.69.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 60bc6a78213..0f319891649 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ef8fc1f2018412749fdb29d109245fe06bcbfc10 Mon Sep 17 00:00:00 2001 From: damarco Date: Fri, 11 May 2018 07:32:16 +0200 Subject: [PATCH 678/924] Update sensor state before adding device (#14357) --- homeassistant/components/sensor/zha.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 41dab282997..6979690708d 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -25,7 +25,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return sensor = yield from make_sensor(discovery_info) - async_add_devices([sensor]) + async_add_devices([sensor], update_before_add=True) @asyncio.coroutine @@ -61,6 +61,11 @@ class Sensor(zha.Entity): value_attribute = 0 min_reportable_change = 1 + @property + def should_poll(self) -> bool: + """State gets pushed from device.""" + return False + @property def state(self) -> str: """Return the state of the entity.""" @@ -75,6 +80,14 @@ class Sensor(zha.Entity): self._state = value self.async_schedule_update_ha_state() + async def async_update(self): + """Retrieve latest state.""" + result = await zha.safe_read( + list(self._in_clusters.values())[0], + [self.value_attribute] + ) + self._state = result.get(self.value_attribute, self._state) + class TemperatureSensor(Sensor): """ZHA temperature sensor.""" From be3b227a878b8f570bf320b776d861301967b20b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 May 2018 09:39:18 +0200 Subject: [PATCH 679/924] Make mysensors component async (#13641) * Make mysensors component async * Use async dispatcher and discovery. * Run I/O in executor. * Make mysensors actuator methods async. * Upgrade pymysensors to 0.13.0. * Use async serial gateway. * Use async TCP gateway. * Use async mqtt gateway. * Start gateway before hass start event * Make sure gateway is started after discovery of persistent devices and after corresponding platforms have been loaded. * Don't wait to start gateway until after hass start. * Bump pymysensors to 0.14.0 --- homeassistant/components/climate/mysensors.py | 12 +- homeassistant/components/cover/mysensors.py | 14 +- homeassistant/components/light/mysensors.py | 16 +-- homeassistant/components/mysensors.py | 120 +++++++++++------- homeassistant/components/notify/mysensors.py | 2 +- homeassistant/components/switch/mysensors.py | 24 ++-- requirements_all.txt | 2 +- 7 files changed, 110 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 2545094ceec..9fab56c61ac 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): """List of available fan modes.""" return ['Auto', 'Min', 'Normal', 'Max'] - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" set_req = self.gateway.const.SetReq temp = kwargs.get(ATTR_TEMPERATURE) @@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan_mode - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode): """Set new target temperature.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, @@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[self.value_type] = operation_mode - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 669a7ce6723..3f8eb054710 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_DIMMER) - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): self._values[set_req.V_DIMMER] = 100 else: self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): self._values[set_req.V_DIMMER] = 0 else: self._values[set_req.V_LIGHT] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq @@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): if self.gateway.optimistic: # Optimistically assume that cover has changed state. self._values[set_req.V_DIMMER] = position - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the device.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 6e41e0f5693..55387288d7f 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): self._white = white self._values[self.value_type] = hex_color - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value( @@ -139,7 +139,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): # optimistically assume that light has changed state self._state = False self._values[value_type] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def _async_update_light(self): """Update the controller with values from light child.""" @@ -171,12 +171,12 @@ class MySensorsLightDimmer(MySensorsLight): """Flag supported features.""" return SUPPORT_BRIGHTNESS - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -196,13 +196,13 @@ class MySensorsLightRGB(MySensorsLight): return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_COLOR - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -225,10 +225,10 @@ class MySensorsLightRGBW(MySensorsLightRGB): return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW return SUPPORT_MYSENSORS_RGBW - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 9b394457973..f5ad59095dc 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,6 +4,7 @@ Connect to a MySensors gateway via pymysensors API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mysensors/ """ +import asyncio from collections import defaultdict import logging import os @@ -16,17 +17,17 @@ import voluptuous as vol from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, + ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) + async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component -REQUIREMENTS = ['pymysensors==0.11.1'] +REQUIREMENTS = ['pymysensors==0.14.0'] _LOGGER = logging.getLogger(__name__) @@ -280,67 +281,62 @@ MYSENSORS_CONST_SCHEMA = { } -def setup(hass, config): +async def async_setup(hass, config): """Set up the MySensors component.""" import mysensors.mysensors as mysensors version = config[DOMAIN].get(CONF_VERSION) persistence = config[DOMAIN].get(CONF_PERSISTENCE) - def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix): + async def setup_gateway( + device, persistence_file, baud_rate, tcp_port, in_prefix, + out_prefix): """Return gateway after setup of the gateway.""" if device == MQTT_COMPONENT: - if not setup_component(hass, MQTT_COMPONENT, config): - return + if not await async_setup_component(hass, MQTT_COMPONENT, config): + return None mqtt = hass.components.mqtt retain = config[DOMAIN].get(CONF_RETAIN) def pub_callback(topic, payload, qos, retain): """Call MQTT publish function.""" - mqtt.publish(topic, payload, qos, retain) + mqtt.async_publish(topic, payload, qos, retain) def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(topic, sub_cb, qos) - gateway = mysensors.MQTTGateway( - pub_callback, sub_callback, + @callback + def internal_callback(*args): + """Call callback.""" + sub_cb(*args) + + hass.async_add_job( + mqtt.async_subscribe(topic, internal_callback, qos)) + + gateway = mysensors.AsyncMQTTGateway( + pub_callback, sub_callback, in_prefix=in_prefix, + out_prefix=out_prefix, retain=retain, loop=hass.loop, event_callback=None, persistence=persistence, persistence_file=persistence_file, - protocol_version=version, in_prefix=in_prefix, - out_prefix=out_prefix, retain=retain) + protocol_version=version) else: try: - is_serial_port(device) - gateway = mysensors.SerialGateway( - device, event_callback=None, persistence=persistence, + await hass.async_add_job(is_serial_port, device) + gateway = mysensors.AsyncSerialGateway( + device, baud=baud_rate, loop=hass.loop, + event_callback=None, persistence=persistence, persistence_file=persistence_file, - protocol_version=version, baud=baud_rate) + protocol_version=version) except vol.Invalid: - try: - socket.getaddrinfo(device, None) - # valid ip address - gateway = mysensors.TCPGateway( - device, event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version, port=tcp_port) - except OSError: - # invalid ip address - return + gateway = mysensors.AsyncTCPGateway( + device, port=tcp_port, loop=hass.loop, event_callback=None, + persistence=persistence, persistence_file=persistence_file, + protocol_version=version) gateway.metric = hass.config.units.is_metric gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) gateway.device = device gateway.event_callback = gw_callback_factory(hass) - - def gw_start(event): - """Trigger to start of the gateway and any persistence.""" - if persistence: - discover_persistent_devices(hass, gateway) - gateway.start() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start) + if persistence: + await gateway.start_persistence() return gateway @@ -357,7 +353,7 @@ def setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = setup_gateway( + ready_gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) if ready_gateway is not None: @@ -371,9 +367,36 @@ def setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways + hass.async_add_job(finish_setup(hass, gateways)) + return True +async def finish_setup(hass, gateways): + """Load any persistent devices and platforms and start gateway.""" + discover_tasks = [] + start_tasks = [] + for gateway in gateways.values(): + discover_tasks.append(discover_persistent_devices(hass, gateway)) + start_tasks.append(gw_start(hass, gateway)) + if discover_tasks: + # Make sure all devices and platforms are loaded before gateway start. + await asyncio.wait(discover_tasks, loop=hass.loop) + if start_tasks: + await asyncio.wait(start_tasks, loop=hass.loop) + + +async def gw_start(hass, gateway): + """Start the gateway.""" + @callback + def gw_stop(event): + """Trigger to stop the gateway.""" + hass.async_add_job(gateway.stop()) + + await gateway.start() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + + def validate_child(gateway, node_id, child): """Validate that a child has the correct values according to schema. @@ -431,14 +454,18 @@ def validate_child(gateway, node_id, child): return validated +@callback def discover_mysensors_platform(hass, platform, new_devices): """Discover a MySensors platform.""" - discovery.load_platform( - hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}) + task = hass.async_add_job(discovery.async_load_platform( + hass, platform, DOMAIN, + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + return task -def discover_persistent_devices(hass, gateway): +async def discover_persistent_devices(hass, gateway): """Discover platforms for devices loaded via persistence file.""" + tasks = [] new_devices = defaultdict(list) for node_id in gateway.sensors: node = gateway.sensors[node_id] @@ -447,7 +474,9 @@ def discover_persistent_devices(hass, gateway): for platform, dev_ids in validated.items(): new_devices[platform].extend(dev_ids) for platform, dev_ids in new_devices.items(): - discover_mysensors_platform(hass, platform, dev_ids) + tasks.append(discover_mysensors_platform(hass, platform, dev_ids)) + if tasks: + await asyncio.wait(tasks, loop=hass.loop) def get_mysensors_devices(hass, domain): @@ -459,6 +488,7 @@ def get_mysensors_devices(hass, domain): def gw_callback_factory(hass): """Return a new callback for the gateway.""" + @callback def mysensors_callback(msg): """Handle messages from a MySensors gateway.""" start = timer() @@ -489,7 +519,7 @@ def gw_callback_factory(hass): # Only one signal per device is needed. # A device can have multiple platforms, ie multiple schemas. # FOR LATER: Add timer to not signal if another update comes in. - dispatcher_send(hass, signal) + async_dispatcher_send(hass, signal) end = timer() if end - start > 0.1: _LOGGER.debug( diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 257b5995446..1374779c5f0 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -42,7 +42,7 @@ class MySensorsNotificationService(BaseNotificationService): """Initialize the service.""" self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" target_devices = kwargs.get(ATTR_TARGET) devices = [device for device in self.devices.values() diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index c0f45cad861..a91ca6d11e7 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -42,7 +42,7 @@ async def async_setup_platform( hass, DOMAIN, discovery_info, device_class_map, async_add_devices=async_add_devices) - def send_ir_code_service(service): + async def async_send_ir_code_service(service): """Set IR code as device state attribute.""" entity_ids = service.data.get(ATTR_ENTITY_ID) ir_code = service.data.get(ATTR_IR_CODE) @@ -58,10 +58,10 @@ async def async_setup_platform( kwargs = {ATTR_IR_CODE: ir_code} for device in _devices: - device.turn_on(**kwargs) + await device.async_turn_on(**kwargs) hass.services.async_register( - DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service, + DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, schema=SEND_IR_CODE_SERVICE_SCHEMA) @@ -84,23 +84,23 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Return True if switch is on.""" return self._values.get(self.value_type) == STATE_ON - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1) if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0) if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() class MySensorsIRSwitch(MySensorsSwitch): @@ -117,7 +117,7 @@ class MySensorsIRSwitch(MySensorsSwitch): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_LIGHT) == STATE_ON - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the IR switch on.""" set_req = self.gateway.const.SetReq if ATTR_IR_CODE in kwargs: @@ -130,11 +130,11 @@ class MySensorsIRSwitch(MySensorsSwitch): # optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() # turn off switch after switch was turned on - self.turn_off() + await self.async_turn_off() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -142,7 +142,7 @@ class MySensorsIRSwitch(MySensorsSwitch): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index cbc5cce0590..bb822934a1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -869,7 +869,7 @@ pymusiccast==0.1.6 pymyq==0.0.8 # homeassistant.components.mysensors -pymysensors==0.11.1 +pymysensors==0.14.0 # homeassistant.components.lock.nello pynello==1.5.1 From 528ad56530484a7f450fe153a7873e12089a4fde Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 11 May 2018 08:57:00 +0100 Subject: [PATCH 680/924] Adds facebox (#14356) * Adds facebox * Update .coveragerc * Remove facebox * Add test of faces attribute * Add event test * Adds more tests * Adds tests to increase coverage * Rename MOCK_FACES to MOCK_FACE * Adds STATE_UNKNOWN --- .../components/image_processing/facebox.py | 110 ++++++++++++++ .../image_processing/test_facebox.py | 139 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 homeassistant/components/image_processing/facebox.py create mode 100644 tests/components/image_processing/test_facebox.py diff --git a/homeassistant/components/image_processing/facebox.py b/homeassistant/components/image_processing/facebox.py new file mode 100644 index 00000000000..81b43c1f8e0 --- /dev/null +++ b/homeassistant/components/image_processing/facebox.py @@ -0,0 +1,110 @@ +""" +Component that will perform facial detection and identification via facebox. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/image_processing.facebox +""" +import base64 +import logging + +import requests +import voluptuous as vol + +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA, ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) +from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT) + +_LOGGER = logging.getLogger(__name__) + +CLASSIFIER = 'facebox' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, +}) + + +def encode_image(image): + """base64 encode an image stream.""" + base64_img = base64.b64encode(image).decode('ascii') + return {"base64": base64_img} + + +def get_matched_faces(faces): + """Return the name and rounded confidence of matched faces.""" + return {face['name']: round(face['confidence'], 2) + for face in faces if face['matched']} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the classifier.""" + entities = [] + for camera in config[CONF_SOURCE]: + entities.append(FaceClassifyEntity( + config[CONF_IP_ADDRESS], + config[CONF_PORT], + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME) + )) + add_devices(entities) + + +class FaceClassifyEntity(ImageProcessingFaceEntity): + """Perform a face classification.""" + + def __init__(self, ip, port, camera_entity, name=None): + """Init with the API key and model id.""" + super().__init__() + self._url = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER) + self._camera = camera_entity + if name: + self._name = name + else: + camera_name = split_entity_id(camera_entity)[1] + self._name = "{} {}".format( + CLASSIFIER, camera_name) + self._matched = {} + + def process_image(self, image): + """Process an image.""" + response = {} + try: + response = requests.post( + self._url, + json=encode_image(image), + timeout=9 + ).json() + except requests.exceptions.ConnectionError: + _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) + response['success'] = False + + if response['success']: + faces = response['faces'] + total = response['facesCount'] + self.process_faces(faces, total) + self._matched = get_matched_faces(faces) + + else: + self.total_faces = None + self.faces = [] + self._matched = {} + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the classifier attributes.""" + return { + 'matched_faces': self._matched, + } diff --git a/tests/components/image_processing/test_facebox.py b/tests/components/image_processing/test_facebox.py new file mode 100644 index 00000000000..cdc19a3d8d1 --- /dev/null +++ b/tests/components/image_processing/test_facebox.py @@ -0,0 +1,139 @@ +"""The tests for the facebox component.""" +from unittest.mock import patch + +import pytest +import requests +import requests_mock + +from homeassistant.core import callback +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, + CONF_IP_ADDRESS, CONF_PORT, STATE_UNKNOWN) +from homeassistant.setup import async_setup_component +import homeassistant.components.image_processing as ip +import homeassistant.components.image_processing.facebox as fb + +MOCK_IP = '192.168.0.1' +MOCK_PORT = '8080' + +MOCK_FACE = {'confidence': 0.5812028911604818, + 'id': 'john.jpg', + 'matched': True, + 'name': 'John Lennon', + 'rect': {'height': 75, 'left': 63, 'top': 262, 'width': 74} + } + +MOCK_JSON = {"facesCount": 1, + "success": True, + "faces": [MOCK_FACE] + } + +VALID_ENTITY_ID = 'image_processing.facebox_demo_camera' +VALID_CONFIG = { + ip.DOMAIN: { + 'platform': 'facebox', + CONF_IP_ADDRESS: MOCK_IP, + CONF_PORT: MOCK_PORT, + ip.CONF_SOURCE: { + ip.CONF_ENTITY_ID: 'camera.demo_camera'} + }, + 'camera': { + 'platform': 'demo' + } + } + + +def test_encode_image(): + """Test that binary data is encoded correctly.""" + assert fb.encode_image(b'test')["base64"] == 'dGVzdA==' + + +def test_get_matched_faces(): + """Test that matched faces are parsed correctly.""" + assert fb.get_matched_faces([MOCK_FACE]) == {MOCK_FACE['name']: 0.58} + + +@pytest.fixture +def mock_image(): + """Return a mock camera image.""" + with patch('homeassistant.components.camera.demo.DemoCamera.camera_image', + return_value=b'Test') as image: + yield image + + +async def test_setup_platform(hass): + """Setup platform with one entity.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + +async def test_process_image(hass, mock_image): + """Test processing of an image.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + face_events = [] + + @callback + def mock_face_event(event): + """Mock event.""" + face_events.append(event) + + hass.bus.async_listen('image_processing.detect_face', mock_face_event) + + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.post(url, json=MOCK_JSON) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == '1' + assert state.attributes.get('matched_faces') == {MOCK_FACE['name']: 0.58} + + MOCK_FACE[ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update. + assert state.attributes.get('faces') == [MOCK_FACE] + assert state.attributes.get(CONF_FRIENDLY_NAME) == 'facebox demo_camera' + + assert len(face_events) == 1 + assert face_events[0].data['name'] == MOCK_FACE['name'] + assert face_events[0].data['confidence'] == MOCK_FACE['confidence'] + assert face_events[0].data['entity_id'] == VALID_ENTITY_ID + + +async def test_connection_error(hass, mock_image): + """Test connection error.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.register_uri( + 'POST', url, exc=requests.exceptions.ConnectTimeout) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == STATE_UNKNOWN + assert state.attributes.get('faces') == [] + assert state.attributes.get('matched_faces') == {} + + +async def test_setup_platform_with_name(hass): + """Setup platform with one entity and a name.""" + MOCK_NAME = 'mock_name' + NAMED_ENTITY_ID = 'image_processing.{}'.format(MOCK_NAME) + + VALID_CONFIG_NAMED = VALID_CONFIG.copy() + VALID_CONFIG_NAMED[ip.DOMAIN][ip.CONF_SOURCE][ip.CONF_NAME] = MOCK_NAME + + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG_NAMED) + assert hass.states.get(NAMED_ENTITY_ID) + state = hass.states.get(NAMED_ENTITY_ID) + assert state.attributes.get(CONF_FRIENDLY_NAME) == MOCK_NAME From 48d70e520f21338d3f5e698158094e7cf30183aa Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 11 May 2018 20:28:28 +1000 Subject: [PATCH 681/924] more detailed error message (#14385) --- homeassistant/components/sensor/rest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 74bfaa38f02..75235bedaab 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -176,6 +176,7 @@ class RestData(object): self._request, timeout=10, verify=self._verify_ssl) self.data = response.text - except requests.exceptions.RequestException: - _LOGGER.error("Error fetching data: %s", self._request) + except requests.exceptions.RequestException as ex: + _LOGGER.error("Error fetching data: %s from %s failed with %s", + self._request, self._request.url, ex) self.data = None From 621c653fed587508f0052156b2a25bb7111037c7 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 11 May 2018 08:22:45 -0400 Subject: [PATCH 682/924] Allow HomeKit name to be customized (#14159) --- homeassistant/components/homekit/__init__.py | 17 ++++---- .../components/homekit/accessories.py | 12 +++--- .../components/homekit/type_covers.py | 6 +-- .../components/homekit/type_lights.py | 2 +- .../components/homekit/type_locks.py | 2 +- .../homekit/type_security_systems.py | 4 +- .../components/homekit/type_sensors.py | 12 +++--- .../components/homekit/type_switches.py | 2 +- .../components/homekit/type_thermostats.py | 2 +- homeassistant/components/homekit/util.py | 11 +++-- tests/components/homekit/test_accessories.py | 5 ++- .../homekit/test_get_accessories.py | 43 +++++++++++-------- tests/components/homekit/test_type_covers.py | 8 ++-- tests/components/homekit/test_type_lights.py | 8 ++-- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 10 ++--- tests/components/homekit/test_type_sensors.py | 14 +++--- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 8 ++-- tests/components/homekit/test_util.py | 6 ++- 20 files changed, 97 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c31093a5eb8..028155593fb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -12,20 +12,19 @@ import voluptuous as vol from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, - TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, - DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, + DEFAULT_AUTO_START, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) -from .util import ( - validate_entity_config, show_setup_message) +from .util import show_setup_message, validate_entity_config TYPES = Registry() _LOGGER = logging.getLogger(__name__) @@ -93,7 +92,7 @@ def get_accessory(hass, state, aid, config): return None a_type = None - config = config or {} + name = config.get(CONF_NAME, state.name) if state.domain == 'alarm_control_panel': a_type = 'SecuritySystem' @@ -147,7 +146,7 @@ def get_accessory(hass, state, aid, config): return None _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config) + return TYPES[a_type](hass, name, state.entity_id, aid, config) def generate_aid(entity_id): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c47c3f8fbe7..7ec1fb542c9 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -16,8 +16,8 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, - BRIDGE_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + DEBOUNCE_TIMEOUT, MANUFACTURER) from .util import ( show_setup_message, dismiss_setup_message) @@ -64,14 +64,16 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): + def __init__(self, hass, name, entity_id, aid, config, + category=CATEGORY_OTHER): """Initialize a Accessory object.""" super().__init__(name, aid=aid) - domain = split_entity_id(entity_id)[0].replace("_", " ").title() + model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, - model=domain, serial_number=entity_id) + model=model, serial_number=entity_id) self.category = category + self.config = config self.entity_id = entity_id self.hass = hass diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3de87cf63e8..a32ba0370ec 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -28,7 +28,7 @@ class GarageDoorOpener(HomeAccessory): and support no more than open, close, and stop. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a GarageDoorOpener accessory object.""" super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) self.flag_target_state = False @@ -69,7 +69,7 @@ class WindowCovering(HomeAccessory): The cover entity must support: set_cover_position. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.homekit_target = None @@ -108,7 +108,7 @@ class WindowCoveringBasic(HomeAccessory): stop_cover (optional). """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) features = self.hass.states.get(self.entity_id) \ diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 3efb0e99df6..d8a205d7026 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -26,7 +26,7 @@ class Light(HomeAccessory): Currently supports: state, brightness, color temperature, rgb_color. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index e7f18d44805..b08ac5930bd 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -29,7 +29,7 @@ class Lock(HomeAccessory): The lock entity must support: unlock and lock. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index ab16f921e99..bd29453e10a 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -32,10 +32,10 @@ STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) - self._alarm_code = config.get(ATTR_CODE) + self._alarm_code = self.config.get(ATTR_CODE) self.flag_target_state = False serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 393b6beffd6..0005c6184ee 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -51,7 +51,7 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a TemperatureSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) @@ -74,7 +74,7 @@ class TemperatureSensor(HomeAccessory): class HumiditySensor(HomeAccessory): """Generate a HumiditySensor accessory as humidity sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a HumiditySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) @@ -94,7 +94,7 @@ class HumiditySensor(HomeAccessory): class AirQualitySensor(HomeAccessory): """Generate a AirQualitySensor accessory as air quality sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -118,7 +118,7 @@ class AirQualitySensor(HomeAccessory): class CarbonDioxideSensor(HomeAccessory): """Generate a CarbonDioxideSensor accessory as CO2 sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -146,7 +146,7 @@ class CarbonDioxideSensor(HomeAccessory): class LightSensor(HomeAccessory): """Generate a LightSensor accessory as light sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a LightSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -166,7 +166,7 @@ class LightSensor(HomeAccessory): class BinarySensor(HomeAccessory): """Generate a BinarySensor accessory as binary sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a BinarySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) device_class = self.hass.states.get(self.entity_id).attributes \ diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 68a4fcdab0a..ff4bf1611b8 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Switch accessory object to represent a remote.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 15fd8160a7e..ab4d7faf875 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -38,7 +38,7 @@ SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \ class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 29fe3c8f265..c201d884a75 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, TEMP_CELSIUS) + ATTR_CODE, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID @@ -16,13 +16,18 @@ _LOGGER = logging.getLogger(__name__) def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" entities = {} - for key, config in values.items(): - entity = cv.entity_id(key) + for entity_id, config in values.items(): + entity = cv.entity_id(entity_id) params = {} if not isinstance(config, dict): raise vol.Invalid('The configuration for "{}" must be ' ' an dictionary.'.format(entity)) + for key in (CONF_NAME, ): + value = config.get(key, -1) + if value != -1: + params[key] = cv.string(value) + domain, _ = split_entity_id(entity) if domain == 'alarm_control_panel': diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 48c6357c28d..799a831b745 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -56,9 +56,10 @@ async def test_debounce(hass): async def test_home_accessory(hass): """Test HomeAccessory class.""" - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' + assert acc.aid == 2 assert acc.category == 1 # Category.OTHER assert len(acc.services) == 1 serv = acc.services[0] # SERV_ACCESSORY_INFO @@ -75,7 +76,7 @@ async def test_home_accessory(hass): hass.states.async_set('homekit.accessory', 'off') await hass.async_block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) assert acc.display_name == 'test_name' assert acc.aid == 2 assert len(acc.services) == 1 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 2ff591983c6..a6827300862 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -11,33 +11,42 @@ from homeassistant.components.climate import ( from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) _LOGGER = logging.getLogger(__name__) -def test_get_accessory_invalid_aid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('light.demo', 'on'), - None, config=None) is None +def test_not_supported(caplog): + """Test if none is returned if entity isn't supported.""" + # not supported entity + assert get_accessory(None, State('demo.demo', 'on'), 2, {}) is None + + # invalid aid + assert get_accessory(None, State('light.demo', 'on'), None, None) is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg -def test_not_supported(): - """Test if none is returned if entity isn't supported.""" - assert get_accessory(None, State('demo.demo', 'on'), 2, config=None) \ - is None +@pytest.mark.parametrize('config, name', [ + ({CONF_NAME: 'Customize Name'}, 'Customize Name'), +]) +def test_customize_options(config, name): + """Test with customized options.""" + mock_type = Mock() + with patch.dict(TYPES, {'Light': mock_type}): + entity_state = State('light.demo', 'on') + get_accessory(None, entity_state, 2, config) + mock_type.assert_called_with(None, name, 'light.demo', 2, config) @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ - ('Light', 'light.test', 'on', {}, None), - ('Lock', 'lock.test', 'locked', {}, None), + ('Light', 'light.test', 'on', {}, {}), + ('Lock', 'lock.test', 'locked', {}, {}), - ('Thermostat', 'climate.test', 'auto', {}, None), + ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}, None), + SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), @@ -51,7 +60,7 @@ def test_types(type_name, entity_id, state, attrs, config): assert mock_type.called if config: - assert mock_type.call_args[1]['config'] == config + assert mock_type.call_args[0][-1] == config @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ @@ -68,7 +77,7 @@ def test_type_covers(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, None) + get_accessory(None, entity_state, 2, {}) assert mock_type.called @@ -104,7 +113,7 @@ def test_type_sensors(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, None) + get_accessory(None, entity_state, 2, {}) assert mock_type.called @@ -118,5 +127,5 @@ def test_type_switches(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, None) + get_accessory(None, entity_state, 2, {}) assert mock_type.called diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index b833e1a03c9..fcc807338a9 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -32,7 +32,7 @@ async def test_garage_door_open_close(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' - acc = cls.garage(hass, 'Garage Door', entity_id, 2, config=None) + acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -87,7 +87,7 @@ async def test_window_set_cover_position(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' - acc = cls.window(hass, 'Cover', entity_id, 2, config=None) + acc = cls.window(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -135,7 +135,7 @@ async def test_window_open_close(hass, cls): hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -198,7 +198,7 @@ async def test_window_open_close_stop(hass, cls): hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) # Set from HomeKit diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index b4965fc5ab8..d9602a6e41f 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -33,7 +33,7 @@ async def test_light_basic(hass, cls): hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 5 # Lightbulb @@ -81,7 +81,7 @@ async def test_light_brightness(hass, cls): hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.char_brightness.value == 0 @@ -126,7 +126,7 @@ async def test_light_color_temperature(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.char_color_temperature.value == 153 @@ -153,7 +153,7 @@ async def test_light_rgb_color(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3442c0da6c8..343fce288ac 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -11,7 +11,7 @@ async def test_lock_unlock(hass): """Test if accessory and HA are updated accordingly.""" entity_id = 'lock.kitchen_door' - acc = Lock(hass, 'Lock', entity_id, 2, config=None) + acc = Lock(hass, 'Lock', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 8c3d9474f26..59a700f73ee 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -5,9 +5,9 @@ from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.homekit.type_security_systems import ( SecuritySystem) from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED) from tests.common import async_mock_service @@ -18,7 +18,7 @@ async def test_switch_set_state(hass): config = {ATTR_CODE: code} entity_id = 'alarm_control_panel.test' - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -97,7 +97,7 @@ async def test_no_alarm_code(hass, config): """Test accessory if security_system doesn't require a alarm_code.""" entity_id = 'alarm_control_panel.test' - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 39f48abd60e..a422116014d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -12,7 +12,7 @@ async def test_temperature(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' - acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, config=None) + acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -42,7 +42,7 @@ async def test_humidity(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' - acc = HumiditySensor(hass, 'Humidity', entity_id, 2, config=None) + acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -63,7 +63,7 @@ async def test_air_quality(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' - acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, config=None) + acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -92,7 +92,7 @@ async def test_co2(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' - acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, config=None) + acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -125,7 +125,7 @@ async def test_light(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' - acc = LightSensor(hass, 'Light', entity_id, 2, config=None) + acc = LightSensor(hass, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -150,7 +150,7 @@ async def test_binary(hass): {ATTR_DEVICE_CLASS: 'opening'}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Window Opening', entity_id, 2, config=None) + acc = BinarySensor(hass, 'Window Opening', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -192,6 +192,6 @@ async def test_binary_device_classes(hass): {ATTR_DEVICE_CLASS: device_class}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, config=None) + acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 7368179f232..00c1966305f 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -14,7 +14,7 @@ async def test_switch_set_state(hass, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] - acc = Switch(hass, 'Switch', entity_id, 2, config=None) + acc = Switch(hass, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index eea256c134d..ea592bd63dd 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -33,7 +33,7 @@ async def test_default_thermostat(hass, cls): hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -173,7 +173,7 @@ async def test_auto_thermostat(hass, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.char_cooling_thresh_temp.value == 23.0 @@ -252,7 +252,7 @@ async def test_power_state(hass, cls): ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.support_power_state is True @@ -304,7 +304,7 @@ async def test_thermostat_fahrenheit(hass, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) hass.states.async_set(entity_id, STATE_AUTO, diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 2ec35975618..0b3a5475f7e 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -12,7 +12,7 @@ from homeassistant.components.homekit.util import validate_entity_config \ from homeassistant.components.persistent_notification import ( DOMAIN, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) from tests.common import async_mock_service @@ -21,13 +21,15 @@ def test_validate_entity_config(): """Test validate entities.""" configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}] + {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] for conf in configs: with pytest.raises(vol.Invalid): vec(conf) assert vec({}) == {} + assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ + {'demo.test': {CONF_NAME: 'Name'}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} From d6b81fb3459582854e09c6ec4d1adb3c9af14774 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 11 May 2018 22:40:32 +0200 Subject: [PATCH 683/924] Xiaomi Aqara: Add new cube model (sensor_cube.aqgl01) (#14393) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 49f716b9eb7..1c0b903d868 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'channel_1', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Both)', 'dual_channel', hass, gateway)) - elif model in ['cube', 'sensor_cube']: + elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']: devices.append(XiaomiCube(device, hass, gateway)) add_devices(devices) From e80628d45bd77121a03b3177cd85ec8db3bd5f1f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 12 May 2018 01:51:48 -0400 Subject: [PATCH 684/924] Bump pycmus version (#14395) This commit bumps the pycmus version used by the cmus component. There was a bug in the previous version used, 1.0.0, when running in local mode. This was caused by a mtreinish/pycmus#1 and also was reported in the home-assistant forums (but not as an issue): https://community.home-assistant.io/t/cant-install-cmus-component/7961 Version 0.1.1 of pycmus fixes this issue so it should work properly for users running cmus and home-assistant on the same machine. --- homeassistant/components/media_player/cmus.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index bcbee5c4ff7..0758b5f3058 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_PASSWORD) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pycmus==0.1.0'] +REQUIREMENTS = ['pycmus==0.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bb822934a1a..1ebc27cf248 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -729,7 +729,7 @@ pychannels==1.0.0 pychromecast==2.1.0 # homeassistant.components.media_player.cmus -pycmus==0.1.0 +pycmus==0.1.1 # homeassistant.components.comfoconnect pycomfoconnect==0.3 From 304137e7ff84ba504c6ebdae451b046f80f46131 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 12 May 2018 10:07:10 +0200 Subject: [PATCH 685/924] Fix name of tox pylint env (#14402) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 86acefe9b3f..fb1f7c8bda3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, lint, requirements, typing +envlist = py35, py36, lint, pylint, typing skip_missing_interpreters = True [testenv] From b903bbc04212a4cdfcdac5288827fa405a6020ca Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sat, 12 May 2018 10:30:21 +0200 Subject: [PATCH 686/924] Fix waiting for setup that never happens (#14346) --- homeassistant/components/matrix.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py index 569b012b484..b2805c994e8 100644 --- a/homeassistant/components/matrix.py +++ b/homeassistant/components/matrix.py @@ -114,9 +114,6 @@ class MatrixBot(object): self._listening_rooms = listening_rooms - # Logging in is deferred b/c it does I/O - self._setup_done = False - # We have to fetch the aliases for every room to make sure we don't # join it twice by accident. However, fetching aliases is costly, # so we only do it once per room. @@ -343,9 +340,5 @@ class MatrixBot(object): def handle_send_message(self, service): """Handle the send_message service.""" - if not self._setup_done: - _LOGGER.warning("Could not send message: setup is not done!") - return - self._send_message(service.data[ATTR_MESSAGE], service.data[ATTR_TARGET]) From 01ce43ec7cc6007713ec32b8edfbe8d3a9c54665 Mon Sep 17 00:00:00 2001 From: damarco Date: Sat, 12 May 2018 14:41:44 +0200 Subject: [PATCH 687/924] Use None as initial state in zha component (#14389) * Return None if state is unknown * Use None as initial state --- homeassistant/components/binary_sensor/zha.py | 2 +- homeassistant/components/fan/zha.py | 5 ++--- homeassistant/components/light/zha.py | 3 +-- homeassistant/components/sensor/zha.py | 12 ++++++------ homeassistant/components/switch/zha.py | 2 +- homeassistant/components/zha/__init__.py | 2 +- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 4f3f824c8f9..d3b31188760 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -108,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - if self._state == 'unknown': + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py index 3288a788e1f..01b1d0a92cf 100644 --- a/homeassistant/components/fan/zha.py +++ b/homeassistant/components/fan/zha.py @@ -10,7 +10,6 @@ from homeassistant.components import zha from homeassistant.components.fan import ( DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) -from homeassistant.const import STATE_UNKNOWN DEPENDENCIES = ['zha'] @@ -72,7 +71,7 @@ class ZhaFan(zha.Entity, FanEntity): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return False return self._state != SPEED_OFF @@ -103,7 +102,7 @@ class ZhaFan(zha.Entity, FanEntity): """Retrieve latest state.""" result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) new_value = result.get('fan_mode', None) - self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) + self._state = VALUE_TO_SPEED.get(new_value, None) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 8eb1b3dc9b6..b44bf820b23 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -6,7 +6,6 @@ at https://home-assistant.io/components/light.zha/ """ import logging from homeassistant.components import light, zha -from homeassistant.const import STATE_UNKNOWN import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -76,7 +75,7 @@ class Light(zha.Entity, light.Light): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 6979690708d..3ca908a679d 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -102,8 +102,8 @@ class TemperatureSensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None celsius = round(float(self._state) / 100, 1) return convert_temperature( celsius, TEMP_CELSIUS, self.unit_of_measurement) @@ -122,8 +122,8 @@ class RelativeHumiditySensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None return round(float(self._state) / 100, 1) @@ -139,7 +139,7 @@ class PressureSensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None return round(float(self._state)) diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index 22eb50be86b..6109dc192f3 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -51,7 +51,7 @@ class Switch(zha.Entity, SwitchDevice): @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" - if self._state == 'unknown': + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index d293d4d07cd..238e89c07f0 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -319,7 +319,7 @@ class Entity(entity.Entity): self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters - self._state = ha_const.STATE_UNKNOWN + self._state = None self._unique_id = unique_id # Normally the entity itself is the listener. Sub-classes may set this From b371bf700f761c40a621acf7285d362a1a5bb68e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 12 May 2018 15:09:48 +0200 Subject: [PATCH 688/924] Bump PyXiaomiGateway version (#14412) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 48c54cdecff..cc7f3c8139d 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1ebc27cf248..269e9f22a6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.0 +PyXiaomiGateway==0.9.1 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 6fedad7890b8f043b49b5ee22f52b059514c43cd Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sat, 12 May 2018 10:30:21 +0200 Subject: [PATCH 689/924] Fix waiting for setup that never happens (#14346) --- homeassistant/components/matrix.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py index 569b012b484..b2805c994e8 100644 --- a/homeassistant/components/matrix.py +++ b/homeassistant/components/matrix.py @@ -114,9 +114,6 @@ class MatrixBot(object): self._listening_rooms = listening_rooms - # Logging in is deferred b/c it does I/O - self._setup_done = False - # We have to fetch the aliases for every room to make sure we don't # join it twice by accident. However, fetching aliases is costly, # so we only do it once per room. @@ -343,9 +340,5 @@ class MatrixBot(object): def handle_send_message(self, service): """Handle the send_message service.""" - if not self._setup_done: - _LOGGER.warning("Could not send message: setup is not done!") - return - self._send_message(service.data[ATTR_MESSAGE], service.data[ATTR_TARGET]) From d17186a8b708170ed4e5727a8c28d1264c8b146b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 May 2018 09:34:28 -0400 Subject: [PATCH 690/924] Version bump to 0.69.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0f319891649..8de8922e4e0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 69 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 990f476ac9188c1728ea1d69ce6cb2b52c51bcca Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 12 May 2018 17:10:19 +0200 Subject: [PATCH 691/924] Homekit test cleanup (#14416) --- tests/components/homekit/test_accessories.py | 12 ++++++++++-- tests/components/homekit/test_get_accessories.py | 3 --- tests/components/homekit/test_type_covers.py | 4 ++++ tests/components/homekit/test_type_locks.py | 2 ++ .../components/homekit/test_type_security_systems.py | 4 ++++ tests/components/homekit/test_type_sensors.py | 10 ++++++++++ tests/components/homekit/test_type_switches.py | 2 ++ 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 799a831b745..f12b80632b6 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -56,7 +56,11 @@ async def test_debounce(hass): async def test_home_accessory(hass): """Test HomeAccessory class.""" - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, None) + entity_id = 'homekit.accessory' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, 'Home Accessory', entity_id, 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' assert acc.aid == 2 @@ -76,7 +80,11 @@ async def test_home_accessory(hass): hass.states.async_set('homekit.accessory', 'off') await hass.async_block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) + entity_id = 'test_model.demo' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = HomeAccessory('hass', 'test_name', entity_id, 2, None) assert acc.display_name == 'test_name' assert acc.aid == 2 assert len(acc.services) == 1 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index a6827300862..cdfb858b727 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,5 +1,4 @@ """Package to test the get_accessory method.""" -import logging from unittest.mock import patch, Mock import pytest @@ -13,8 +12,6 @@ from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) -_LOGGER = logging.getLogger(__name__) - def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index fcc807338a9..dc4caeb35a6 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -32,6 +32,8 @@ async def test_garage_door_open_close(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -87,6 +89,8 @@ async def test_window_set_cover_position(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = cls.window(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 343fce288ac..984d032a1d9 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -11,6 +11,8 @@ async def test_lock_unlock(hass): """Test if accessory and HA are updated accordingly.""" entity_id = 'lock.kitchen_door' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = Lock(hass, 'Lock', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 59a700f73ee..da5dac2d81b 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -18,6 +18,8 @@ async def test_switch_set_state(hass): config = {ATTR_CODE: code} entity_id = 'alarm_control_panel.test' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) await hass.async_add_job(acc.run) @@ -97,6 +99,8 @@ async def test_no_alarm_code(hass, config): """Test accessory if security_system doesn't require a alarm_code.""" entity_id = 'alarm_control_panel.test' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) # Set from HomeKit diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index a422116014d..56742bada92 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -12,6 +12,8 @@ async def test_temperature(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -42,6 +44,8 @@ async def test_humidity(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -63,6 +67,8 @@ async def test_air_quality(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -92,6 +98,8 @@ async def test_co2(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -125,6 +133,8 @@ async def test_light(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = LightSensor(hass, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 00c1966305f..399a8bd84c8 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -14,6 +14,8 @@ async def test_switch_set_state(hass, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = Switch(hass, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) From 99e272fc8d37de29e7660dfee6b062007201c66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 12 May 2018 21:12:53 +0200 Subject: [PATCH 692/924] Upgrade PyXiaomiGatewa to 0.9.3 (#14420) (Closes: #14417) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index cc7f3c8139d..2cbf977443c 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.1'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 269e9f22a6c..d81dcc8280d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.1 +PyXiaomiGateway==0.9.3 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 70af7e5fad509d91db4e7900a3f3863f5cbb3f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 12 May 2018 22:22:20 +0200 Subject: [PATCH 693/924] Update pylint to 1.8.4 (#14421) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6d5f68615be..9dcccd0d1da 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.3 +pylint==1.8.4 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 630ed06580c..e2157389a16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.3 +pylint==1.8.4 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From 7aec098a05bf424f06988aa2d4fbeac0eb41269f Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 13 May 2018 00:44:53 +0300 Subject: [PATCH 694/924] Bring back typing check. Meanwhile just for homeassistant/*.py (#14410) * Bring back typing check. Meanwhile just for homeassistant/.py * Change follow-imports to silent. Add a few more checks. --- .travis.yml | 4 ++-- homeassistant/__main__.py | 5 +++-- homeassistant/auth.py | 2 +- homeassistant/bootstrap.py | 5 +++-- homeassistant/config.py | 6 +++--- homeassistant/core.py | 25 +++++++++++++++---------- homeassistant/exceptions.py | 3 ++- homeassistant/loader.py | 2 +- homeassistant/setup.py | 10 ++++++---- tests/conftest.py | 6 +++--- tox.ini | 3 ++- 11 files changed, 41 insertions(+), 30 deletions(-) diff --git a/.travis.yml b/.travis.yml index bf2d05bb185..b089d3f89be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ matrix: env: TOXENV=lint - python: "3.5.3" env: TOXENV=pylint - # - python: "3.5" - # env: TOXENV=typing + - python: "3.5.3" + env: TOXENV=typing - python: "3.5.3" env: TOXENV=py35 - python: "3.6" diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index deb1746c167..7d3d2d2af88 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -8,7 +8,8 @@ import subprocess import sys import threading -from typing import Optional, List +from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import + from homeassistant import monkey_patch from homeassistant.const import ( @@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str, config = { 'frontend': {}, 'demo': {} - } + } # type: Dict[str, Any] hass = bootstrap.from_config_dict( config, config_dir=config_dir, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 2c6c95f9b42..7c01776b7b1 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -35,7 +35,7 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) DATA_REQS = 'auth_reqs_processed' -def generate_secret(entropy=32): +def generate_secret(entropy: int = 32) -> str: """Generate a secret. Backport of secrets.token_hex from Python 3.6 diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 826cc563e82..a405362d368 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -278,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant, if log_rotate_days: err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when='midnight', backupCount=log_rotate_days) + err_log_path, when='midnight', + backupCount=log_rotate_days) # type: logging.FileHandler else: err_handler = logging.FileHandler( err_log_path, mode='w', delay=True) @@ -297,7 +298,7 @@ def async_enable_logging(hass: core.HomeAssistant, EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) logger = logging.getLogger('') - logger.addHandler(async_handler) + logger.addHandler(async_handler) # type: ignore logger.setLevel(logging.INFO) # Save the log file location for access by other components. diff --git a/homeassistant/config.py b/homeassistant/config.py index 5c432490f6a..2f916e69b76 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ import os import re import shutil # pylint: disable=unused-import -from typing import Any, List, Tuple # NOQA +from typing import Any, List, Tuple, Optional # NOQA import voluptuous as vol from voluptuous.humanize import humanize_error @@ -60,7 +60,7 @@ DEFAULT_CORE_CONFIG = ( (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), (CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'), -) # type: Tuple[Tuple[str, Any, Any, str], ...] +) # type: Tuple[Tuple[str, Any, Any, Optional[str]], ...] DEFAULT_CONFIG = """ # Show links to resources in log and frontend introduction: @@ -167,7 +167,7 @@ def get_default_config_dir() -> str: """Put together the default configuration directory based on the OS.""" data_dir = os.getenv('APPDATA') if os.name == "nt" \ else os.path.expanduser('~') - return os.path.join(data_dir, CONFIG_DIR_NAME) + return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore def ensure_config_exists(config_dir: str, detect_location: bool = True) -> str: diff --git a/homeassistant/core.py b/homeassistant/core.py index feb8d331ae8..bc3b598180c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,7 @@ import threading from time import monotonic from types import MappingProxyType -from typing import Optional, Any, Callable, List # NOQA +from typing import Optional, Any, Callable, List, TypeVar, Dict # NOQA from async_timeout import timeout import voluptuous as vol @@ -41,6 +41,8 @@ import homeassistant.util.dt as dt_util import homeassistant.util.location as location from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA +T = TypeVar('T') + DOMAIN = 'homeassistant' # How long we wait for the result of a service call @@ -70,16 +72,15 @@ def valid_state(state: str) -> bool: return len(state) < 256 -def callback(func: Callable[..., None]) -> Callable[..., None]: +def callback(func: Callable[..., T]) -> Callable[..., T]: """Annotation to mark method as safe to call from within the event loop.""" - # pylint: disable=protected-access - func._hass_callback = True + setattr(func, '_hass_callback', True) return func def is_callback(func: Callable[..., Any]) -> bool: """Check if function is safe to be called in the event loop.""" - return '_hass_callback' in getattr(func, '__dict__', {}) + return getattr(func, '_hass_callback', False) is True @callback @@ -136,13 +137,14 @@ class HomeAssistant(object): self.data = {} self.state = CoreState.not_running self.exit_code = None + self.config_entries = None @property def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state in (CoreState.starting, CoreState.running) - def start(self) -> None: + def start(self) -> int: """Start home assistant.""" # Register the async start fire_coroutine_threadsafe(self.async_start(), self.loop) @@ -152,13 +154,13 @@ class HomeAssistant(object): # Block until stopped _LOGGER.info("Starting Home Assistant core loop") self.loop.run_forever() - return self.exit_code except KeyboardInterrupt: self.loop.call_soon_threadsafe( self.loop.create_task, self.async_stop()) self.loop.run_forever() finally: self.loop.close() + return self.exit_code async def async_start(self): """Finalize startup from inside the event loop. @@ -200,7 +202,10 @@ class HomeAssistant(object): self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback - def async_add_job(self, target: Callable[..., None], *args: Any) -> None: + def async_add_job( + self, + target: Callable[..., Any], + *args: Any) -> Optional[asyncio.tasks.Task]: """Add a job from within the eventloop. This method must be run in the event loop. @@ -354,7 +359,7 @@ class EventBus(object): def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners = {} + self._listeners = {} # type: Dict[str, List[Callable]] self._hass = hass @callback @@ -1039,7 +1044,7 @@ class Config(object): # List of allowed external dirs to access self.whitelist_external_dirs = set() - def distance(self: object, lat: float, lon: float) -> float: + def distance(self, lat: float, lon: float) -> float: """Calculate distance from Home Assistant. Async friendly. diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index cb8a3c87820..73bd2377950 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,4 +1,5 @@ """The exceptions used by Home Assistant.""" +import jinja2 class HomeAssistantError(Exception): @@ -22,7 +23,7 @@ class NoEntitySpecifiedError(HomeAssistantError): class TemplateError(HomeAssistantError): """Error during template rendering.""" - def __init__(self, exception): + def __init__(self, exception: jinja2.TemplateError) -> None: """Init the error.""" super().__init__('{}: {}'.format(exception.__class__.__name__, exception)) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 67647a323c9..ce93c8705b5 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -93,7 +93,7 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: # This prevents that when only # custom_components/switch/some_platform.py exists, # the import custom_components.switch would succeed. - if module.__spec__.origin == 'namespace': + if module.__spec__ and module.__spec__.origin == 'namespace': continue _LOGGER.info("Loaded %s from %s", comp_or_platform, path) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f26aa9b61f1..1664653f2a7 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -139,10 +139,11 @@ async def _async_setup_component(hass: core.HomeAssistant, try: if hasattr(component, 'async_setup'): - result = await component.async_setup(hass, processed_config) + result = await component.async_setup( # type: ignore + hass, processed_config) else: result = await hass.async_add_job( - component.setup, hass, processed_config) + component.setup, hass, processed_config) # type: ignore except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, True) @@ -165,14 +166,15 @@ async def _async_setup_component(hass: core.HomeAssistant, for entry in hass.config_entries.async_entries(domain): await entry.async_setup(hass, component=component) - hass.config.components.add(component.DOMAIN) + hass.config.components.add(component.DOMAIN) # type: ignore # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) hass.bus.async_fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} + EVENT_COMPONENT_LOADED, + {ATTR_COMPONENT: component.DOMAIN} # type: ignore ) return True diff --git a/tests/conftest.py b/tests/conftest.py index 73e69605eae..4d619c5ef61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ logging.basicConfig(level=logging.INFO) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) -def test_real(func): +def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" @functools.wraps(func) def guard_func(*args, **kwargs): @@ -40,8 +40,8 @@ def test_real(func): # Guard a few functions that would make network connections -location.detect_location_info = test_real(location.detect_location_info) -location.elevation = test_real(location.elevation) +location.detect_location_info = check_real(location.detect_location_info) +location.elevation = check_real(location.elevation) util.get_local_ip = lambda: '127.0.0.1' diff --git a/tox.ini b/tox.ini index fb1f7c8bda3..d4bea81a2f5 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,8 @@ commands = [testenv:typing] basepython = {env:PYTHON3_PATH:python3} +whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt commands = - mypy --ignore-missing-imports --follow-imports=skip homeassistant + /bin/bash -c 'mypy --ignore-missing-imports --follow-imports=silent homeassistant/*.py' From d1228d5cf4fc4296c58c9417dde4b2ca3f3203a6 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 13 May 2018 00:45:36 +0300 Subject: [PATCH 695/924] Look at registry before pulling zwave config values (#14408) * Look at registry before deciding on ID for zwave values * Reuse the new function --- homeassistant/components/zwave/__init__.py | 35 ++++++++++++------ homeassistant/helpers/entity_registry.py | 16 ++++++-- tests/components/zwave/test_init.py | 43 +++++++++++++++++++++- tests/helpers/test_entity_registry.py | 10 +++++ tests/mock/zwave.py | 1 + 5 files changed, 87 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 7562ac0ff14..a8ba5e4a6d3 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -16,10 +16,11 @@ from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import track_time_change +from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv @@ -218,7 +219,7 @@ async def async_setup_platform(hass, config, async_add_devices, # pylint: disable=R0914 -def setup(hass, config): +async def async_setup(hass, config): """Set up Z-Wave. Will automatically load components to support devices found on the network. @@ -286,7 +287,7 @@ def setup(hass, config): continue values = ZWaveDeviceEntityValues( - hass, schema, value, config, device_config) + hass, schema, value, config, device_config, registry) # We create a new list and update the reference here so that # the list can be safely iterated over in the main thread @@ -294,6 +295,7 @@ def setup(hass, config): hass.data[DATA_ENTITY_VALUES] = new_values component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = await async_get_registry(hass) def node_added(node): """Handle a new node on the network.""" @@ -702,9 +704,9 @@ def setup(hass, config): # Setup autoheal if autoheal: _LOGGER.info("Z-Wave network autoheal is enabled") - track_time_change(hass, heal_network, hour=0, minute=0, second=0) + async_track_time_change(hass, heal_network, hour=0, minute=0, second=0) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) return True @@ -713,7 +715,7 @@ class ZWaveDeviceEntityValues(): """Manages entity access to the underlying zwave value objects.""" def __init__(self, hass, schema, primary_value, zwave_config, - device_config): + device_config, registry): """Initialize the values object with the passed entity schema.""" self._hass = hass self._zwave_config = zwave_config @@ -722,6 +724,7 @@ class ZWaveDeviceEntityValues(): self._values = {} self._entity = None self._workaround_ignore = False + self._registry = registry for name in self._schema[const.DISC_VALUES].keys(): self._values[name] = None @@ -794,9 +797,13 @@ class ZWaveDeviceEntityValues(): workaround_component, component) component = workaround_component - value_name = _value_name(self.primary) - generated_id = generate_entity_id(component + '.{}', value_name, []) - node_config = self._device_config.get(generated_id) + entity_id = self._registry.async_get_entity_id( + component, DOMAIN, + compute_value_unique_id(self._node, self.primary)) + if entity_id is None: + value_name = _value_name(self.primary) + entity_id = generate_entity_id(component + '.{}', value_name, []) + node_config = self._device_config.get(entity_id) # Configure node _LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, " @@ -809,7 +816,7 @@ class ZWaveDeviceEntityValues(): if node_config.get(CONF_IGNORED): _LOGGER.info( - "Ignoring entity %s due to device settings", generated_id) + "Ignoring entity %s due to device settings", entity_id) # No entity will be created for this value self._workaround_ignore = True return @@ -964,6 +971,10 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): if (is_node_parsed(self.node) and self.values.primary.label != "Unknown") or \ self.node.is_ready: - return "{}-{}".format(self.node.node_id, - self.values.primary.object_id) + return compute_value_unique_id(self.node, self.values.primary) return None + + +def compute_value_unique_id(node, value): + """Compute unique_id a value would get if it were to get one.""" + return "{}-{}".format(node.node_id, value.object_id) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b5a9c309119..35cc1015aaf 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -83,6 +83,15 @@ class EntityRegistry: """Check if an entity_id is currently registered.""" return entity_id in self.entities + @callback + def async_get_entity_id(self, domain: str, platform: str, unique_id: str): + """Check if an entity_id is currently registered.""" + for entity in self.entities.values(): + if entity.domain == domain and entity.platform == platform and \ + entity.unique_id == unique_id: + return entity.entity_id + return None + @callback def async_generate_entity_id(self, domain, suggested_object_id): """Generate an entity ID that does not conflict. @@ -99,10 +108,9 @@ class EntityRegistry: def async_get_or_create(self, domain, platform, unique_id, *, suggested_object_id=None): """Get entity. Create if it doesn't exist.""" - for entity in self.entities.values(): - if entity.domain == domain and entity.platform == platform and \ - entity.unique_id == unique_id: - return entity + entity_id = self.async_get_entity_id(domain, platform, unique_id) + if entity_id: + return self.entities[entity_id] entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 0eba19f03a4..a25b725e500 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor.zwave import get_device from homeassistant.components.zwave import ( const, CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB, DATA_NETWORK) from homeassistant.setup import setup_component +from tests.common import mock_registry import pytest @@ -468,6 +469,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() self.hass.start() + self.registry = mock_registry(self.hass) setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() @@ -487,7 +489,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): const.DISC_OPTIONAL: True, }}} self.primary = MockValue( - command_class='mock_primary_class', node=self.node) + command_class='mock_primary_class', node=self.node, value_id=1000) self.secondary = MockValue( command_class='mock_secondary_class', node=self.node) self.duplicate_secondary = MockValue( @@ -521,6 +523,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) assert values.primary is self.primary @@ -592,6 +595,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) self.hass.block_till_done() @@ -630,6 +634,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -639,7 +644,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): - """Test ignore workaround.""" + """Test component workaround.""" discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform @@ -666,6 +671,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -697,6 +703,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -720,12 +727,42 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() assert not discovery.async_load_platform.called + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_config_ignore_with_registry(self, discovery, get_platform): + """Test ignore config. + + The case when the device is in entity registry. + """ + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + self.device_config = {'mock_component.registry_id': { + zwave.CONF_IGNORED: True + }} + self.registry.async_get_or_create( + 'mock_component', zwave.DOMAIN, '567-1000', + suggested_object_id='registry_id') + zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + registry=self.registry + ) + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') def test_entity_platform_ignore(self, discovery, get_platform): @@ -743,6 +780,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) self.hass.block_till_done() @@ -770,6 +808,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cb8703d1fe6..492b97f6387 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -180,3 +180,13 @@ test.disabled_hass: assert entry_disabled_hass.disabled_by == entity_registry.DISABLED_HASS assert entry_disabled_user.disabled assert entry_disabled_user.disabled_by == entity_registry.DISABLED_USER + + +@asyncio.coroutine +def test_async_get_entity_id(registry): + """Test that entity_id is returned.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234' + assert registry.async_get_entity_id( + 'light', 'hue', '1234') == 'light.hue_1234' + assert registry.async_get_entity_id('light', 'hue', '123') is None diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 67bfb590c3f..59d97ddb621 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -178,6 +178,7 @@ class MockValue(MagicMock): MockValue._mock_value_id += 1 value_id = MockValue._mock_value_id self.value_id = value_id + self.object_id = value_id for attr_name in kwargs: setattr(self, attr_name, kwargs[attr_name]) From ea2c0736123f3445c7183cf7f0a097da6374d302 Mon Sep 17 00:00:00 2001 From: Krasimir Chariyski Date: Sun, 13 May 2018 00:46:00 +0300 Subject: [PATCH 696/924] Add Bulgarian to Google TTS (#14422) --- homeassistant/components/tts/google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index bf03ec1adad..cb05795c445 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -29,7 +29,7 @@ SUPPORT_LANGUAGES = [ 'hr', 'cs', 'da', 'nl', 'en', 'en-au', 'en-uk', 'en-us', 'eo', 'fi', 'fr', 'de', 'el', 'hi', 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', 'mk', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru', 'sr', 'sk', 'es', 'es-es', - 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', + 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', 'bg-BG' ] DEFAULT_LANG = 'en' From 843789528ed800268362782782d7c857bc68c6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 11:06:15 +0200 Subject: [PATCH 697/924] Remove extra quotes from docstrings (#14431) --- homeassistant/components/folder_watcher.py | 2 +- homeassistant/components/sensor/sigfox.py | 2 +- homeassistant/components/sensor/simulated.py | 2 +- tests/components/automation/test_event.py | 2 +- .../automation/test_numeric_state.py | 54 +++++++++---------- tests/components/binary_sensor/test_nx584.py | 2 +- .../components/binary_sensor/test_template.py | 16 +++--- tests/components/camera/test_uvc.py | 26 ++++----- .../device_tracker/test_unifi_direct.py | 6 +-- .../components/device_tracker/test_xiaomi.py | 6 +-- tests/components/fan/test_mqtt.py | 2 +- tests/components/light/test_template.py | 2 +- tests/components/notify/test_demo.py | 2 +- tests/components/notify/test_file.py | 2 +- tests/components/notify/test_group.py | 2 +- tests/components/notify/test_smtp.py | 2 +- tests/components/sensor/test_template.py | 4 +- tests/components/switch/test_mqtt.py | 2 +- tests/components/switch/test_template.py | 2 +- tests/components/test_mqtt_eventstream.py | 16 +++--- tests/components/test_mqtt_statestream.py | 22 ++++---- 21 files changed, 88 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py index 44110647632..098b34ac948 100644 --- a/homeassistant/components/folder_watcher.py +++ b/homeassistant/components/folder_watcher.py @@ -43,7 +43,7 @@ def setup(hass, config): def create_event_handler(patterns, hass): - """"Return the Watchdog EventHandler object.""" + """Return the Watchdog EventHandler object.""" from watchdog.events import PatternMatchingEventHandler class EventHandler(PatternMatchingEventHandler): diff --git a/homeassistant/components/sensor/sigfox.py b/homeassistant/components/sensor/sigfox.py index ef47132eefc..da8f3fcc639 100644 --- a/homeassistant/components/sensor/sigfox.py +++ b/homeassistant/components/sensor/sigfox.py @@ -66,7 +66,7 @@ class SigfoxAPI(object): self._devices = self.get_devices(device_types) def check_credentials(self): - """"Check API credentials are valid.""" + """Check API credentials are valid.""" url = urljoin(API_URL, 'devicetypes') response = requests.get(url, auth=self._auth, timeout=10) if response.status_code != 200: diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index 7091146e3ac..ae2d4939eab 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -87,7 +87,7 @@ class SimulatedSensor(Entity): self._state = None def time_delta(self): - """"Return the time delta.""" + """Return the time delta.""" dt0 = self._start_time dt1 = dt_util.utcnow() return dt1 - dt0 diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index df9ab69e7e8..aea6e517e38 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -26,7 +26,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.services.register('test', 'automation', record_call) def tearDown(self): - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_if_fires_on_event(self): diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 63ca4b5cd1a..de453675a57 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -35,7 +35,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.stop() def test_if_fires_on_entity_change_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -62,7 +62,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_over_to_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -85,7 +85,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entities_change_over_to_below(self): - """"Test the firing with changed entities.""" + """Test the firing with changed entities.""" self.hass.states.set('test.entity_1', 11) self.hass.states.set('test.entity_2', 11) self.hass.block_till_done() @@ -115,7 +115,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(2, len(self.calls)) def test_if_not_fires_on_entity_change_below_to_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -148,7 +148,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_below_fires_on_entity_change_to_equal(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -171,7 +171,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_initial_entity_below(self): - """"Test the firing when starting with a match.""" + """Test the firing when starting with a match.""" self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -194,7 +194,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_initial_entity_above(self): - """"Test the firing when starting with a match.""" + """Test the firing when starting with a match.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -217,7 +217,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -236,7 +236,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_below_to_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -260,7 +260,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_entity_change_above_to_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -289,7 +289,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_above_fires_on_entity_change_to_equal(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -313,7 +313,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_below_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_below_above_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -353,7 +353,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_over_to_below_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -377,7 +377,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_over_to_below_above_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -401,7 +401,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_if_entity_not_match(self): - """"Test if not fired with non matching entity.""" + """Test if not fired with non matching entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -420,7 +420,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_below_with_attribute(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -439,7 +439,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_entity_change_not_below_with_attribute(self): - """"Test attributes.""" + """Test attributes.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -458,7 +458,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_attribute_change_with_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -478,7 +478,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_attribute_change_with_attribute_not_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -498,7 +498,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_on_entity_change_with_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -518,7 +518,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_on_entity_change_with_not_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -538,7 +538,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -559,7 +559,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_template_list(self): - """"Test template list.""" + """Test template list.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -581,7 +581,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_template_string(self): - """"Test template string.""" + """Test template string.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -614,7 +614,7 @@ class TestAutomationNumericState(unittest.TestCase): self.calls[0].data['some']) def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self): - """"Test if not fired changed attributes.""" + """Test if not fired changed attributes.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -635,7 +635,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_action(self): - """"Test if action.""" + """Test if action.""" entity_id = 'domain.test_entity' assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index d94d887c641..4d1d85d30fb 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -113,7 +113,7 @@ class TestNX584SensorSetup(unittest.TestCase): self._test_assert_graceful_fail({}) def test_setup_version_too_old(self): - """"Test if version is too old.""" + """Test if version is too old.""" nx584_client.Client.return_value.get_version.return_value = '1.0' self._test_assert_graceful_fail({}) diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 18c095f4bc1..62623a04f3c 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -31,7 +31,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass.stop() def test_setup(self): - """"Test the setup.""" + """Test the setup.""" config = { 'binary_sensor': { 'platform': 'template', @@ -49,7 +49,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass, 'binary_sensor', config) def test_setup_no_sensors(self): - """"Test setup with no sensors.""" + """Test setup with no sensors.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -58,7 +58,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_device(self): - """"Test the setup with invalid devices.""" + """Test the setup with invalid devices.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -70,7 +70,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_device_class(self): - """"Test setup with invalid sensor class.""" + """Test setup with invalid sensor class.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -85,7 +85,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_missing_template(self): - """"Test setup with invalid and missing template.""" + """Test setup with invalid and missing template.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -161,7 +161,7 @@ class TestBinarySensorTemplate(unittest.TestCase): assert state.attributes['entity_picture'] == '/local/sensor.png' def test_attributes(self): - """"Test the attributes.""" + """Test the attributes.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', @@ -182,7 +182,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.assertTrue(vs.is_on) def test_event(self): - """"Test the event.""" + """Test the event.""" config = { 'binary_sensor': { 'platform': 'template', @@ -214,7 +214,7 @@ class TestBinarySensorTemplate(unittest.TestCase): @mock.patch('homeassistant.helpers.template.Template.render') def test_update_template_error(self, mock_render): - """"Test the template update error.""" + """Test the template update error.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 40b4fb2d8e2..dabad953bea 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -26,7 +26,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_full_config(self, mock_uvc, mock_remote): - """"Test the setup with full configuration.""" + """Test the setup with full configuration.""" config = { 'platform': 'uvc', 'nvr': 'foo', @@ -41,7 +41,7 @@ class TestUVCSetup(unittest.TestCase): ] def fake_get_camera(uuid): - """"Create a fake camera.""" + """Create a fake camera.""" if uuid == 'id3': return {'model': 'airCam'} else: @@ -65,7 +65,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_partial_config(self, mock_uvc, mock_remote): - """"Test the setup with partial configuration.""" + """Test the setup with partial configuration.""" config = { 'platform': 'uvc', 'nvr': 'foo', @@ -152,7 +152,7 @@ class TestUVC(unittest.TestCase): """Test class for UVC.""" def setup_method(self, method): - """"Setup the mock camera.""" + """Setup the mock camera.""" self.nvr = mock.MagicMock() self.uuid = 'uuid' self.name = 'name' @@ -171,7 +171,7 @@ class TestUVC(unittest.TestCase): self.nvr.server_version = (3, 2, 0) def test_properties(self): - """"Test the properties.""" + """Test the properties.""" self.assertEqual(self.name, self.uvc.name) self.assertTrue(self.uvc.is_recording) self.assertEqual('Ubiquiti', self.uvc.brand) @@ -180,7 +180,7 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login(self, mock_camera, mock_store): - """"Test the login.""" + """Test the login.""" self.uvc._login() self.assertEqual(mock_camera.call_count, 1) self.assertEqual( @@ -205,7 +205,7 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store): - """"Test the login tries.""" + """Test the login tries.""" responses = [0] def fake_login(*a): @@ -234,13 +234,13 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_fails_both_properly(self, mock_camera, mock_store): - """"Test if login fails properly.""" + """Test if login fails properly.""" mock_camera.return_value.login.side_effect = socket.error self.assertEqual(None, self.uvc._login()) self.assertEqual(None, self.uvc._connect_addr) def test_camera_image_tries_login_bails_on_failure(self): - """"Test retrieving failure.""" + """Test retrieving failure.""" with mock.patch.object(self.uvc, '_login') as mock_login: mock_login.return_value = False self.assertEqual(None, self.uvc.camera_image()) @@ -248,19 +248,19 @@ class TestUVC(unittest.TestCase): self.assertEqual(mock_login.call_args, mock.call()) def test_camera_image_logged_in(self): - """"Test the login state.""" + """Test the login state.""" self.uvc._camera = mock.MagicMock() self.assertEqual(self.uvc._camera.get_snapshot.return_value, self.uvc.camera_image()) def test_camera_image_error(self): - """"Test the camera image error.""" + """Test the camera image error.""" self.uvc._camera = mock.MagicMock() self.uvc._camera.get_snapshot.side_effect = camera.CameraConnectError self.assertEqual(None, self.uvc.camera_image()) def test_camera_image_reauths(self): - """"Test the re-authentication.""" + """Test the re-authentication.""" responses = [0] def fake_snapshot(): @@ -281,7 +281,7 @@ class TestUVC(unittest.TestCase): self.assertEqual([], responses) def test_camera_image_reauths_only_once(self): - """"Test if the re-authentication only happens once.""" + """Test if the re-authentication only happens once.""" self.uvc._camera = mock.MagicMock() self.uvc._camera.get_snapshot.side_effect = camera.CameraAuthError with mock.patch.object(self.uvc, '_login') as mock_login: diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py index 8bc3a60146c..ccfa59404a1 100644 --- a/tests/components/device_tracker/test_unifi_direct.py +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -71,7 +71,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh') def test_get_device_name(self, mock_ssh): - """"Testing MAC matching.""" + """Testing MAC matching.""" conf_dict = { DOMAIN: { CONF_PLATFORM: 'unifi_direct', @@ -95,7 +95,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh.logout') @patch('pexpect.pxssh.pxssh.login') def test_failed_to_log_in(self, mock_login, mock_logout): - """"Testing exception at login results in False.""" + """Testing exception at login results in False.""" from pexpect import exceptions conf_dict = { @@ -120,7 +120,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh.sendline') def test_to_get_update(self, mock_sendline, mock_prompt, mock_login, mock_logout): - """"Testing exception in get_update matching.""" + """Testing exception in get_update matching.""" conf_dict = { DOMAIN: { CONF_PLATFORM: 'unifi_direct', diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index 19f25b514db..bdd921f395f 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -210,7 +210,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_invalid_credential(self, mock_get, mock_post): - """"Testing invalid credential handling.""" + """Testing invalid credential handling.""" config = { DOMAIN: xiaomi.PLATFORM_SCHEMA({ CONF_PLATFORM: xiaomi.DOMAIN, @@ -224,7 +224,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_valid_credential(self, mock_get, mock_post): - """"Testing valid refresh.""" + """Testing valid refresh.""" config = { DOMAIN: xiaomi.PLATFORM_SCHEMA({ CONF_PLATFORM: xiaomi.DOMAIN, @@ -244,7 +244,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_token_timed_out(self, mock_get, mock_post): - """"Testing refresh with a timed out token. + """Testing refresh with a timed out token. New token is requested and list is downloaded a second time. """ diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index ec68492ed1e..9060d7b9986 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -18,7 +18,7 @@ class TestMqttFan(unittest.TestCase): self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_default_availability_payload(self): diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index 2d45ad1bf94..962760672f1 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -36,7 +36,7 @@ class TestTemplateLight: self.hass.stop() def test_template_state_text(self): - """"Test the state text of a template.""" + """Test the state text of a template.""" with assert_setup_component(1, 'light'): assert setup.setup_component(self.hass, 'light', { 'light': { diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 5bd3270b922..71b472afe74 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -33,7 +33,7 @@ class TestNotifyDemo(unittest.TestCase): self.hass.bus.listen(demo.EVENT_NOTIFY, record_event) def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() def _setup_notify(self): diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index c5064fca851..d59bbe4d720 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -20,7 +20,7 @@ class TestNotifyFile(unittest.TestCase): self.hass = get_test_home_assistant() def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() def test_bad_config(self): diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index c96a49d7cb3..a847de51142 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -53,7 +53,7 @@ class TestNotifyGroup(unittest.TestCase): assert self.service is not None def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_send_message_with_data(self): diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 127eecae2b7..29e34974c6c 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -27,7 +27,7 @@ class TestNotifySmtp(unittest.TestCase): 'HomeAssistant', 0) def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() @patch('email.utils.make_msgid', return_value='') diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index f8d912f24dd..6861d3a5070 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -269,7 +269,7 @@ class TestTemplateSensor: assert self.hass.states.all() == [] def test_setup_invalid_device_class(self): - """"Test setup with invalid device_class.""" + """Test setup with invalid device_class.""" with assert_setup_component(0): assert setup_component(self.hass, 'sensor', { 'sensor': { @@ -284,7 +284,7 @@ class TestTemplateSensor: }) def test_setup_valid_device_class(self): - """"Test setup with valid device_class.""" + """Test setup with valid device_class.""" with assert_setup_component(1): assert setup_component(self.hass, 'sensor', { 'sensor': { diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 24db0540012..31f9a729c53 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -20,7 +20,7 @@ class TestSwitchMQTT(unittest.TestCase): self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_controlling_state_via_topic(self): diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 7456ae11a0d..8f7bbda8e98 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -32,7 +32,7 @@ class TestTemplateSwitch: self.hass.stop() def test_template_state_text(self): - """"Test the state text of a template.""" + """Test the state text of a template.""" with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py index f4fc3e89ee0..48bc04d46ed 100644 --- a/tests/components/test_mqtt_eventstream.py +++ b/tests/components/test_mqtt_eventstream.py @@ -44,11 +44,11 @@ class TestMqttEventStream(object): eventstream.DOMAIN: config}) def test_setup_succeeds(self): - """"Test the success of the setup.""" + """Test the success of the setup.""" assert self.add_eventstream() def test_setup_with_pub(self): - """"Test the setup with subscription.""" + """Test the setup with subscription.""" # Should start off with no listeners for all events assert self.hass.bus.listeners.get('*') is None @@ -60,7 +60,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_subscribe') def test_subscribe(self, mock_sub): - """"Test the subscription.""" + """Test the subscription.""" sub_topic = 'foo' assert self.add_eventstream(sub_topic=sub_topic) self.hass.block_till_done() @@ -71,7 +71,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if event changed.""" + """Test the sending of a new message if event changed.""" now = dt_util.as_utc(dt_util.now()) e_id = 'fake.entity' pub_topic = 'bar' @@ -113,7 +113,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_time_event_does_not_send_message(self, mock_pub): - """"Test the sending of a new message if time event.""" + """Test the sending of a new message if time event.""" assert self.add_eventstream(pub_topic='bar') self.hass.block_till_done() @@ -125,7 +125,7 @@ class TestMqttEventStream(object): assert not mock_pub.called def test_receiving_remote_event_fires_hass_event(self): - """"Test the receiving of the remotely fired event.""" + """Test the receiving of the remotely fired event.""" sub_topic = 'foo' assert self.add_eventstream(sub_topic=sub_topic) self.hass.block_till_done() @@ -150,7 +150,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_ignored_event_doesnt_send_over_stream(self, mock_pub): - """"Test the ignoring of sending events if defined.""" + """Test the ignoring of sending events if defined.""" assert self.add_eventstream(pub_topic='bar', ignore_event=['state_changed']) self.hass.block_till_done() @@ -177,7 +177,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_wrong_ignored_event_sends_over_stream(self, mock_pub): - """"Test the ignoring of sending events if defined.""" + """Test the ignoring of sending events if defined.""" assert self.add_eventstream(pub_topic='bar', ignore_event=['statee_changed']) self.hass.block_till_done() diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index e120c3a7dd2..2ed2f4487ea 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -47,17 +47,17 @@ class TestMqttStateStream(object): assert self.add_statestream() is False def test_setup_succeeds_without_attributes(self): - """"Test the success of the setup with a valid base_topic.""" + """Test the success of the setup with a valid base_topic.""" assert self.add_statestream(base_topic='pub') def test_setup_succeeds_with_attributes(self): - """"Test setup with a valid base_topic and publish_attributes.""" + """Test setup with a valid base_topic and publish_attributes.""" assert self.add_statestream(base_topic='pub', publish_attributes=True) @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if event changed.""" + """Test the sending of a new message if event changed.""" e_id = 'fake.entity' base_topic = 'pub' @@ -84,7 +84,7 @@ class TestMqttStateStream(object): self, mock_utcnow, mock_pub): - """"Test the sending of a message and timestamps if event changed.""" + """Test the sending of a message and timestamps if event changed.""" e_id = 'another.entity' base_topic = 'pub' @@ -118,7 +118,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_attr_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if attribute changed.""" + """Test the sending of a new message if attribute changed.""" e_id = 'fake.entity' base_topic = 'pub' @@ -160,7 +160,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_domain(self, mock_utcnow, mock_pub): - """"Test that filtering on included domain works as expected.""" + """Test that filtering on included domain works as expected.""" base_topic = 'pub' incl = { @@ -198,7 +198,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_entity(self, mock_utcnow, mock_pub): - """"Test that filtering on included entity works as expected.""" + """Test that filtering on included entity works as expected.""" base_topic = 'pub' incl = { @@ -236,7 +236,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_domain(self, mock_utcnow, mock_pub): - """"Test that filtering on excluded domain works as expected.""" + """Test that filtering on excluded domain works as expected.""" base_topic = 'pub' incl = {} @@ -274,7 +274,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_entity(self, mock_utcnow, mock_pub): - """"Test that filtering on excluded entity works as expected.""" + """Test that filtering on excluded entity works as expected.""" base_topic = 'pub' incl = {} @@ -313,7 +313,7 @@ class TestMqttStateStream(object): @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_domain_include_entity( self, mock_utcnow, mock_pub): - """"Test filtering with excluded domain and included entity.""" + """Test filtering with excluded domain and included entity.""" base_topic = 'pub' incl = { @@ -354,7 +354,7 @@ class TestMqttStateStream(object): @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_domain_exclude_entity( self, mock_utcnow, mock_pub): - """"Test filtering with included domain and excluded entity.""" + """Test filtering with included domain and excluded entity.""" base_topic = 'pub' incl = { From 234bf1f0ead4edeb2b522e40a72ec92cdc409c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 12:09:28 +0200 Subject: [PATCH 698/924] Spelling, grammar etc fixes (#14432) * Spelling, grammar etc fixes * s/an api data/data of an api/ --- homeassistant/components/bmw_connected_drive/services.yaml | 4 ++-- homeassistant/components/hassio/handler.py | 2 +- homeassistant/components/homekit/util.py | 2 +- homeassistant/components/insteon_plm/services.yaml | 2 +- homeassistant/components/media_player/yamaha.py | 2 +- homeassistant/components/sensor/buienradar.py | 2 +- homeassistant/components/sensor/hive.py | 2 +- homeassistant/components/sensor/statistics.py | 2 +- homeassistant/components/switch/tahoma.py | 4 ++-- tests/components/homekit/test_type_security_systems.py | 2 +- tests/components/media_player/test_blackbird.py | 2 +- tests/components/sensor/test_sigfox.py | 4 ++-- tests/components/test_folder_watcher.py | 2 +- tests/components/test_prometheus.py | 2 +- tests/test_core.py | 6 +++--- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 3c180271919..b9605429a8e 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -27,7 +27,7 @@ activate_air_conditioning: description: > Start the air conditioning of the vehicle. What exactly is started here depends on the type of vehicle. It might range from just ventilation over - auxilary heating to real air conditioning. The vehicle is identified via + auxiliary heating to real air conditioning. The vehicle is identified via the vin (see below). fields: vin: @@ -39,4 +39,4 @@ update_state: description: > Fetch the last state of the vehicles of all your accounts from the BMW server. This does *not* trigger an update from the vehicle, it just gets - the data from the BMW servers. This service does not require any attributes. \ No newline at end of file + the data from the BMW servers. This service does not require any attributes. diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index a954aaccbd4..c3caf40ba62 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -33,7 +33,7 @@ def _api_bool(funct): def _api_data(funct): - """Return a api data.""" + """Return data of an api.""" @asyncio.coroutine def _wrapper(*argv, **kwargs): """Wrap function.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index c201d884a75..5ddef534202 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -21,7 +21,7 @@ def validate_entity_config(values): params = {} if not isinstance(config, dict): raise vol.Invalid('The configuration for "{}" must be ' - ' an dictionary.'.format(entity)) + ' a dictionary.'.format(entity)) for key in (CONF_NAME, ): value = config.get(key, -1) diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml index a0e250fef1f..9ea53c10fbf 100644 --- a/homeassistant/components/insteon_plm/services.yaml +++ b/homeassistant/components/insteon_plm/services.yaml @@ -14,7 +14,7 @@ delete_all_link: description: All-Link group number. example: 1 load_all_link_database: - description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. + description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records. fields: entity_id: description: Name of the device to print diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 5b8ac2ad236..bb7942a2545 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -222,7 +222,7 @@ class YamahaDevice(MediaPlayerDevice): @property def zone_id(self): - """Return an zone_id to ensure 1 media player per zone.""" + """Return a zone_id to ensure 1 media player per zone.""" return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) @property diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 6eb67f7cbd8..590d5a8f1ce 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -197,7 +197,7 @@ class BrSensor(Entity): def uid(self, coordinates): """Generate a unique id using coordinates and sensor type.""" - # The combination of the location, name an sensor type is unique + # The combination of the location, name and sensor type is unique return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], self.type) diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index 8f8ce2d1681..82816c83404 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -70,7 +70,7 @@ class HiveSensorEntity(Entity): return DEVICETYPE_ICONS.get(self.device_type) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" if self.session.core.update_data(self.node_id): for entity in self.session.entities: entity.handle_update(self.data_updatesource) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 7b2ae537d4b..a77509c18d4 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -156,7 +156,7 @@ class StatisticsSensor(Entity): ATTR_CHANGE: self.change, ATTR_AVERAGE_CHANGE: self.average_change, } - # Only return min/max age if we have a age span + # Only return min/max age if we have an age span if self._max_age: state.update({ ATTR_MAX_AGE: self.max_age, diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py index 339a0c39386..aa3554a494c 100644 --- a/homeassistant/components/switch/tahoma.py +++ b/homeassistant/components/switch/tahoma.py @@ -1,7 +1,7 @@ """ Support for Tahoma Switch - those are push buttons for garage door etc. -Those buttons are implemented as switchs that are never on. They only +Those buttons are implemented as switches that are never on. They only receive the turn_on action, perform the relay click, and stay in OFF state For more details about this platform, please refer to the documentation at @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Tahoma switchs.""" + """Set up Tahoma switches.""" controller = hass.data[TAHOMA_DOMAIN]['controller'] devices = [] for switch in hass.data[TAHOMA_DOMAIN]['devices']['switch']: diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index da5dac2d81b..577d2f2175d 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -96,7 +96,7 @@ async def test_switch_set_state(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) async def test_no_alarm_code(hass, config): - """Test accessory if security_system doesn't require a alarm_code.""" + """Test accessory if security_system doesn't require an alarm_code.""" entity_id = 'alarm_control_panel.test' hass.states.async_set(entity_id, None) diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index eea6295b79e..7c85775949c 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player.blackbird import ( class AttrDict(dict): - """Helper clas for mocking attributes.""" + """Helper class for mocking attributes.""" def __setattr__(self, name, value): """Set attribute.""" diff --git a/tests/components/sensor/test_sigfox.py b/tests/components/sensor/test_sigfox.py index dcdeef56b98..569fab584ad 100644 --- a/tests/components/sensor/test_sigfox.py +++ b/tests/components/sensor/test_sigfox.py @@ -38,7 +38,7 @@ class TestSigfoxSensor(unittest.TestCase): self.hass.stop() def test_invalid_credentials(self): - """Test for a invalid credentials.""" + """Test for invalid credentials.""" with requests_mock.Mocker() as mock_req: url = re.compile(API_URL + 'devicetypes') mock_req.get(url, text='{}', status_code=401) @@ -47,7 +47,7 @@ class TestSigfoxSensor(unittest.TestCase): assert len(self.hass.states.entity_ids()) == 0 def test_valid_credentials(self): - """Test for a valid credentials.""" + """Test for valid credentials.""" with requests_mock.Mocker() as mock_req: url1 = re.compile(API_URL + 'devicetypes') mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py index 16ec7a58a02..b5ac9cca9d9 100644 --- a/tests/components/test_folder_watcher.py +++ b/tests/components/test_folder_watcher.py @@ -8,7 +8,7 @@ from tests.common import MockDependency async def test_invalid_path_setup(hass): - """Test that a invalid path is not setup.""" + """Test that an invalid path is not setup.""" assert not await async_setup_component( hass, folder_watcher.DOMAIN, { folder_watcher.DOMAIN: { diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index 6cc0e4fcada..e336a28eb03 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -8,7 +8,7 @@ import homeassistant.components.prometheus as prometheus @pytest.fixture def prometheus_client(loop, hass, aiohttp_client): - """Initialize a aiohttp_client with Prometheus component.""" + """Initialize an aiohttp_client with Prometheus component.""" assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, diff --git a/tests/test_core.py b/tests/test_core.py index 1fcd9416f36..4abce180093 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -375,7 +375,7 @@ class TestEventBus(unittest.TestCase): self.assertEqual(1, len(runs)) def test_thread_event_listener(self): - """Test a event listener listeners.""" + """Test thread event listener.""" thread_calls = [] def thread_listener(event): @@ -387,7 +387,7 @@ class TestEventBus(unittest.TestCase): assert len(thread_calls) == 1 def test_callback_event_listener(self): - """Test a event listener listeners.""" + """Test callback event listener.""" callback_calls = [] @ha.callback @@ -400,7 +400,7 @@ class TestEventBus(unittest.TestCase): assert len(callback_calls) == 1 def test_coroutine_event_listener(self): - """Test a event listener listeners.""" + """Test coroutine event listener.""" coroutine_calls = [] @asyncio.coroutine From 4d63baf705750de3a17bec4429db47338d2b5445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 12:11:55 +0200 Subject: [PATCH 699/924] Invoke pytest instead of py.test per upstream recommendation, #dropthedot (#14434) http://blog.pytest.org/2016/whats-new-in-pytest-30/ https://twitter.com/hashtag/dropthedot --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d4bea81a2f5..8b034346475 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ setenv = whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = - py.test --timeout=9 --duration=10 --cov --cov-report= {posargs} + pytest --timeout=9 --duration=10 --cov --cov-report= {posargs} deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt From e5d714ef528bff1a874cd17ecc3d1e1fd3043724 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 May 2018 14:41:42 +0200 Subject: [PATCH 700/924] Fix fan service description (#14423) --- homeassistant/components/fan/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index a74f67b83fb..039cc33f748 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -51,8 +51,8 @@ set_direction: description: Name(s) of the entities to toggle example: 'fan.living_room' direction: - description: The direction to rotate - example: 'left' + description: The direction to rotate. Either 'forward' or 'reverse' + example: 'forward' dyson_set_night_mode: description: Set the fan in night mode. From 146a9492ecbf1bbf4184a9945447182b2847bd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 17:56:42 +0200 Subject: [PATCH 701/924] Clean up some Python 3.4 remnants (#14433) --- homeassistant/components/system_log/__init__.py | 6 +----- tests/components/camera/test_local_file.py | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 5994184d815..2a2a19aa2f5 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -126,11 +126,7 @@ class LogErrorHandler(logging.Handler): if record.levelno >= logging.WARN: stack = [] if not record.exc_info: - try: - stack = [f for f, _, _, _ in traceback.extract_stack()] - except ValueError: - # On Python 3.4 under py.test getting the stack might fail. - pass + stack = [f for f, _, _, _ in traceback.extract_stack()] entry = self._create_entry(record, stack) self.records.appendleft(entry) diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 40517ea1298..0a57512aabd 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -2,10 +2,6 @@ import asyncio from unittest import mock -# Using third party package because of a bug reading binary data in Python 3.4 -# https://bugs.python.org/issue23004 -from mock_open import MockOpen - from homeassistant.components.camera import DOMAIN from homeassistant.components.camera.local_file import ( SERVICE_UPDATE_FILE_PATH) @@ -30,7 +26,7 @@ def test_loading_file(hass, aiohttp_client): client = yield from aiohttp_client(hass.http.app) - m_open = MockOpen(read_data=b'hello') + m_open = mock.mock_open(read_data=b'hello') with mock.patch( 'homeassistant.components.camera.local_file.open', m_open, create=True @@ -90,7 +86,7 @@ def test_camera_content_type(hass, aiohttp_client): client = yield from aiohttp_client(hass.http.app) image = 'hello' - m_open = MockOpen(read_data=image.encode()) + m_open = mock.mock_open(read_data=image.encode()) with mock.patch('homeassistant.components.camera.local_file.open', m_open, create=True): resp_1 = yield from client.get('/api/camera_proxy/camera.test_jpg') From b904a4e7709dec3618ff6077979cfe62c5c2c5e8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 13 May 2018 17:57:52 +0200 Subject: [PATCH 702/924] Remove universal wheel setting (#14445) * Home assistant should not build a universal wheel since we don't support Python 2. --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index d6dfdfe0ea5..8b17da455dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal = 1 - [tool:pytest] testpaths = tests norecursedirs = .git testing_config From 3ec56d55c5f56a857f387047a38de31e9988288d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 17:58:18 +0200 Subject: [PATCH 703/924] Upgrade requests_mock to 1.5 (#14444) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9dcccd0d1da..0a4a0bcb5b0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,4 +14,4 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout>=1.2.1 pytest==3.4.2 -requests_mock==1.4 +requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2157389a16..b91a6500b07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout>=1.2.1 pytest==3.4.2 -requests_mock==1.4 +requests_mock==1.5 # homeassistant.components.homekit From e0bc894cbba67282a85de8400fe813d41342f3af Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 17:58:57 +0200 Subject: [PATCH 704/924] Upgrade pyota to 2.0.5 (#14442) * Use constants * Upgrade pyota to 2.0.5 --- homeassistant/components/iota.py | 2 +- homeassistant/components/sensor/iota.py | 26 ++++++++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py index 442be6e22e7..ada70f8a9eb 100644 --- a/homeassistant/components/iota.py +++ b/homeassistant/components/iota.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyota==2.0.4'] +REQUIREMENTS = ['pyota==2.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/iota.py b/homeassistant/components/sensor/iota.py index c973fa83148..2e3e58a18f3 100644 --- a/homeassistant/components/sensor/iota.py +++ b/homeassistant/components/sensor/iota.py @@ -7,10 +7,18 @@ https://home-assistant.io/components/iota import logging from datetime import timedelta -from homeassistant.components.iota import IotaDevice +from homeassistant.components.iota import IotaDevice, CONF_WALLETS +from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) +ATTR_TESTNET = 'testnet' +ATTR_URL = 'url' + +CONF_IRI = 'iri' +CONF_SEED = 'seed' +CONF_TESTNET = 'testnet' + DEPENDENCIES = ['iota'] SCAN_INTERVAL = timedelta(minutes=3) @@ -21,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Add sensors for wallet balance iota_config = discovery_info sensors = [IotaBalanceSensor(wallet, iota_config) - for wallet in iota_config['wallets']] + for wallet in iota_config[CONF_WALLETS]] # Add sensor for node information sensors.append(IotaNodeSensor(iota_config=iota_config)) @@ -34,10 +42,9 @@ class IotaBalanceSensor(IotaDevice): def __init__(self, wallet_config, iota_config): """Initialize the sensor.""" - super().__init__(name=wallet_config['name'], - seed=wallet_config['seed'], - iri=iota_config['iri'], - is_testnet=iota_config['testnet']) + super().__init__( + name=wallet_config[CONF_NAME], seed=wallet_config[CONF_SEED], + iri=iota_config[CONF_IRI], is_testnet=iota_config[CONF_TESTNET]) self._state = None @property @@ -65,10 +72,11 @@ class IotaNodeSensor(IotaDevice): def __init__(self, iota_config): """Initialize the sensor.""" - super().__init__(name='Node Info', seed=None, iri=iota_config['iri'], - is_testnet=iota_config['testnet']) + super().__init__( + name='Node Info', seed=None, iri=iota_config[CONF_IRI], + is_testnet=iota_config[CONF_TESTNET]) self._state = None - self._attr = {'url': self.iri, 'testnet': self.is_testnet} + self._attr = {ATTR_URL: self.iri, ATTR_TESTNET: self.is_testnet} @property def name(self): diff --git a/requirements_all.txt b/requirements_all.txt index d81dcc8280d..281df00312e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -891,7 +891,7 @@ pynut2==2.1.2 pynx584==0.4 # homeassistant.components.iota -pyota==2.0.4 +pyota==2.0.5 # homeassistant.components.sensor.otp pyotp==2.2.6 From a5bff4cd8dd9e3a3f4659f2f570b9f89a6e6a222 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 17:59:25 +0200 Subject: [PATCH 705/924] Upgrade python-telegram-bot to 10.1.0 (#14441) --- homeassistant/components/telegram_bot/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index af0fe5bd572..b9329a46b72 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.0.2'] +REQUIREMENTS = ['python-telegram-bot==10.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 281df00312e..d2b77726729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1030,7 +1030,7 @@ python-synology==0.1.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.0.2 +python-telegram-bot==10.1.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From a750f8444ef355acac12dbad11e645fbb2c8e059 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 18:00:08 +0200 Subject: [PATCH 706/924] Upgrade Sphinx to 1.7.4 (#14439) --- requirements_docs.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index bb0d30462ce..5ef38e1537e 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.1 -sphinx-autodoc-typehints==1.2.5 +Sphinx==1.7.4 +sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 From cb709931e469ab3b1d276055fbe9c1af1190887e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 18:00:37 +0200 Subject: [PATCH 707/924] Upgrade youtube_dl to 2018.05.09 (#14438) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index fe6ebe8e618..89cc296111b 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.04.25'] +REQUIREMENTS = ['youtube_dl==2018.05.09'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d2b77726729..ad64e4ae736 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1367,7 +1367,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.25 +youtube_dl==2018.05.09 # homeassistant.components.light.zengge zengge==0.2 From 391e3196ea439bf6ef6503bed9982d8871e72ea4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 18:01:10 +0200 Subject: [PATCH 708/924] Upgrade distro to 1.3.0 (#14436) --- homeassistant/components/updater.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 9ccf280ed04..0cb22bd98dc 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['distro==1.2.0'] +REQUIREMENTS = ['distro==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ad64e4ae736..e293814e661 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -258,7 +258,7 @@ discogs_client==2.2.1 discord.py==0.16.12 # homeassistant.components.updater -distro==1.2.0 +distro==1.3.0 # homeassistant.components.switch.digitalloggers dlipower==0.7.165 From 8ae3caa2928d5b6ae9bb73f4b9fbd288fec5991d Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Sun, 13 May 2018 10:04:21 -0600 Subject: [PATCH 709/924] Add priority and cycles to LaMetric (#14414) * Add priority and cycles to LaMetric Priority can be "info", "warning" (default), or "critical" and cycles is the number of times the message is displayed. If cycles is set to 0 we get a persistent notification that has to be dismissed manually. * Fix for schema and style * Fix for style --- homeassistant/components/notify/lametric.py | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index 895ffd9db10..f6c3e152b0a 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -23,11 +23,16 @@ _LOGGER = logging.getLogger(__name__) CONF_LIFETIME = "lifetime" CONF_CYCLES = "cycles" +CONF_PRIORITY = "priority" + +AVAILABLE_PRIORITIES = ["info", "warning", "critical"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ICON, default="i555"): cv.string, vol.Optional(CONF_LIFETIME, default=10): cv.positive_int, vol.Optional(CONF_CYCLES, default=1): cv.positive_int, + vol.Optional(CONF_PRIORITY, default="warning"): + vol.In(AVAILABLE_PRIORITIES) }) @@ -38,18 +43,20 @@ def get_service(hass, config, discovery_info=None): return LaMetricNotificationService(hlmn, config[CONF_ICON], config[CONF_LIFETIME] * 1000, - config[CONF_CYCLES]) + config[CONF_CYCLES], + config[CONF_PRIORITY]) class LaMetricNotificationService(BaseNotificationService): """Implement the notification service for LaMetric.""" - def __init__(self, hasslametricmanager, icon, lifetime, cycles): + def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): """Initialize the service.""" self.hasslametricmanager = hasslametricmanager self._icon = icon self._lifetime = lifetime self._cycles = cycles + self._priority = priority self._devices = [] # pylint: disable=broad-except @@ -64,6 +71,7 @@ class LaMetricNotificationService(BaseNotificationService): icon = self._icon cycles = self._cycles sound = None + priority = self._priority # Additional data? if data is not None: @@ -78,6 +86,14 @@ class LaMetricNotificationService(BaseNotificationService): except AssertionError: _LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) + if "cycles" in data: + cycles = data['cycles'] + if "priority" in data: + if data['priority'] in AVAILABLE_PRIORITIES: + priority = data['priority'] + else: + _LOGGER.warning("Priority %s invalid, using default %s", + data['priority'], priority) text_frame = SimpleFrame(icon, message) _LOGGER.debug("Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", @@ -100,7 +116,8 @@ class LaMetricNotificationService(BaseNotificationService): if targets is None or dev["name"] in targets: try: lmn.set_device(dev) - lmn.send_notification(model, lifetime=self._lifetime) + lmn.send_notification(model, lifetime=self._lifetime, + priority=priority) _LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) except OSError: From 6b9c65c9ce8523b7f28a4f7b0758c945a75d8668 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 14 May 2018 08:40:25 +0200 Subject: [PATCH 710/924] Allow qwikswitch sensors as part of devices (#14454) --- homeassistant/components/qwikswitch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index f26318fa7a9..63e30a9491e 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -150,8 +150,10 @@ async def async_setup(hass, config): comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []} try: + sensor_ids = [] for sens in sensors: _, _type = SENSORS[sens['type']] + sensor_ids.append(sens['id']) if _type is bool: comps['binary_sensor'].append(sens) continue @@ -192,9 +194,7 @@ async def async_setup(hass, config): 'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket) return - if qspacket[QS_ID] not in qsusb.devices: - # Not a standard device in, component can handle packet - # i.e. sensors + if qspacket[QS_ID] in sensor_ids: _LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket) hass.helpers.dispatcher.async_dispatcher_send( qspacket[QS_ID], qspacket) From c06351f2a9038e4e128ff89c5392f7c2cfc965ae Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 May 2018 08:41:17 +0200 Subject: [PATCH 711/924] Bump requirement to pydeconz v38 (#14452) --- homeassistant/components/deconz/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 47573be6add..bbab4029d7e 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ from .const import ( CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==37'] +REQUIREMENTS = ['pydeconz==38'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index e293814e661..a1158d2af3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==37 +pydeconz==38 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b91a6500b07..d3e2e16dc57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==37 +pydeconz==38 # homeassistant.components.zwave pydispatcher==2.0.5 From fb501282ccb61ad89088619eb12613a4d542a759 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 May 2018 09:13:59 +0200 Subject: [PATCH 712/924] Add SpaceAPI support (#14204) * Add SpaceAPI support * Changes according PR comments * Add tests * Remove print * Minor changes --- homeassistant/components/spaceapi.py | 175 +++++++++++++++++++++++++++ tests/components/test_spaceapi.py | 113 +++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 homeassistant/components/spaceapi.py create mode 100644 tests/components/test_spaceapi.py diff --git a/homeassistant/components/spaceapi.py b/homeassistant/components/spaceapi.py new file mode 100644 index 00000000000..eaf1508071a --- /dev/null +++ b/homeassistant/components/spaceapi.py @@ -0,0 +1,175 @@ +""" +Support for the SpaceAPI. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/spaceapi/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, + ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, CONF_EMAIL, + CONF_ENTITY_ID, CONF_SENSORS, CONF_STATE, CONF_URL) +import homeassistant.core as ha +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_ADDRESS = 'address' +ATTR_API = 'api' +ATTR_CLOSE = 'close' +ATTR_CONTACT = 'contact' +ATTR_ISSUE_REPORT_CHANNELS = 'issue_report_channels' +ATTR_LASTCHANGE = 'lastchange' +ATTR_LOGO = 'logo' +ATTR_NAME = 'name' +ATTR_OPEN = 'open' +ATTR_SENSORS = 'sensors' +ATTR_SPACE = 'space' +ATTR_UNIT = 'unit' +ATTR_URL = 'url' +ATTR_VALUE = 'value' + +CONF_CONTACT = 'contact' +CONF_HUMIDITY = 'humidity' +CONF_ICON_CLOSED = 'icon_closed' +CONF_ICON_OPEN = 'icon_open' +CONF_ICONS = 'icons' +CONF_IRC = 'irc' +CONF_ISSUE_REPORT_CHANNELS = 'issue_report_channels' +CONF_LOCATION = 'location' +CONF_LOGO = 'logo' +CONF_MAILING_LIST = 'mailing_list' +CONF_PHONE = 'phone' +CONF_SPACE = 'space' +CONF_TEMPERATURE = 'temperature' +CONF_TWITTER = 'twitter' + +DATA_SPACEAPI = 'data_spaceapi' +DEPENDENCIES = ['http'] +DOMAIN = 'spaceapi' + +ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER] + +SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE] +SPACEAPI_VERSION = 0.13 + +URL_API_SPACEAPI = '/api/spaceapi' + +LOCATION_SCHEMA = vol.Schema({ + vol.Optional(CONF_ADDRESS): cv.string, +}, required=True) + +CONTACT_SCHEMA = vol.Schema({ + vol.Optional(CONF_EMAIL): cv.string, + vol.Optional(CONF_IRC): cv.string, + vol.Optional(CONF_MAILING_LIST): cv.string, + vol.Optional(CONF_PHONE): cv.string, + vol.Optional(CONF_TWITTER): cv.string, +}, required=False) + +STATE_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Inclusive(CONF_ICON_CLOSED, CONF_ICONS): cv.url, + vol.Inclusive(CONF_ICON_OPEN, CONF_ICONS): cv.url, +}, required=False) + +SENSOR_SCHEMA = vol.Schema( + {vol.In(SENSOR_TYPES): [cv.entity_id]} +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CONTACT): CONTACT_SCHEMA, + vol.Required(CONF_ISSUE_REPORT_CHANNELS): + vol.All(cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)]), + vol.Required(CONF_LOCATION): LOCATION_SCHEMA, + vol.Required(CONF_LOGO): cv.url, + vol.Required(CONF_SPACE): cv.string, + vol.Required(CONF_STATE): STATE_SCHEMA, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Register the SpaceAPI with the HTTP interface.""" + hass.data[DATA_SPACEAPI] = config[DOMAIN] + hass.http.register_view(APISpaceApiView) + + return True + + +class APISpaceApiView(HomeAssistantView): + """View to provide details according to the SpaceAPI.""" + + url = URL_API_SPACEAPI + name = 'api:spaceapi' + + @ha.callback + def get(self, request): + """Get SpaceAPI data.""" + hass = request.app['hass'] + spaceapi = dict(hass.data[DATA_SPACEAPI]) + is_sensors = spaceapi.get('sensors') + + location = { + ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS], + ATTR_LATITUDE: hass.config.latitude, + ATTR_LONGITUDE: hass.config.longitude, + } + + state_entity = spaceapi['state'][ATTR_ENTITY_ID] + space_state = hass.states.get(state_entity) + + if space_state is not None: + state = { + ATTR_OPEN: False if space_state.state == 'off' else True, + ATTR_LASTCHANGE: + dt_util.as_timestamp(space_state.last_updated), + } + else: + state = {ATTR_OPEN: 'null', ATTR_LASTCHANGE: 0} + + try: + state[ATTR_ICON] = { + ATTR_OPEN: spaceapi['state'][CONF_ICON_OPEN], + ATTR_CLOSE: spaceapi['state'][CONF_ICON_CLOSED], + } + except KeyError: + pass + + data = { + ATTR_API: SPACEAPI_VERSION, + ATTR_CONTACT: spaceapi[CONF_CONTACT], + ATTR_ISSUE_REPORT_CHANNELS: spaceapi[CONF_ISSUE_REPORT_CHANNELS], + ATTR_LOCATION: location, + ATTR_LOGO: spaceapi[CONF_LOGO], + ATTR_SPACE: spaceapi[CONF_SPACE], + ATTR_STATE: state, + ATTR_URL: spaceapi[CONF_URL], + } + + if is_sensors is not None: + sensors = {} + for sensor_type in is_sensors: + sensors[sensor_type] = [] + for sensor in spaceapi['sensors'][sensor_type]: + sensor_state = hass.states.get(sensor) + unit = sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT] + value = sensor_state.state + sensor_data = { + ATTR_LOCATION: spaceapi[CONF_SPACE], + ATTR_NAME: sensor_state.name, + ATTR_UNIT: unit, + ATTR_VALUE: value, + } + sensors[sensor_type].append(sensor_data) + data[ATTR_SENSORS] = sensors + + return self.json(data) diff --git a/tests/components/test_spaceapi.py b/tests/components/test_spaceapi.py new file mode 100644 index 00000000000..e7e7d158a31 --- /dev/null +++ b/tests/components/test_spaceapi.py @@ -0,0 +1,113 @@ +"""The tests for the Home Assistant SpaceAPI component.""" +# pylint: disable=protected-access +from unittest.mock import patch + +import pytest +from tests.common import mock_coro + +from homeassistant.components.spaceapi import ( + DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI) +from homeassistant.setup import async_setup_component + +CONFIG = { + DOMAIN: { + 'space': 'Home', + 'logo': 'https://home-assistant.io/logo.png', + 'url': 'https://home-assistant.io', + 'location': {'address': 'In your Home'}, + 'contact': {'email': 'hello@home-assistant.io'}, + 'issue_report_channels': ['email'], + 'state': { + 'entity_id': 'test.test_door', + 'icon_open': 'https://home-assistant.io/open.png', + 'icon_closed': 'https://home-assistant.io/close.png', + }, + 'sensors': { + 'temperature': ['test.temp1', 'test.temp2'], + 'humidity': ['test.hum1'], + } + } +} + +SENSOR_OUTPUT = { + 'temperature': [ + { + 'location': 'Home', + 'name': 'temp1', + 'unit': '°C', + 'value': '25' + }, + { + 'location': 'Home', + 'name': 'temp2', + 'unit': '°C', + 'value': '23' + }, + ], + 'humidity': [ + { + 'location': 'Home', + 'name': 'hum1', + 'unit': '%', + 'value': '88' + }, + ] +} + + +@pytest.fixture +def mock_client(hass, aiohttp_client): + """Start the Home Assistant HTTP component.""" + with patch('homeassistant.components.spaceapi', + return_value=mock_coro(True)): + hass.loop.run_until_complete( + async_setup_component(hass, 'spaceapi', CONFIG)) + + hass.states.async_set('test.temp1', 25, + attributes={'unit_of_measurement': '°C'}) + hass.states.async_set('test.temp2', 23, + attributes={'unit_of_measurement': '°C'}) + hass.states.async_set('test.hum1', 88, + attributes={'unit_of_measurement': '%'}) + + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +async def test_spaceapi_get(hass, mock_client): + """Test response after start-up Home Assistant.""" + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + + assert data['api'] == SPACEAPI_VERSION + assert data['space'] == 'Home' + assert data['contact']['email'] == 'hello@home-assistant.io' + assert data['location']['address'] == 'In your Home' + assert data['location']['latitude'] == 32.87336 + assert data['location']['longitude'] == -117.22743 + assert data['state']['open'] == 'null' + assert data['state']['icon']['open'] == \ + 'https://home-assistant.io/open.png' + assert data['state']['icon']['close'] == \ + 'https://home-assistant.io/close.png' + + +async def test_spaceapi_state_get(hass, mock_client): + """Test response if the state entity was set.""" + hass.states.async_set('test.test_door', True) + + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + assert data['state']['open'] == bool(1) + + +async def test_spaceapi_sensors_get(hass, mock_client): + """Test the response for the sensors.""" + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + assert data['sensors'] == SENSOR_OUTPUT From 954e4796b84082a9654cdaee9006fc22dd60f0e0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 May 2018 13:05:52 +0200 Subject: [PATCH 713/924] Use ATTR_NAME from const.py (#14450) --- .../components/binary_sensor/rfxtrx.py | 5 +- .../components/device_tracker/__init__.py | 3 +- homeassistant/components/group/__init__.py | 4 +- homeassistant/components/hassio/__init__.py | 4 +- .../components/homematic/__init__.py | 11 ++-- .../components/image_processing/__init__.py | 13 ++-- .../image_processing/microsoft_face_detect.py | 8 +-- .../microsoft_face_identify.py | 9 +-- homeassistant/components/lock/wink.py | 4 +- homeassistant/components/logbook.py | 50 ++++++++-------- homeassistant/components/microsoft_face.py | 33 +++++------ homeassistant/components/notify/apns.py | 11 ++-- homeassistant/components/rfxtrx.py | 18 ++---- homeassistant/components/sensor/citybikes.py | 46 +++++++-------- .../components/sensor/coinmarketcap.py | 5 +- .../components/sensor/comed_hourly_pricing.py | 20 +++---- .../components/sensor/linux_battery.py | 5 +- homeassistant/components/sensor/qnap.py | 3 +- homeassistant/components/sensor/rfxtrx.py | 6 +- homeassistant/components/sensor/tado.py | 13 ++-- homeassistant/components/sensor/wsdot.py | 59 +++++++++---------- homeassistant/components/switch/rfxtrx.py | 4 +- homeassistant/components/switch/rpi_pfio.py | 3 +- homeassistant/components/wink/__init__.py | 6 +- homeassistant/const.py | 3 + tests/components/sensor/test_wsdot.py | 11 ++-- 26 files changed, 163 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index 8c026131fd3..6ac604a4f1e 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.rfxtrx import ( ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY) @@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): - DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_OFF_DELAY): vol.Any(cv.time_period, cv.positive_timedelta), diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index e1dd52a28ea..580c0272e46 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, - CONF_ICON, ATTR_ICON) + CONF_ICON, ATTR_ICON, ATTR_NAME) _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,6 @@ ATTR_GPS = 'gps' ATTR_HOST_NAME = 'host_name' ATTR_LOCATION_NAME = 'location_name' ATTR_MAC = 'mac' -ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' ATTR_CONSIDER_HOME = 'consider_home' diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index f70a2d29351..a33e91f3aa9 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, SERVICE_RELOAD) + ATTR_ASSUMED_STATE, SERVICE_RELOAD, ATTR_NAME, ATTR_ICON) from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -35,8 +35,6 @@ ATTR_ADD_ENTITIES = 'add_entities' ATTR_AUTO = 'auto' ATTR_CONTROL = 'control' ATTR_ENTITIES = 'entities' -ATTR_ICON = 'icon' -ATTR_NAME = 'name' ATTR_OBJECT_ID = 'object_id' ATTR_ORDER = 'order' ATTR_VIEW = 'view' diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 87251a2745c..aa24cc61af3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -13,12 +13,13 @@ import voluptuous as vol from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.const import ( - SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) + ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) from homeassistant.core import DOMAIN as HASS_DOMAIN from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow + from .handler import HassIO from .http import HassIOView @@ -47,7 +48,6 @@ ATTR_SNAPSHOT = 'snapshot' ATTR_ADDONS = 'addons' ATTR_FOLDERS = 'folders' ATTR_HOMEASSISTANT = 'homeassistant' -ATTR_NAME = 'name' ATTR_PASSWORD = 'password' SCHEMA_NO_DATA = vol.Schema({}) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 0291cc28fed..aa19875d43a 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -13,17 +13,19 @@ import socket import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, - CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, + CONF_PLATFORM, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass REQUIREMENTS = ['pyhomematic==0.1.42'] -DOMAIN = 'homematic' + _LOGGER = logging.getLogger(__name__) +DOMAIN = 'homematic' + SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) @@ -38,7 +40,6 @@ DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' ATTR_CHANNEL = 'channel' -ATTR_NAME = 'name' ATTR_ADDRESS = 'address' ATTR_VALUE = 'value' ATTR_INTERFACE = 'interface' diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index c6100ff701d..29f26cc84e6 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -10,14 +10,14 @@ import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) + ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,6 @@ ATTR_CONFIDENCE = 'confidence' ATTR_FACES = 'faces' ATTR_GENDER = 'gender' ATTR_GLASSES = 'glasses' -ATTR_NAME = 'name' ATTR_MOTION = 'motion' ATTR_TOTAL_FACES = 'total_faces' @@ -60,7 +59,7 @@ SOURCE_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]), vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE): - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), }) SERVICE_SCAN_SCHEMA = vol.Schema({ @@ -77,7 +76,7 @@ def scan(hass, entity_id=None): @asyncio.coroutine def async_setup(hass, config): - """Set up image processing.""" + """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) yield from component.async_setup(config) diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index cd1e341a218..bda0e1bc550 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -9,12 +9,12 @@ import logging import voluptuous as vol +from homeassistant.components.image_processing import ( + ATTR_AGE, ATTR_GENDER, ATTR_GLASSES, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingFaceEntity) +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER, - ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 32f02e1820e..8984f25cdf2 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -9,12 +9,13 @@ import logging import voluptuous as vol +from homeassistant.components.image_processing import ( + ATTR_CONFIDENCE, CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE, + PLATFORM_SCHEMA, ImageProcessingFaceEntity) +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE +from homeassistant.const import ATTR_NAME from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME, - CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index a5cd18454df..1c42e427a00 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -11,7 +11,8 @@ import voluptuous as vol from homeassistant.components.lock import LockDevice from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['wink'] @@ -28,7 +29,6 @@ SERVICE_ADD_KEY = 'wink_add_new_lock_key_code' ATTR_ENABLED = 'enabled' ATTR_SENSITIVITY = 'sensitivity' ATTR_MODE = 'mode' -ATTR_NAME = 'name' ALARM_SENSITIVITY_MAP = { 'low': 0.2, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 8bab6fe0440..1ea0b586d33 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,44 +4,49 @@ Event parser and human readable log generator. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ -import logging from datetime import timedelta from itertools import groupby +import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components import sun from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, - EVENT_LOGBOOK_ENTRY) -from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN - -DOMAIN = 'logbook' -DEPENDENCIES = ['recorder', 'frontend'] + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE, + CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, + STATE_OFF, STATE_ON) +from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.core import State, callback, split_entity_id +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_EXCLUDE = 'exclude' -CONF_INCLUDE = 'include' -CONF_ENTITIES = 'entities' +ATTR_MESSAGE = 'message' + CONF_DOMAINS = 'domains' +CONF_ENTITIES = 'entities' +CONTINUOUS_DOMAINS = ['proximity', 'sensor'] + +DEPENDENCIES = ['recorder', 'frontend'] + +DOMAIN = 'logbook' + +GROUP_BY_MINUTES = 15 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ CONF_EXCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) }), CONF_INCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) }) }), }, extra=vol.ALLOW_EXTRA) @@ -51,15 +56,6 @@ ALL_EVENT_TYPES = [ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP ] -GROUP_BY_MINUTES = 15 - -CONTINUOUS_DOMAINS = ['proximity', 'sensor'] - -ATTR_NAME = 'name' -ATTR_MESSAGE = 'message' -ATTR_DOMAIN = 'domain' -ATTR_ENTITY_ID = 'entity_id' - LOG_MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_MESSAGE): cv.template, diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 7c167f93142..847f4131f43 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -1,5 +1,5 @@ """ -Support for microsoft face recognition. +Support for Microsoft face recognition. For more details about this component, please refer to the documentation at https://home-assistant.io/components/microsoft_face/ @@ -13,7 +13,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT +from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,28 +22,25 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'microsoft_face' -DEPENDENCIES = ['camera'] - -FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" - -DATA_MICROSOFT_FACE = 'microsoft_face' +ATTR_CAMERA_ENTITY = 'camera_entity' +ATTR_GROUP = 'group' +ATTR_PERSON = 'person' CONF_AZURE_REGION = 'azure_region' +DATA_MICROSOFT_FACE = 'microsoft_face' +DEFAULT_TIMEOUT = 10 +DEPENDENCIES = ['camera'] +DOMAIN = 'microsoft_face' + +FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" + SERVICE_CREATE_GROUP = 'create_group' -SERVICE_DELETE_GROUP = 'delete_group' -SERVICE_TRAIN_GROUP = 'train_group' SERVICE_CREATE_PERSON = 'create_person' +SERVICE_DELETE_GROUP = 'delete_group' SERVICE_DELETE_PERSON = 'delete_person' SERVICE_FACE_PERSON = 'face_person' - -ATTR_GROUP = 'group' -ATTR_PERSON = 'person' -ATTR_CAMERA_ENTITY = 'camera_entity' -ATTR_NAME = 'name' - -DEFAULT_TIMEOUT = 10 +SERVICE_TRAIN_GROUP = 'train_group' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -111,7 +108,7 @@ def face_person(hass, group, person, camera_entity): @asyncio.coroutine def async_setup(hass, config): - """Set up microsoft face.""" + """Set up Microsoft Face.""" entities = {} face = MicrosoftFace( hass, diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index dcbd1ce1317..9cca81e1485 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -12,8 +12,8 @@ import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN) -from homeassistant.const import CONF_NAME, CONF_PLATFORM + ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, ATTR_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template as template_helper @@ -27,9 +27,8 @@ DEVICE_TRACKER_DOMAIN = 'device_tracker' SERVICE_REGISTER = 'apns_register' ATTR_PUSH_ID = 'push_id' -ATTR_NAME = 'name' -PLATFORM_SCHEMA = vol.Schema({ +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): 'apns', vol.Required(CONF_NAME): cv.string, vol.Required(CONF_CERTFILE): cv.isfile, @@ -66,7 +65,7 @@ class ApnsDevice(object): """ def __init__(self, push_id, name, tracking_device_id=None, disabled=False): - """Initialize Apns Device.""" + """Initialize APNS Device.""" self.device_push_id = push_id self.device_name = name self.tracking_id = tracking_device_id @@ -104,7 +103,7 @@ class ApnsDevice(object): @property def disabled(self): - """Return the .""" + """Return the state of the service.""" return self.device_disabled def disable(self): diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 2e96ec64d97..2f170a20646 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -4,21 +4,18 @@ Support for RFXtrx components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/rfxtrx/ """ - import asyncio -import logging from collections import OrderedDict +import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - ATTR_ENTITY_ID, TEMP_CELSIUS, - CONF_DEVICES -) + ATTR_ENTITY_ID, ATTR_NAME, ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify REQUIREMENTS = ['pyRFXtrx==0.22.1'] @@ -29,8 +26,6 @@ DEFAULT_SIGNAL_REPETITIONS = 1 ATTR_AUTOMATIC_ADD = 'automatic_add' ATTR_DEVICE = 'device' ATTR_DEBUG = 'debug' -ATTR_STATE = 'state' -ATTR_NAME = 'name' ATTR_FIRE_EVENT = 'fire_event' ATTR_DATA_TYPE = 'data_type' ATTR_DUMMY = 'dummy' @@ -40,7 +35,6 @@ CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_FIRE_EVENT = 'fire_event' CONF_DUMMY = 'dummy' -CONF_DEVICE = 'device' CONF_DEBUG = 'debug' CONF_OFF_DELAY = 'off_delay' EVENT_BUTTON_PRESSED = 'button_pressed' diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index b7635f729e2..a8bc441b722 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -4,32 +4,31 @@ Sensor for the CityBikes data. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.citybikes/ """ -import logging -from datetime import timedelta - import asyncio +from datetime import timedelta +import logging + import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT +from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - ATTR_ATTRIBUTION, ATTR_LOCATION, ATTR_LATITUDE, ATTR_LONGITUDE, - STATE_UNKNOWN, LENGTH_METERS, LENGTH_FEET, ATTR_ID) + ATTR_ATTRIBUTION, ATTR_ID, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, + ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS, + LENGTH_FEET, LENGTH_METERS) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import location, distance +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import distance, location _LOGGER = logging.getLogger(__name__) ATTR_EMPTY_SLOTS = 'empty_slots' ATTR_EXTRA = 'extra' ATTR_FREE_BIKES = 'free_bikes' -ATTR_NAME = 'name' ATTR_NETWORK = 'network' ATTR_NETWORKS_LIST = 'networks' ATTR_STATIONS_LIST = 'stations' @@ -151,8 +150,7 @@ def async_setup_platform(hass, config, async_add_devices, network = CityBikesNetwork(hass, network_id) hass.data[PLATFORM][MONITORED_NETWORKS][network_id] = network hass.async_add_job(network.async_refresh) - async_track_time_interval(hass, network.async_refresh, - SCAN_INTERVAL) + async_track_time_interval(hass, network.async_refresh, SCAN_INTERVAL) else: network = hass.data[PLATFORM][MONITORED_NETWORKS][network_id] @@ -160,14 +158,14 @@ def async_setup_platform(hass, config, async_add_devices, devices = [] for station in network.stations: - dist = location.distance(latitude, longitude, - station[ATTR_LATITUDE], - station[ATTR_LONGITUDE]) + dist = location.distance( + latitude, longitude, station[ATTR_LATITUDE], + station[ATTR_LONGITUDE]) station_id = station[ATTR_ID] station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, '')) - if radius > dist or stations_list.intersection((station_id, - station_uid)): + if radius > dist or stations_list.intersection( + (station_id, station_uid)): devices.append(CityBikesStation(hass, network, station_id, name)) async_add_devices(devices, True) @@ -199,8 +197,8 @@ class CityBikesNetwork: for network in networks_list[1:]: network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE] network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE] - dist = location.distance(latitude, longitude, - network_latitude, network_longitude) + dist = location.distance( + latitude, longitude, network_latitude, network_longitude) if dist < minimum_dist: minimum_dist = dist result = network[ATTR_ID] @@ -246,13 +244,13 @@ class CityBikesStation(Entity): uid = "_".join([network.network_id, base_name, station_id]) else: uid = "_".join([network.network_id, station_id]) - self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, - hass=hass) + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, uid, hass=hass) @property def state(self): """Return the state of the sensor.""" - return self._station_data.get(ATTR_FREE_BIKES, STATE_UNKNOWN) + return self._station_data.get(ATTR_FREE_BIKES, None) @property def name(self): diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index f8ada07eec6..849e21a0901 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_24H_VOLUME = '24h_volume' ATTR_AVAILABLE_SUPPLY = 'available_supply' ATTR_MARKET_CAP = 'market_cap' -ATTR_NAME = 'name' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' @@ -130,6 +129,4 @@ class CoinMarketCapData(object): """Get the latest data from blockchain.info.""" from coinmarketcap import Market self.ticker = Market().ticker( - self.currency, - limit=1, - convert=self.display_currency) + self.currency, limit=1, convert=self.display_currency) diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 01e9f443e0e..c0c477ade0b 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -4,19 +4,21 @@ Support for ComEd Hourly Pricing data. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.comed_hourly_pricing/ """ -from datetime import timedelta -import logging import asyncio +from datetime import timedelta import json -import async_timeout +import logging + import aiohttp +import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, STATE_UNKNOWN) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://hourlypricing.comed.com/api' @@ -27,8 +29,6 @@ CONF_ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" CONF_CURRENT_HOUR_AVERAGE = 'current_hour_average' CONF_FIVE_MINUTE = 'five_minute' CONF_MONITORED_FEEDS = 'monitored_feeds' -CONF_NAME = 'name' -CONF_OFFSET = 'offset' CONF_SENSOR_TYPE = 'type' SENSOR_TYPES = { @@ -40,12 +40,12 @@ TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema({ vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_OFFSET, default=0.0): vol.Coerce(float), - vol.Optional(CONF_NAME): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA] + vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA], }) diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index aad8c2f7a92..e7b8bf600a4 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -10,15 +10,14 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY -from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['batinfo==0.4.2'] _LOGGER = logging.getLogger(__name__) -ATTR_NAME = 'name' ATTR_PATH = 'path' ATTR_ALARM = 'alarm' ATTR_CAPACITY = 'capacity' diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index b3ca054f88f..7dd795d8f8d 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, ATTR_NAME, CONF_VERIFY_SSL, CONF_TIMEOUT, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -29,7 +29,6 @@ ATTR_MASK = 'Mask' ATTR_MAX_SPEED = 'Max Speed' ATTR_MEMORY_SIZE = 'Memory Size' ATTR_MODEL = 'Model' -ATTR_NAME = 'Name' ATTR_PACKETS_TX = 'Packets (TX)' ATTR_PACKETS_RX = 'Packets (RX)' ATTR_PACKETS_ERR = 'Packets (Err)' diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 4a555905d50..a5a6eb5f07b 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -10,10 +10,10 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.rfxtrx import ( - ATTR_DATA_TYPE, ATTR_FIRE_EVENT, ATTR_NAME, CONF_AUTOMATIC_ADD, - CONF_DATA_TYPE, CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) + ATTR_DATA_TYPE, ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, CONF_DATA_TYPE, + CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 7acdc1a20bd..ff8ad7fe849 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -6,16 +6,14 @@ https://home-assistant.io/components/sensor.tado/ """ import logging -from homeassistant.const import TEMP_CELSIUS +from homeassistant.components.tado import DATA_TADO +from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.components.tado import (DATA_TADO) -from homeassistant.const import (ATTR_ID) _LOGGER = logging.getLogger(__name__) ATTR_DATA_ID = 'data_id' ATTR_DEVICE = 'device' -ATTR_NAME = 'name' ATTR_ZONE = 'zone' CLIMATE_SENSOR_TYPES = ['temperature', 'humidity', 'power', @@ -39,14 +37,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if zone['type'] == 'HEATING': for variable in CLIMATE_SENSOR_TYPES: sensor_items.append(create_zone_sensor( - tado, zone, zone['name'], zone['id'], - variable)) + tado, zone, zone['name'], zone['id'], variable)) elif zone['type'] == 'HOT_WATER': for variable in HOT_WATER_SENSOR_TYPES: sensor_items.append(create_zone_sensor( - tado, zone, zone['name'], zone['id'], - variable - )) + tado, zone, zone['name'], zone['id'], variable)) me_data = tado.get_me() sensor_items.append(create_device_sensor( diff --git a/homeassistant/components/sensor/wsdot.py b/homeassistant/components/sensor/wsdot.py index fecff260716..0cd5ba44349 100644 --- a/homeassistant/components/sensor/wsdot.py +++ b/homeassistant/components/sensor/wsdot.py @@ -13,24 +13,27 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION, CONF_ID - ) + CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION, CONF_ID, ATTR_NAME) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +ATTR_ACCESS_CODE = 'AccessCode' +ATTR_AVG_TIME = 'AverageTime' +ATTR_CURRENT_TIME = 'CurrentTime' +ATTR_DESCRIPTION = 'Description' +ATTR_TIME_UPDATED = 'TimeUpdated' +ATTR_TRAVEL_TIME_ID = 'TravelTimeID' + +CONF_ATTRIBUTION = "Data provided by WSDOT" + CONF_TRAVEL_TIMES = 'travel_time' -# API codes for travel time details -ATTR_ACCESS_CODE = 'AccessCode' -ATTR_TRAVEL_TIME_ID = 'TravelTimeID' -ATTR_CURRENT_TIME = 'CurrentTime' -ATTR_AVG_TIME = 'AverageTime' -ATTR_NAME = 'Name' -ATTR_TIME_UPDATED = 'TimeUpdated' -ATTR_DESCRIPTION = 'Description' -ATTRIBUTION = "Data provided by WSDOT" +ICON = 'mdi:car' + +RESOURCE = 'http://www.wsdot.wa.gov/Traffic/api/TravelTimes/' \ + 'TravelTimesREST.svc/GetTravelTimeAsJson' SCAN_INTERVAL = timedelta(minutes=3) @@ -43,16 +46,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Get the WSDOT sensor.""" + """Set up the WSDOT sensor.""" sensors = [] for travel_time in config.get(CONF_TRAVEL_TIMES): - name = (travel_time.get(CONF_NAME) or - travel_time.get(CONF_ID)) + name = (travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)) sensors.append( WashingtonStateTravelTimeSensor( - name, - config.get(CONF_API_KEY), - travel_time.get(CONF_ID))) + name, config.get(CONF_API_KEY), travel_time.get(CONF_ID))) + add_devices(sensors, True) @@ -65,8 +66,6 @@ class WashingtonStateTransportSensor(Entity): can read them and make them available. """ - ICON = 'mdi:car' - def __init__(self, name, access_code): """Initialize the sensor.""" self._data = {} @@ -87,16 +86,12 @@ class WashingtonStateTransportSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return self.ICON + return ICON class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" - RESOURCE = ('http://www.wsdot.wa.gov/Traffic/api/TravelTimes/' - 'TravelTimesREST.svc/GetTravelTimeAsJson') - ICON = 'mdi:car' - def __init__(self, name, access_code, travel_time_id): """Construct a travel time sensor.""" self._travel_time_id = travel_time_id @@ -104,10 +99,12 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def update(self): """Get the latest data from WSDOT.""" - params = {ATTR_ACCESS_CODE: self._access_code, - ATTR_TRAVEL_TIME_ID: self._travel_time_id} + params = { + ATTR_ACCESS_CODE: self._access_code, + ATTR_TRAVEL_TIME_ID: self._travel_time_id, + } - response = requests.get(self.RESOURCE, params, timeout=10) + response = requests.get(RESOURCE, params, timeout=10) if response.status_code != 200: _LOGGER.warning("Invalid response from WSDOT API") else: @@ -118,7 +115,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def device_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} for key in [ATTR_AVG_TIME, ATTR_NAME, ATTR_DESCRIPTION, ATTR_TRAVEL_TIME_ID]: attrs[key] = self._data.get(key) @@ -129,7 +126,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return "min" + return 'min' def _parse_wsdot_timestamp(timestamp): @@ -139,5 +136,5 @@ def _parse_wsdot_timestamp(timestamp): # ex: Date(1485040200000-0800) milliseconds, tzone = re.search( r'Date\((\d+)([+-]\d\d)\d\d\)', timestamp).groups() - return datetime.fromtimestamp(int(milliseconds) / 1000, - tz=timezone(timedelta(hours=int(tzone)))) + return datetime.fromtimestamp( + int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone)))) diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 7dd1d25ad94..68e91612008 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -10,11 +10,11 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME from homeassistant.components.rfxtrx import ( CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, CONF_SIGNAL_REPETITIONS, CONF_DEVICES) from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_NAME DEPENDENCIES = ['rfxtrx'] @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, }) }, vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/switch/rpi_pfio.py index c10f417ba49..3031b1e0290 100644 --- a/homeassistant/components/switch/rpi_pfio.py +++ b/homeassistant/components/switch/rpi_pfio.py @@ -10,7 +10,7 @@ import voluptuous as vol import homeassistant.components.rpi_pfio as rpi_pfio from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -19,7 +19,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['rpi_pfio'] ATTR_INVERT_LOGIC = 'invert_logic' -ATTR_NAME = 'name' CONF_PORTS = 'ports' diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index eab67c18aed..042943f7a3f 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, CONF_EMAIL, CONF_PASSWORD, + ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_NAME, CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, __version__) from homeassistant.core import callback @@ -45,7 +45,6 @@ ATTR_ACCESS_TOKEN = 'access_token' ATTR_REFRESH_TOKEN = 'refresh_token' ATTR_CLIENT_ID = 'client_id' ATTR_CLIENT_SECRET = 'client_secret' -ATTR_NAME = 'name' ATTR_PAIRING_MODE = 'pairing_mode' ATTR_KIDDE_RADIO_CODE = 'kidde_radio_code' ATTR_HUB_NAME = 'hub_name' @@ -53,7 +52,8 @@ ATTR_HUB_NAME = 'hub_name' WINK_AUTH_CALLBACK_PATH = '/auth/wink/callback' WINK_AUTH_START = '/auth/wink' WINK_CONFIG_FILE = '.wink.conf' -USER_AGENT = "Manufacturer/Home-Assistant%s python/3 Wink/3" % __version__ +USER_AGENT = "Manufacturer/Home-Assistant{} python/3 Wink/3".format( + __version__) DEFAULT_CONFIG = { 'client_id': 'CLIENT_ID_HERE', diff --git a/homeassistant/const.py b/homeassistant/const.py index 37e0c32ca03..acc30bcd57c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -221,6 +221,9 @@ ATTR_SERVICE_DATA = 'service_data' # IDs ATTR_ID = 'id' +# Name +ATTR_NAME = 'name' + # Data for a SERVICE_EXECUTED event ATTR_SERVICE_CALL_ID = 'service_call_id' diff --git a/tests/components/sensor/test_wsdot.py b/tests/components/sensor/test_wsdot.py index ee2cec3bb2a..8eb542b2b68 100644 --- a/tests/components/sensor/test_wsdot.py +++ b/tests/components/sensor/test_wsdot.py @@ -1,17 +1,16 @@ """The tests for the WSDOT platform.""" +from datetime import datetime, timedelta, timezone import re import unittest -from datetime import timedelta, datetime, timezone import requests_mock +from tests.common import get_test_home_assistant, load_fixture from homeassistant.components.sensor import wsdot from homeassistant.components.sensor.wsdot import ( - WashingtonStateTravelTimeSensor, ATTR_DESCRIPTION, - ATTR_TIME_UPDATED, CONF_API_KEY, CONF_NAME, - CONF_ID, CONF_TRAVEL_TIMES, SCAN_INTERVAL) + ATTR_DESCRIPTION, ATTR_TIME_UPDATED, CONF_API_KEY, CONF_ID, CONF_NAME, + CONF_TRAVEL_TIMES, RESOURCE, SCAN_INTERVAL) from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant class TestWSDOT(unittest.TestCase): @@ -50,7 +49,7 @@ class TestWSDOT(unittest.TestCase): @requests_mock.Mocker() def test_setup(self, mock_req): """Test for operational WSDOT sensor with proper attributes.""" - uri = re.compile(WashingtonStateTravelTimeSensor.RESOURCE + '*') + uri = re.compile(RESOURCE + '*') mock_req.get(uri, text=load_fixture('wsdot.json')) wsdot.setup_platform(self.hass, self.config, self.add_entities) self.assertEqual(len(self.entities), 1) From 2f74ffcf81e22a9c038f2591dc008b7938b96d92 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Mon, 14 May 2018 07:50:09 -0700 Subject: [PATCH 714/924] zha: Fix cluster class check in single-cluster device type (#14303) zigpy now allows custom devices, which might mean that devices have cluster objects which are not instances of the default, but may be instances of sub-classes of the default. This fixes the check for finding single-cluster device entities to handle sub-classes properly. --- homeassistant/components/zha/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 238e89c07f0..3ea95ff1dd1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -256,11 +256,16 @@ class ApplicationListener: """Try to set up an entity from a "bare" cluster.""" if cluster.cluster_id in profile_clusters: return - # pylint: disable=unidiomatic-typecheck - if type(cluster) not in device_classes: + + component = None + for cluster_type, candidate_component in device_classes.items(): + if isinstance(cluster, cluster_type): + component = candidate_component + break + + if component is None: return - component = device_classes[type(cluster)] cluster_key = "{}-{}".format(device_key, cluster.cluster_id) discovery_info = { 'application_listener': self, From 1b5c02ff67473b17cf0959c5e1779f43a451cfc5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 May 2018 21:52:54 +0200 Subject: [PATCH 715/924] Upgrade pygatt to 3.2.0 (#14447) --- homeassistant/components/sensor/skybeacon.py | 32 ++++++++++---------- requirements_all.txt | 3 ++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index eabc33312b2..61933614a74 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -10,38 +10,38 @@ from uuid import UUID import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_MAC, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) + CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity -# REQUIREMENTS = ['pygatt==3.1.1'] +REQUIREMENTS = ['pygatt==3.2.0'] _LOGGER = logging.getLogger(__name__) -CONNECT_LOCK = threading.Lock() - ATTR_DEVICE = 'device' ATTR_MODEL = 'model' +BLE_TEMP_HANDLE = 0x24 +BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' + +CONNECT_LOCK = threading.Lock() +CONNECT_TIMEOUT = 30 + +DEFAULT_NAME = 'Skybeacon' + +SKIP_HANDLE_LOOKUP = True + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=""): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' -BLE_TEMP_HANDLE = 0x24 -SKIP_HANDLE_LOOKUP = True -CONNECT_TIMEOUT = 30 - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" - _LOGGER.warning("This platform has been disabled due to having a " - "requirement depending on enum34.") - return # pylint: disable=unreachable name = config.get(CONF_NAME) mac = config.get(CONF_MAC) @@ -150,7 +150,7 @@ class Monitor(threading.Thread): adapter = pygatt.backends.GATTToolBackend() while True: try: - _LOGGER.info("Connecting to %s", self.name) + _LOGGER.debug("Connecting to %s", self.name) # We need concurrent connect, so lets not reset the device adapter.start(reset_on_start=False) # Seems only one connection can be initiated at a time diff --git a/requirements_all.txt b/requirements_all.txt index a1158d2af3e..63d9b9dcca2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,6 +783,9 @@ pyfritzhome==0.3.7 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.sensor.skybeacon +pygatt==3.2.0 + # homeassistant.components.cover.gogogate2 pygogogate2==0.1.1 From 44e9783c7c45ff9cbaf3c716932a1a06808632cf Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Mon, 14 May 2018 16:37:49 -0400 Subject: [PATCH 716/924] Add support for direction to fan template (#14371) * Initial commit * Update and add tests --- homeassistant/components/fan/template.py | 93 ++++++++++++--- tests/components/fan/test_template.py | 141 +++++++++++++++++++---- 2 files changed, 197 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index 31b335eb2bc..a40437e719b 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -18,11 +18,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, FanEntity, - ATTR_SPEED, ATTR_OSCILLATING, - ENTITY_ID_FORMAT) +from homeassistant.components.fan import ( + SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, + FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT, + SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script @@ -33,25 +32,30 @@ CONF_FANS = 'fans' CONF_SPEED_LIST = 'speeds' CONF_SPEED_TEMPLATE = 'speed_template' CONF_OSCILLATING_TEMPLATE = 'oscillating_template' +CONF_DIRECTION_TEMPLATE = 'direction_template' CONF_ON_ACTION = 'turn_on' CONF_OFF_ACTION = 'turn_off' CONF_SET_SPEED_ACTION = 'set_speed' CONF_SET_OSCILLATING_ACTION = 'set_oscillating' +CONF_SET_DIRECTION_ACTION = 'set_direction' _VALID_STATES = [STATE_ON, STATE_OFF] _VALID_OSC = [True, False] +_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.Schema({ vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional( CONF_SPEED_LIST, @@ -80,18 +84,21 @@ async def async_setup_platform( oscillating_template = device_config.get( CONF_OSCILLATING_TEMPLATE ) + direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) + set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) speed_list = device_config[CONF_SPEED_LIST] entity_ids = set() manual_entity_ids = device_config.get(CONF_ENTITY_ID) - for template in (state_template, speed_template, oscillating_template): + for template in (state_template, speed_template, oscillating_template, + direction_template): if template is None: continue template.hass = hass @@ -114,8 +121,9 @@ async def async_setup_platform( TemplateFan( hass, device, friendly_name, state_template, speed_template, oscillating_template, - on_action, off_action, set_speed_action, - set_oscillating_action, speed_list, entity_ids + direction_template, on_action, off_action, set_speed_action, + set_oscillating_action, set_direction_action, speed_list, + entity_ids ) ) @@ -127,8 +135,9 @@ class TemplateFan(FanEntity): def __init__(self, hass, device_id, friendly_name, state_template, speed_template, oscillating_template, - on_action, off_action, set_speed_action, - set_oscillating_action, speed_list, entity_ids): + direction_template, on_action, off_action, set_speed_action, + set_oscillating_action, set_direction_action, speed_list, + entity_ids): """Initialize the fan.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -138,6 +147,7 @@ class TemplateFan(FanEntity): self._template = state_template self._speed_template = speed_template self._oscillating_template = oscillating_template + self._direction_template = direction_template self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -151,9 +161,14 @@ class TemplateFan(FanEntity): if set_oscillating_action: self._set_oscillating_script = Script(hass, set_oscillating_action) + self._set_direction_script = None + if set_direction_action: + self._set_direction_script = Script(hass, set_direction_action) + self._state = STATE_OFF self._speed = None self._oscillating = None + self._direction = None self._template.hass = self.hass if self._speed_template: @@ -162,6 +177,9 @@ class TemplateFan(FanEntity): if self._oscillating_template: self._oscillating_template.hass = self.hass self._supported_features |= SUPPORT_OSCILLATE + if self._direction_template: + self._direction_template.hass = self.hass + self._supported_features |= SUPPORT_DIRECTION self._entities = entity_ids # List of valid speeds @@ -197,6 +215,11 @@ class TemplateFan(FanEntity): """Return the oscillation state.""" return self._oscillating + @property + def direction(self): + """Return the oscillation state.""" + return self._direction + @property def should_poll(self): """Return the polling state.""" @@ -236,10 +259,30 @@ class TemplateFan(FanEntity): if self._set_oscillating_script is None: return - await self._set_oscillating_script.async_run( - {ATTR_OSCILLATING: oscillating} - ) - self._oscillating = oscillating + if oscillating in _VALID_OSC: + self._oscillating = oscillating + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating}) + else: + _LOGGER.error( + 'Received invalid oscillating value: %s. ' + + 'Expected: %s.', + oscillating, ', '.join(_VALID_OSC)) + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + if self._set_direction_script is None: + return + + if direction in _VALID_DIRECTIONS: + self._direction = direction + await self._set_direction_script.async_run( + {ATTR_DIRECTION: direction}) + else: + _LOGGER.error( + 'Received invalid direction: %s. ' + + 'Expected: %s.', + direction, ', '.join(_VALID_DIRECTIONS)) async def async_added_to_hass(self): """Register callbacks.""" @@ -308,6 +351,7 @@ class TemplateFan(FanEntity): oscillating = self._oscillating_template.async_render() except TemplateError as ex: _LOGGER.error(ex) + oscillating = None self._state = None # Validate osc @@ -322,3 +366,24 @@ class TemplateFan(FanEntity): 'Received invalid oscillating: %s. ' + 'Expected: True/False.', oscillating) self._oscillating = None + + # Update direction if 'direction_template' is configured + if self._direction_template is not None: + try: + direction = self._direction_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + direction = None + self._state = None + + # Validate speed + if direction in _VALID_DIRECTIONS: + self._direction = direction + elif direction == STATE_UNKNOWN: + self._direction = None + else: + _LOGGER.error( + 'Received invalid direction: %s. ' + + 'Expected: %s.', + direction, ', '.join(_VALID_DIRECTIONS)) + self._direction = None diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py index 719a3f96aed..53eb9e8e2d4 100644 --- a/tests/components/fan/test_template.py +++ b/tests/components/fan/test_template.py @@ -6,7 +6,8 @@ from homeassistant import setup import homeassistant.components as components from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.fan import ( - ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + ATTR_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE) from tests.common import ( get_test_home_assistant, assert_setup_component) @@ -20,6 +21,8 @@ _STATE_INPUT_BOOLEAN = 'input_boolean.state' _SPEED_INPUT_SELECT = 'input_select.speed' # Represent for fan's oscillating _OSC_INPUT = 'input_select.osc' +# Represent for fan's direction +_DIRECTION_INPUT_SELECT = 'input_select.direction' class TestTemplateFan: @@ -71,7 +74,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_missing_value_template_config(self): """Test: missing 'value_template' will fail.""" @@ -185,6 +188,8 @@ class TestTemplateFan: "{{ states('input_select.speed') }}", 'oscillating_template': "{{ states('input_select.osc') }}", + 'direction_template': + "{{ states('input_select.direction') }}", 'turn_on': { 'service': 'script.fan_on' }, @@ -199,14 +204,15 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) self.hass.states.set(_STATE_INPUT_BOOLEAN, True) self.hass.states.set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) self.hass.states.set(_OSC_INPUT, 'True') + self.hass.states.set(_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD) self.hass.block_till_done() - self._verify(STATE_ON, SPEED_MEDIUM, True) + self._verify(STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) def test_templates_with_valid_values(self): """Test templates with valid values.""" @@ -222,6 +228,8 @@ class TestTemplateFan: "{{ 'medium' }}", 'oscillating_template': "{{ 1 == 1 }}", + 'direction_template': + "{{ 'forward' }}", 'turn_on': { 'service': 'script.fan_on' @@ -237,7 +245,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_ON, SPEED_MEDIUM, True) + self._verify(STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) def test_templates_invalid_values(self): """Test templates with invalid values.""" @@ -253,6 +261,8 @@ class TestTemplateFan: "{{ '0' }}", 'oscillating_template': "{{ 'xyz' }}", + 'direction_template': + "{{ 'right' }}", 'turn_on': { 'service': 'script.fan_on' @@ -268,7 +278,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) # End of template tests # @@ -283,7 +293,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) # Turn off fan components.fan.turn_off(self.hass, _TEST_FAN) @@ -291,7 +301,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) def test_on_with_speed(self): """Test turn on with speed.""" @@ -304,7 +314,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) def test_set_speed(self): """Test set valid speed.""" @@ -320,7 +330,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to medium components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) @@ -328,7 +338,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM - self._verify(STATE_ON, SPEED_MEDIUM, None) + self._verify(STATE_ON, SPEED_MEDIUM, None, None) def test_set_invalid_speed_from_initial_stage(self): """Test set invalid speed when fan is in initial state.""" @@ -344,7 +354,7 @@ class TestTemplateFan: # verify speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '' - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_set_invalid_speed(self): """Test set invalid speed when fan has valid speed.""" @@ -360,7 +370,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to 'invalid' components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') @@ -368,7 +378,7 @@ class TestTemplateFan: # verify speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) def test_custom_speed_list(self): """Test set custom speed list.""" @@ -384,7 +394,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' - self._verify(STATE_ON, '1', None) + self._verify(STATE_ON, '1', None, None) # Set fan's speed to 'medium' which is invalid components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) @@ -392,7 +402,7 @@ class TestTemplateFan: # verify that speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' - self._verify(STATE_ON, '1', None) + self._verify(STATE_ON, '1', None, None) def test_set_osc(self): """Test set oscillating.""" @@ -408,7 +418,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) # Set fan's osc to False components.fan.oscillate(self.hass, _TEST_FAN, False) @@ -416,7 +426,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'False' - self._verify(STATE_ON, None, False) + self._verify(STATE_ON, None, False, None) def test_set_invalid_osc_from_initial_state(self): """Test set invalid oscillating when fan is in initial state.""" @@ -432,7 +442,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == '' - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_set_invalid_osc(self): """Test set invalid oscillating when fan has valid osc.""" @@ -448,7 +458,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) # Set fan's osc to False components.fan.oscillate(self.hass, _TEST_FAN, None) @@ -456,15 +466,85 @@ class TestTemplateFan: # verify osc is unchanged assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) - def _verify(self, expected_state, expected_speed, expected_oscillating): + def test_set_direction(self): + """Test set valid direction.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to forward + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state \ + == DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + # Set fan's direction to reverse + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state \ + == DIRECTION_REVERSE + self._verify(STATE_ON, None, None, DIRECTION_REVERSE) + + def test_set_invalid_direction_from_initial_stage(self): + """Test set invalid direction when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to 'invalid' + components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify direction is unchanged + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == '' + self._verify(STATE_ON, None, None, None) + + def test_set_invalid_direction(self): + """Test set invalid direction when fan has valid direction.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to forward + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == \ + DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + # Set fan's direction to 'invalid' + components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify direction is unchanged + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == \ + DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + def _verify(self, expected_state, expected_speed, expected_oscillating, + expected_direction): """Verify fan's state, speed and osc.""" state = self.hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == expected_state assert attributes.get(ATTR_SPEED, None) == expected_speed assert attributes.get(ATTR_OSCILLATING, None) == expected_oscillating + assert attributes.get(ATTR_DIRECTION, None) == expected_direction def _register_components(self, speed_list=None): """Register basic components for testing.""" @@ -475,7 +555,7 @@ class TestTemplateFan: {'input_boolean': {'state': None}} ) - with assert_setup_component(2, 'input_select'): + with assert_setup_component(3, 'input_select'): assert setup.setup_component(self.hass, 'input_select', { 'input_select': { 'speed': { @@ -488,6 +568,11 @@ class TestTemplateFan: 'name': 'oscillating', 'options': ['', 'True', 'False'] }, + + 'direction': { + 'name': 'Direction', + 'options': ['', DIRECTION_FORWARD, DIRECTION_REVERSE] + }, } }) @@ -506,6 +591,8 @@ class TestTemplateFan: "{{ states('input_select.speed') }}", 'oscillating_template': "{{ states('input_select.osc') }}", + 'direction_template': + "{{ states('input_select.direction') }}", 'turn_on': { 'service': 'input_boolean.turn_on', @@ -530,6 +617,14 @@ class TestTemplateFan: 'entity_id': _OSC_INPUT, 'option': '{{ oscillating }}' } + }, + 'set_direction': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _DIRECTION_INPUT_SELECT, + 'option': '{{ direction }}' + } } } From cf44b77225387b5e343df2dee77497468d829610 Mon Sep 17 00:00:00 2001 From: Gregory Benner Date: Mon, 14 May 2018 16:52:35 -0400 Subject: [PATCH 717/924] Samsung Family hub camera component (#14458) * add familyhub.py camera * fix import and REQUIREMENTS * add to coveragerc * fix formatting to make houndci-bot happy * ran scripts/gen_requirements_all.py * use CONF_IP_ADDRESS * Revert "ran scripts/gen_requirements_all.py" This reverts commit 3a38681d8a084e6d4811771ae7a18819477885bc. * fix library name * add missing docstrings and enable polling * Sort imports --- .coveragerc | 1 + homeassistant/components/camera/familyhub.py | 63 ++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 67 insertions(+) create mode 100644 homeassistant/components/camera/familyhub.py diff --git a/.coveragerc b/.coveragerc index 28fe39430f3..431c2f6f976 100644 --- a/.coveragerc +++ b/.coveragerc @@ -347,6 +347,7 @@ omit = homeassistant/components/calendar/todoist.py homeassistant/components/camera/bloomsky.py homeassistant/components/camera/canary.py + homeassistant/components/camera/familyhub.py homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py diff --git a/homeassistant/components/camera/familyhub.py b/homeassistant/components/camera/familyhub.py new file mode 100644 index 00000000000..1868078c4c7 --- /dev/null +++ b/homeassistant/components/camera/familyhub.py @@ -0,0 +1,63 @@ +""" +Family Hub camera for Samsung Refrigerators. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/camera.familyhub/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.camera import Camera +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['python-family-hub-local==0.0.2'] + +DEFAULT_NAME = 'FamilyHub Camera' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the Family Hub Camera.""" + from pyfamilyhublocal import FamilyHubCam + address = config.get(CONF_IP_ADDRESS) + name = config.get(CONF_NAME) + + session = async_get_clientsession(hass) + family_hub_cam = FamilyHubCam(address, hass.loop, session) + + async_add_devices([FamilyHubCamera(name, family_hub_cam)], True) + + +class FamilyHubCamera(Camera): + """The representation of a Family Hub camera.""" + + def __init__(self, name, family_hub_cam): + """Initialize camera component.""" + super().__init__() + self._name = name + self.family_hub_cam = family_hub_cam + + async def camera_image(self): + """Return a still image response.""" + return await self.family_hub_cam.async_get_cam_image() + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def should_poll(self): + """Camera should poll periodically.""" + return True diff --git a/requirements_all.txt b/requirements_all.txt index 63d9b9dcca2..a5ad5ef6d0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -969,6 +969,9 @@ python-ecobee-api==0.0.18 # homeassistant.components.sensor.etherscan python-etherscan-api==0.0.3 +# homeassistant.components.camera.familyhub +python-family-hub-local==0.0.2 + # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky python-forecastio==1.4.0 From 7562b4164b0d79f233f6bc967a03ad582ef7b03b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 14 May 2018 22:52:44 +0200 Subject: [PATCH 718/924] Fix key error upon missing node (#14460) * This is needed after gateway ready message generates an update while persistence is off, or while the gateway node hasn't been presented yet. --- homeassistant/components/mysensors.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index f5ad59095dc..6721669a026 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -495,8 +495,9 @@ def gw_callback_factory(hass): _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) - child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) - if child is None: + try: + child = msg.gateway.sensors[msg.node_id].children[msg.child_id] + except KeyError: _LOGGER.debug("Not a child update for node %s", msg.node_id) return From 11c57f9345e3c8c86174b35bb6b9f7a120a6d680 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Mon, 14 May 2018 22:51:32 -0700 Subject: [PATCH 719/924] Bump lakeside version (#14471) This should fix a couple of issues with T1013 bulbs, and also handle accounts that contain unknown devices. --- homeassistant/components/eufy.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 733aa0adbfe..892c0b9972a 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.5'] +REQUIREMENTS = ['lakeside==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a5ad5ef6d0a..d1da68360be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -465,7 +465,7 @@ keyring==12.2.0 keyrings.alt==3.1 # homeassistant.components.eufy -lakeside==0.5 +lakeside==0.6 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From 710533ae8a51a789ec131ee3944f67e8c1d9cf99 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 15 May 2018 01:53:12 -0400 Subject: [PATCH 720/924] Minor Wink fixes (#14468) * Updated Wink light supported feature to reflect what features a given light support. * Fix typo in wink climate --- homeassistant/components/climate/wink.py | 2 +- homeassistant/components/light/wink.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 8c66567a4aa..c67e032c149 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): @property def cool_on(self): """Return whether or not the heat is actually heating.""" - return self.wink.heat_on() + return self.wink.cool_on() @property def current_operation(self): diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 04e9c34b0f6..a2cc4fd7aeb 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -16,8 +16,6 @@ from homeassistant.util.color import \ DEPENDENCIES = ['wink'] -SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Wink lights.""" @@ -78,7 +76,14 @@ class WinkLight(WinkDevice, Light): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_WINK + supports = SUPPORT_BRIGHTNESS + if self.wink.supports_temperature(): + supports = supports | SUPPORT_COLOR_TEMP + if self.wink.supports_xy_color(): + supports = supports | SUPPORT_COLOR + elif self.wink.supports_hue_saturation(): + supports = supports | SUPPORT_COLOR + return supports def turn_on(self, **kwargs): """Turn the switch on.""" From 16bf10b1a277ed2234395e329743e257f9d1a5ea Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 15 May 2018 08:50:07 +0200 Subject: [PATCH 721/924] Don't poll the Samsung Family hub camera (#14473) --- homeassistant/components/camera/familyhub.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/camera/familyhub.py b/homeassistant/components/camera/familyhub.py index 1868078c4c7..e78d341713b 100644 --- a/homeassistant/components/camera/familyhub.py +++ b/homeassistant/components/camera/familyhub.py @@ -48,7 +48,7 @@ class FamilyHubCamera(Camera): self._name = name self.family_hub_cam = family_hub_cam - async def camera_image(self): + async def async_camera_image(self): """Return a still image response.""" return await self.family_hub_cam.async_get_cam_image() @@ -56,8 +56,3 @@ class FamilyHubCamera(Camera): def name(self): """Return the name of this camera.""" return self._name - - @property - def should_poll(self): - """Camera should poll periodically.""" - return True From d47006c98f5efa5c199ea60738e2037f92165abf Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 15 May 2018 11:25:50 +0100 Subject: [PATCH 722/924] Optimistic MQTT light (#14401) * Restores light state, case the light is optimistic * lint * hound * hound * Added mqtt_json * hound * added mqtt_template * lint * cleanup * use ATTR --- homeassistant/components/light/mqtt.py | 63 ++++++++++++------- homeassistant/components/light/mqtt_json.py | 18 +++++- .../components/light/mqtt_template.py | 34 ++++++---- tests/components/light/test_mqtt.py | 23 +++++-- tests/components/light/test_mqtt_json.py | 46 +++++++++----- tests/components/light/test_mqtt_template.py | 54 ++++++++++------ 6 files changed, 165 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index a0534ba4e95..97a4cc8c137 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -17,12 +16,13 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, - CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability) +from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -100,8 +100,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a MQTT Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -213,10 +213,9 @@ class MqttLight(MqttAvailability, Light): self._supported_features |= ( topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() templates = {} for key, tpl in list(self._templates.items()): @@ -226,6 +225,8 @@ class MqttLight(MqttAvailability, Light): tpl.hass = self.hass templates[key] = tpl.async_render_with_possible_json_value + last_state = await async_get_last_state(self.hass, self.entity_id) + @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -237,9 +238,11 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) + elif self._optimistic and last_state: + self._state = last_state.state == STATE_ON @callback def brightness_received(topic, payload, qos): @@ -250,10 +253,13 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_BRIGHTNESS_STATE_TOPIC], brightness_received, self._qos) self._brightness = 255 + elif self._optimistic_brightness and last_state\ + and last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) elif self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: self._brightness = 255 else: @@ -268,11 +274,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self._qos) self._hs = (0, 0) - if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + if self._optimistic_rgb and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_RGB_COMMAND_TOPIC] is not None: self._hs = (0, 0) @callback @@ -282,11 +291,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC], color_temp_received, self._qos) self._color_temp = 150 - if self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: + if self._optimistic_color_temp and last_state\ + and last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + elif self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: self._color_temp = 150 else: self._color_temp = None @@ -298,11 +310,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_EFFECT_STATE_TOPIC], effect_received, self._qos) self._effect = 'none' - if self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: + if self._optimistic_effect and last_state\ + and last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + elif self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: self._effect = 'none' else: self._effect = None @@ -316,10 +331,13 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC], white_value_received, self._qos) self._white_value = 255 + elif self._optimistic_white_value and last_state\ + and last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: self._white_value = 255 else: @@ -334,11 +352,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_XY_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self._qos) self._hs = (0, 0) - if self._topic[CONF_XY_COMMAND_TOPIC] is not None: + if self._optimistic_xy and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_XY_COMMAND_TOPIC] is not None: self._hs = (0, 0) @property @@ -396,8 +417,7 @@ class MqttLight(MqttAvailability, Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -517,8 +537,7 @@ class MqttLight(MqttAvailability, Light): if should_update: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index ca5c76e905f..14f5ee7a9b9 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, @@ -26,6 +26,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -177,6 +178,8 @@ class MqttJson(MqttAvailability, Light): """Subscribe to MQTT events.""" await super().async_added_to_hass() + last_state = await async_get_last_state(self.hass, self.entity_id) + @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -260,6 +263,19 @@ class MqttJson(MqttAvailability, Light): self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) + if self._optimistic and last_state: + self._state = last_state.state == STATE_ON + if last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + if last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + if last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + if last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + if last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 06a94cd23b4..e32c13fc5b6 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -4,7 +4,6 @@ Support for MQTT Template lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_template/ """ -import asyncio import logging import voluptuous as vol @@ -22,6 +21,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -66,8 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a MQTT Template light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -152,10 +152,11 @@ class MqttTemplate(MqttAvailability, Light): if tpl is not None: tpl.hass = hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() + + last_state = await async_get_last_state(self.hass, self.entity_id) @callback def state_received(topic, payload, qos): @@ -223,10 +224,23 @@ class MqttTemplate(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topics[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topics[CONF_STATE_TOPIC], state_received, self._qos) + if self._optimistic and last_state: + self._state = last_state.state == STATE_ON + if last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + if last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + if last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + if last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + if last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -280,8 +294,7 @@ class MqttTemplate(MqttAvailability, Light): """Return the current effect.""" return self._effect - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on. This method is a coroutine. @@ -339,8 +352,7 @@ class MqttTemplate(MqttAvailability, Light): if self._optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off. This method is a coroutine. diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 7f7841b1a69..8b51adb2187 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -140,14 +140,16 @@ light: """ import unittest from unittest import mock +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message) + fire_mqtt_message, mock_coro) class TestLightMQTT(unittest.TestCase): @@ -481,12 +483,23 @@ class TestLightMQTT(unittest.TestCase): 'payload_on': 'on', 'payload_off': 'off' }} - - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, config) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + with patch('homeassistant.components.light.mqtt.async_get_last_state', + return_value=mock_coro(fake_state)): + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, config) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) light.turn_on(self.hass, 'light.test') diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 5bae1061b7f..275fb42ede9 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -90,15 +90,17 @@ light: import json import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) + assert_setup_component, mock_coro) class TestLightMQTTJSON(unittest.TestCase): @@ -284,22 +286,36 @@ class TestLightMQTTJSON(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): \ # pylint: disable=invalid-name """Test the sending of command in optimistic mode.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'qos': 2 - } - }) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + + with patch('homeassistant.components.light.mqtt_json' + '.async_get_last_state', + return_value=mock_coro(fake_state)): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'qos': 2 + } + }) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 90d68dd10d2..1440a73f98e 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -27,14 +27,16 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) + assert_setup_component, mock_coro) class TestLightMQTTTemplate(unittest.TestCase): @@ -207,26 +209,40 @@ class TestLightMQTTTemplate(unittest.TestCase): def test_optimistic(self): \ # pylint: disable=invalid-name """Test optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'qos': 2 - } - }) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + + with patch('homeassistant.components.light.mqtt_template' + '.async_get_last_state', + return_value=mock_coro(fake_state)): + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'qos': 2 + } + }) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light From 612a37b2dd37f4856ac7103bb7bc6f7dc6d8b970 Mon Sep 17 00:00:00 2001 From: c727 Date: Tue, 15 May 2018 16:57:51 +0200 Subject: [PATCH 723/924] Remove simplepush.io (#14358) --- .coveragerc | 1 - homeassistant/components/notify/simplepush.py | 59 ------------------- requirements_all.txt | 3 - 3 files changed, 63 deletions(-) delete mode 100644 homeassistant/components/notify/simplepush.py diff --git a/.coveragerc b/.coveragerc index 431c2f6f976..d95bcb63b73 100644 --- a/.coveragerc +++ b/.coveragerc @@ -540,7 +540,6 @@ omit = homeassistant/components/notify/rest.py homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py - homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/stride.py diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py deleted file mode 100644 index 9d5c58fc5b1..00000000000 --- a/homeassistant/components/notify/simplepush.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Simplepush notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.simplepush/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_PASSWORD - -REQUIREMENTS = ['simplepush==1.1.4'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_ENCRYPTED = 'encrypted' - -CONF_DEVICE_KEY = 'device_key' -CONF_EVENT = 'event' -CONF_SALT = 'salt' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE_KEY): cv.string, - vol.Optional(CONF_EVENT): cv.string, - vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, - vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Simplepush notification service.""" - return SimplePushNotificationService(config) - - -class SimplePushNotificationService(BaseNotificationService): - """Implementation of the notification service for Simplepush.""" - - def __init__(self, config): - """Initialize the Simplepush notification service.""" - self._device_key = config.get(CONF_DEVICE_KEY) - self._event = config.get(CONF_EVENT) - self._password = config.get(CONF_PASSWORD) - self._salt = config.get(CONF_SALT) - - def send_message(self, message='', **kwargs): - """Send a message to a Simplepush user.""" - from simplepush import send, send_encrypted - - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - if self._password: - send_encrypted(self._device_key, self._password, self._salt, title, - message, event=self._event) - else: - send(self._device_key, title, message, event=self._event) diff --git a/requirements_all.txt b/requirements_all.txt index d1da68360be..820a316a238 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,9 +1180,6 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan shodan==1.7.7 -# homeassistant.components.notify.simplepush -simplepush==1.1.4 - # homeassistant.components.alarm_control_panel.simplisafe simplisafe-python==1.0.5 From de50d5d9c1c0375c5e46042f3168d356c73f6aa1 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Tue, 15 May 2018 13:58:00 -0400 Subject: [PATCH 724/924] Add Konnected component with support for discovery, binary sensor and switch (#13670) * Add Konnected component with support for discovery, binary sensor, and switch Co-authored-by: Eitan Mosenkis * Use more built-in constants from const.py * Fix switch actuation with low-level trigger * Quiet logging; Improve schema validation. * Execute sync request outside of event loop * Whitespace cleanup * Cleanup config validation; async device setup * Update API endpoint for Konnected 2.2.0 changes * Update async coroutines via @OttoWinter * Make backwards compatible with Konnected < 2.2.0 * Add constants suggested by @syssi * Add to CODEOWNERS * Remove TODO comment --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/binary_sensor/konnected.py | 74 ++++ homeassistant/components/discovery.py | 3 + homeassistant/components/konnected.py | 315 ++++++++++++++++++ homeassistant/components/switch/konnected.py | 87 +++++ requirements_all.txt | 3 + 7 files changed, 487 insertions(+) create mode 100644 homeassistant/components/binary_sensor/konnected.py create mode 100644 homeassistant/components/konnected.py create mode 100644 homeassistant/components/switch/konnected.py diff --git a/.coveragerc b/.coveragerc index d95bcb63b73..2eeb3a1530d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -153,6 +153,9 @@ omit = homeassistant/components/knx.py homeassistant/components/*/knx.py + homeassistant/components/konnected.py + homeassistant/components/*/konnected.py + homeassistant/components/lametric.py homeassistant/components/*/lametric.py diff --git a/CODEOWNERS b/CODEOWNERS index 33966d1badb..32639fed43c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/konnected.py @heythisisnate +homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py new file mode 100644 index 00000000000..c7e2b7c84fe --- /dev/null +++ b/homeassistant/components/binary_sensor/konnected.py @@ -0,0 +1,74 @@ +""" +Support for wired binary sensors attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.konnected/ +""" +import asyncio +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) +from homeassistant.const import ( + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_STATE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up binary sensors attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN] + device_id = discovery_info['device_id'] + sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()] + async_add_devices(sensors) + + +class KonnectedBinarySensor(BinarySensorDevice): + """Representation of a Konnected binary sensor.""" + + def __init__(self, device_id, pin_num, data): + """Initialize the binary sensor.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get(ATTR_STATE) + self._device_class = self._data.get(CONF_TYPE) + self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._data['entity'] = self + _LOGGER.debug('Created new Konnected sensor: %s', self._name) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @asyncio.coroutine + def async_set_state(self, state): + """Update the sensor's state.""" + self._state = state + self._data[ATTR_STATE] = state + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 68cf293ce48..a24e82da106 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -37,6 +37,7 @@ SERVICE_WINK = 'wink' SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' +SERVICE_KONNECTED = 'konnected' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' SERVICE_SABNZBD = 'sabnzbd' @@ -62,6 +63,7 @@ SERVICE_HANDLERS = { SERVICE_DAIKIN: ('daikin', None), SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), + SERVICE_KONNECTED: ('konnected', None), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), @@ -191,6 +193,7 @@ def _discover(netdisco): for disc in netdisco.discover(): for service in netdisco.get_info(disc): results.append((disc, service)) + finally: netdisco.stop() diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py new file mode 100644 index 00000000000..8c5578f10e4 --- /dev/null +++ b/homeassistant/components/konnected.py @@ -0,0 +1,315 @@ +""" +Support for Konnected devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/konnected/ +""" +import logging +import hmac +import json +import voluptuous as vol + +from aiohttp.hdrs import AUTHORIZATION +from aiohttp.web import Request, Response # NOQA + +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.discovery import SERVICE_KONNECTED +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, + CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, + CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, + ATTR_STATE) +from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['konnected==0.1.2'] + +DOMAIN = 'konnected' + +CONF_ACTIVATION = 'activation' +STATE_LOW = 'low' +STATE_HIGH = 'high' + +PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} +ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} + +_BINARY_SENSOR_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +_SWITCH_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): + vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)) + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_DEVICES): [{ + vol.Required(CONF_ID): cv.string, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [_SWITCH_SCHEMA]), + }], + }), + }, + extra=vol.ALLOW_EXTRA, +) + +DEPENDENCIES = ['http', 'discovery'] + +ENDPOINT_ROOT = '/api/konnected' +UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') + + +async def async_setup(hass, config): + """Set up the Konnected platform.""" + cfg = config.get(DOMAIN) + if cfg is None: + cfg = {} + + access_token = cfg.get(CONF_ACCESS_TOKEN) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token} + + async def async_device_discovered(service, info): + """Call when a Konnected device has been discovered.""" + _LOGGER.debug("Discovered a new Konnected device: %s", info) + host = info.get(CONF_HOST) + port = info.get(CONF_PORT) + + device = KonnectedDevice(hass, host, port, cfg) + await device.async_setup() + + discovery.async_listen( + hass, + SERVICE_KONNECTED, + async_device_discovered) + + hass.http.register_view(KonnectedView(access_token)) + + return True + + +class KonnectedDevice(object): + """A representation of a single Konnected device.""" + + def __init__(self, hass, host, port, config): + """Initialize the Konnected device.""" + self.hass = hass + self.host = host + self.port = port + self.user_config = config + + import konnected + self.client = konnected.Client(host, str(port)) + self.status = self.client.get_status() + _LOGGER.info('Initialized Konnected device %s', self.device_id) + + async def async_setup(self): + """Set up a newly discovered Konnected device.""" + user_config = self.config() + if user_config: + _LOGGER.debug('Configuring Konnected device %s', self.device_id) + self.save_data() + await self.async_sync_device_config() + await discovery.async_load_platform( + self.hass, 'binary_sensor', + DOMAIN, {'device_id': self.device_id}) + await discovery.async_load_platform( + self.hass, 'switch', DOMAIN, + {'device_id': self.device_id}) + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.status['mac'].replace(':', '') + + def config(self): + """Return an object representing the user defined configuration.""" + device_id = self.device_id + valid_keys = [device_id, device_id.upper(), + device_id[6:], device_id.upper()[6:]] + configured_devices = self.user_config[CONF_DEVICES] + return next((device for device in + configured_devices if device[CONF_ID] in valid_keys), + None) + + def save_data(self): + """Save the device configuration to `hass.data`.""" + sensors = {} + for entity in self.config().get(CONF_BINARY_SENSORS) or []: + if CONF_ZONE in entity: + pin = ZONE_TO_PIN[entity[CONF_ZONE]] + else: + pin = entity[CONF_PIN] + + sensor_status = next((sensor for sensor in + self.status.get('sensors') if + sensor.get(CONF_PIN) == pin), {}) + if sensor_status.get(ATTR_STATE): + initial_state = bool(int(sensor_status.get(ATTR_STATE))) + else: + initial_state = None + + sensors[pin] = { + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state + } + _LOGGER.debug('Set up sensor %s (initial state: %s)', + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) + + actuators = {} + for entity in self.config().get(CONF_SWITCHES) or []: + if 'zone' in entity: + pin = ZONE_TO_PIN[entity['zone']] + else: + pin = entity['pin'] + + actuator_status = next((actuator for actuator in + self.status.get('actuators') if + actuator.get('pin') == pin), {}) + if actuator_status.get(ATTR_STATE): + initial_state = bool(int(actuator_status.get(ATTR_STATE))) + else: + initial_state = None + + actuators[pin] = { + CONF_NAME: entity.get( + CONF_NAME, 'Konnected {} Actuator {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state, + CONF_ACTIVATION: entity[CONF_ACTIVATION], + } + _LOGGER.debug('Set up actuator %s (initial state: %s)', + actuators[pin].get(CONF_NAME), + actuators[pin].get(ATTR_STATE)) + + device_data = { + 'client': self.client, + CONF_BINARY_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_HOST: self.host, + CONF_PORT: self.port, + } + + if CONF_DEVICES not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][CONF_DEVICES] = {} + + _LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data) + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] + + def sensor_configuration(self): + """Return the configuration map for syncing sensors.""" + return [{'pin': p} for p in + self.stored_configuration[CONF_BINARY_SENSORS].keys()] + + def actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [{'pin': p, + 'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] + else 1)} + for p, data in + self.stored_configuration[CONF_SWITCHES].items()] + + async def async_sync_device_config(self): + """Sync the new pin configuration to the Konnected device.""" + desired_sensor_configuration = self.sensor_configuration() + current_sensor_configuration = [ + {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] + _LOGGER.debug('%s: desired sensor config: %s', self.device_id, + desired_sensor_configuration) + _LOGGER.debug('%s: current sensor config: %s', self.device_id, + current_sensor_configuration) + + desired_actuator_config = self.actuator_configuration() + current_actuator_config = self.status.get('actuators') + _LOGGER.debug('%s: desired actuator config: %s', self.device_id, + desired_actuator_config) + _LOGGER.debug('%s: current actuator config: %s', self.device_id, + current_actuator_config) + + if (desired_sensor_configuration != current_sensor_configuration) or \ + (current_actuator_config != desired_actuator_config): + _LOGGER.debug('pushing settings to device %s', self.device_id) + self.client.put_settings( + desired_sensor_configuration, + desired_actuator_config, + self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), + self.hass.config.api.base_url + ENDPOINT_ROOT + ) + + +class KonnectedView(HomeAssistantView): + """View creates an endpoint to receive push updates from the device.""" + + url = UPDATE_ENDPOINT + extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}'] + name = 'api:konnected' + requires_auth = False # Uses access token from configuration + + def __init__(self, auth_token): + """Initialize the view.""" + self.auth_token = auth_token + + async def put(self, request: Request, device_id, + pin_num=None, state=None) -> Response: + """Receive a sensor update via PUT request and async set state.""" + hass = request.app['hass'] + data = hass.data[DOMAIN] + + try: # Konnected 2.2.0 and above supports JSON payloads + payload = await request.json() + pin_num = payload['pin'] + state = payload['state'] + except json.decoder.JSONDecodeError: + _LOGGER.warning(("Your Konnected device software may be out of " + "date. Visit https://help.konnected.io for " + "updating instructions.")) + + auth = request.headers.get(AUTHORIZATION, None) + if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): + return self.json_message( + "unauthorized", status_code=HTTP_UNAUTHORIZED) + pin_num = int(pin_num) + state = bool(int(state)) + device = data[CONF_DEVICES].get(device_id) + if device is None: + return self.json_message('unregistered device', + status_code=HTTP_BAD_REQUEST) + pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or \ + device[CONF_SWITCHES].get(pin_num) + + if pin_data is None: + return self.json_message('unregistered sensor/actuator', + status_code=HTTP_BAD_REQUEST) + entity = pin_data.get('entity') + if entity is None: + return self.json_message('uninitialized sensor/actuator', + status_code=HTTP_INTERNAL_SERVER_ERROR) + + await entity.async_set_state(state) + return self.json_message('ok') diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py new file mode 100644 index 00000000000..e88f9826678 --- /dev/null +++ b/homeassistant/components/switch/konnected.py @@ -0,0 +1,87 @@ +""" +Support for wired switches attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.konnected/ +""" + +import asyncio +import logging + +from homeassistant.components.konnected import ( + DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, STATE_LOW, STATE_HIGH) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import (CONF_DEVICES, CONF_SWITCHES, ATTR_STATE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set switches attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN] + device_id = discovery_info['device_id'] + client = data[CONF_DEVICES][device_id]['client'] + switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_SWITCHES].items()] + async_add_devices(switches) + + +class KonnectedSwitch(ToggleEntity): + """Representation of a Konnected switch.""" + + def __init__(self, device_id, pin_num, data, client): + """Initialize the switch.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get(ATTR_STATE) + self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) + self._name = self._data.get( + 'name', 'Konnected {} Actuator {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._data['entity'] = self + self._client = client + _LOGGER.debug('Created new switch: %s', self._name) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def turn_on(self, **kwargs): + """Send a command to turn on the switch.""" + self._client.put_device(self._pin_num, + int(self._activation == STATE_HIGH)) + self._set_state(True) + + def turn_off(self, **kwargs): + """Send a command to turn off the switch.""" + self._client.put_device(self._pin_num, + int(self._activation == STATE_LOW)) + self._set_state(False) + + def _set_state(self, state): + self._state = state + self._data[ATTR_STATE] = state + self.schedule_update_ha_state() + _LOGGER.debug('Setting status of %s actuator pin %s to %s', + self._device_id, self.name, state) + + @asyncio.coroutine + def async_set_state(self, state): + """Update the switch's state.""" + self._state = state + self._data[ATTR_STATE] = state + self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 820a316a238..a3f8aa13258 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,6 +464,9 @@ keyring==12.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1 +# homeassistant.components.konnected +konnected==0.1.2 + # homeassistant.components.eufy lakeside==0.6 From e49e0b5a13f72bf9413086dc9570bf3f224cf6f7 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 16 May 2018 04:43:27 +1000 Subject: [PATCH 725/924] Make Feedreader component more extendable (#14342) * moved regular updates definition to own method to be able to override behaviour in subclass * moved filter by max entries to own method to be able to override behaviour in subclass * event type used when firing events to the bus now based on variable to be able to override behaviour in subclass * feed id introduced instead of url for storing meta-data about the feed to be able to fetch the same feed from different configs with different filtering rules applied * keep the status of the last update; continue processing the entries retrieved even if a recoverable error was detected while fetching the feed * added test cases for feedreader component * better explanation around breaking change * fixing lint issues and hound violations * fixing lint issue * using assert_called_once_with instead of assert_called_once to make it compatible with python 3.5 --- .coveragerc | 1 - homeassistant/components/feedreader.py | 61 +++++++--- tests/components/test_feedreader.py | 149 +++++++++++++++++++++++++ tests/fixtures/feedreader.xml | 20 ++++ tests/fixtures/feedreader1.xml | 27 +++++ tests/fixtures/feedreader2.xml | 97 ++++++++++++++++ tests/fixtures/feedreader3.xml | 26 +++++ 7 files changed, 362 insertions(+), 19 deletions(-) create mode 100644 tests/components/test_feedreader.py create mode 100644 tests/fixtures/feedreader.xml create mode 100644 tests/fixtures/feedreader1.xml create mode 100644 tests/fixtures/feedreader2.xml create mode 100644 tests/fixtures/feedreader3.xml diff --git a/.coveragerc b/.coveragerc index 2eeb3a1530d..3ecf2411384 100644 --- a/.coveragerc +++ b/.coveragerc @@ -421,7 +421,6 @@ omit = homeassistant/components/emoncms_history.py homeassistant/components/emulated_hue/upnp.py homeassistant/components/fan/mqtt.py - homeassistant/components/feedreader.py homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py homeassistant/components/goalfeed.py diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 2c0e146491a..61fbe9f3171 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -55,16 +55,28 @@ class FeedManager(object): self._firstrun = True self._storage = storage self._last_entry_timestamp = None + self._last_update_successful = False self._has_published_parsed = False + self._event_type = EVENT_FEEDREADER + self._feed_id = url hass.bus.listen_once( EVENT_HOMEASSISTANT_START, lambda _: self._update()) - track_utc_time_change( - hass, lambda now: self._update(), minute=0, second=0) + self._init_regular_updates(hass) def _log_no_entries(self): """Send no entries log at debug level.""" _LOGGER.debug("No new entries to be published in feed %s", self._url) + def _init_regular_updates(self, hass): + """Schedule regular updates at the top of the clock.""" + track_utc_time_change( + hass, lambda now: self._update(), minute=0, second=0) + + @property + def last_update_successful(self): + """Return True if the last feed update was successful.""" + return self._last_update_successful + def _update(self): """Update the feed and publish new entries to the event bus.""" import feedparser @@ -76,26 +88,39 @@ class FeedManager(object): else self._feed.get('modified')) if not self._feed: _LOGGER.error("Error fetching feed data from %s", self._url) + self._last_update_successful = False else: + # The 'bozo' flag really only indicates that there was an issue + # during the initial parsing of the XML, but it doesn't indicate + # whether this is an unrecoverable error. In this case the + # feedparser lib is trying a less strict parsing approach. + # If an error is detected here, log error message but continue + # processing the feed entries if present. if self._feed.bozo != 0: - _LOGGER.error("Error parsing feed %s", self._url) + _LOGGER.error("Error parsing feed %s: %s", self._url, + self._feed.bozo_exception) # Using etag and modified, if there's no new data available, # the entries list will be empty - elif self._feed.entries: + if self._feed.entries: _LOGGER.debug("%s entri(es) available in feed %s", len(self._feed.entries), self._url) - if len(self._feed.entries) > MAX_ENTRIES: - _LOGGER.debug("Processing only the first %s entries " - "in feed %s", MAX_ENTRIES, self._url) - self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + self._filter_entries() self._publish_new_entries() if self._has_published_parsed: self._storage.put_timestamp( - self._url, self._last_entry_timestamp) + self._feed_id, self._last_entry_timestamp) else: self._log_no_entries() + self._last_update_successful = True _LOGGER.info("Fetch from feed %s completed", self._url) + def _filter_entries(self): + """Filter the entries provided and return the ones to keep.""" + if len(self._feed.entries) > MAX_ENTRIES: + _LOGGER.debug("Processing only the first %s entries " + "in feed %s", MAX_ENTRIES, self._url) + self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" # We are lucky, `published_parsed` data available, let's make use of @@ -109,12 +134,12 @@ class FeedManager(object): _LOGGER.debug("No published_parsed info available for entry %s", entry.title) entry.update({'feed_url': self._url}) - self._hass.bus.fire(EVENT_FEEDREADER, entry) + self._hass.bus.fire(self._event_type, entry) def _publish_new_entries(self): """Publish new entries to the event bus.""" new_entries = False - self._last_entry_timestamp = self._storage.get_timestamp(self._url) + self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) if self._last_entry_timestamp: self._firstrun = False else: @@ -157,18 +182,18 @@ class StoredData(object): _LOGGER.error("Error loading data from pickled file %s", self._data_file) - def get_timestamp(self, url): - """Return stored timestamp for given url.""" + def get_timestamp(self, feed_id): + """Return stored timestamp for given feed id (usually the url).""" self._fetch_data() - return self._data.get(url) + return self._data.get(feed_id) - def put_timestamp(self, url, timestamp): - """Update timestamp for given URL.""" + def put_timestamp(self, feed_id, timestamp): + """Update timestamp for given feed id (usually the url).""" self._fetch_data() with self._lock, open(self._data_file, 'wb') as myfile: - self._data.update({url: timestamp}) + self._data.update({feed_id: timestamp}) _LOGGER.debug("Overwriting feed %s timestamp in storage file %s", - url, self._data_file) + feed_id, self._data_file) try: pickle.dump(self._data, myfile) except: # noqa: E722 # pylint: disable=bare-except diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py new file mode 100644 index 00000000000..2288e21e37a --- /dev/null +++ b/tests/components/test_feedreader.py @@ -0,0 +1,149 @@ +"""The tests for the feedreader component.""" +import time +from datetime import datetime + +import unittest +from genericpath import exists +from logging import getLogger +from os import remove +from unittest import mock +from unittest.mock import patch + +from homeassistant.components import feedreader +from homeassistant.components.feedreader import CONF_URLS, FeedManager, \ + StoredData, EVENT_FEEDREADER +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import callback +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, assert_setup_component, \ + load_fixture + +_LOGGER = getLogger(__name__) + +URL = 'http://some.rss.local/rss_feed.xml' +VALID_CONFIG_1 = { + feedreader.DOMAIN: { + CONF_URLS: [URL] + } +} + + +class TestFeedreaderComponent(unittest.TestCase): + """Test the feedreader component.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + # Delete any previously stored data + data_file = self.hass.config.path("{}.pickle".format('feedreader')) + if exists(data_file): + remove(data_file) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_one_feed(self): + """Test the general setup of this component.""" + with assert_setup_component(1, 'feedreader'): + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_1)) + + def setup_manager(self, feed_data): + """Generic test setup method.""" + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(EVENT_FEEDREADER, record_event) + + # Loading raw data from fixture and plug in to data object as URL + # works since the third-party feedparser library accepts a URL + # as well as the actual data. + data_file = self.hass.config.path("{}.pickle".format( + feedreader.DOMAIN)) + storage = StoredData(data_file) + with patch("homeassistant.components.feedreader." + "track_utc_time_change") as track_method: + manager = FeedManager(feed_data, self.hass, storage) + # Can't use 'assert_called_once' here because it's not available + # in Python 3.5 yet. + track_method.assert_called_once_with(self.hass, mock.ANY, minute=0, + second=0) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + return manager, events + + def test_feed(self): + """Test simple feed with valid data.""" + feed_data = load_fixture('feedreader.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + assert events[0].data.title == "Title 1" + assert events[0].data.description == "Description 1" + assert events[0].data.link == "http://www.example.com/link/1" + assert events[0].data.id == "GUID 1" + assert datetime.fromtimestamp( + time.mktime(events[0].data.published_parsed)) == \ + datetime(2018, 4, 30, 5, 10, 0) + assert manager.last_update_successful is True + + def test_feed_updates(self): + """Test feed updates.""" + # 1. Run + feed_data = load_fixture('feedreader.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + # 2. Run + feed_data2 = load_fixture('feedreader1.xml') + # Must patch 'get_timestamp' method because the timestamp is stored + # with the URL which in these tests is the raw XML data. + with patch("homeassistant.components.feedreader.StoredData." + "get_timestamp", return_value=time.struct_time( + (2018, 4, 30, 5, 10, 0, 0, 120, 0))): + manager2, events2 = self.setup_manager(feed_data2) + assert len(events2) == 1 + # 3. Run + feed_data3 = load_fixture('feedreader1.xml') + with patch("homeassistant.components.feedreader.StoredData." + "get_timestamp", return_value=time.struct_time( + (2018, 4, 30, 5, 11, 0, 0, 120, 0))): + manager3, events3 = self.setup_manager(feed_data3) + assert len(events3) == 0 + + def test_feed_max_length(self): + """Test long feed beyond the 20 entry limit.""" + feed_data = load_fixture('feedreader2.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 20 + + def test_feed_without_publication_date(self): + """Test simple feed with entry without publication date.""" + feed_data = load_fixture('feedreader3.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 2 + + def test_feed_invalid_data(self): + """Test feed with invalid data.""" + feed_data = "INVALID DATA" + manager, events = self.setup_manager(feed_data) + assert len(events) == 0 + assert manager.last_update_successful is True + + @mock.patch('feedparser.parse', return_value=None) + def test_feed_parsing_failed(self, mock_parse): + """Test feed where parsing fails.""" + data_file = self.hass.config.path("{}.pickle".format( + feedreader.DOMAIN)) + storage = StoredData(data_file) + manager = FeedManager("FEED DATA", self.hass, storage) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + assert manager.last_update_successful is False diff --git a/tests/fixtures/feedreader.xml b/tests/fixtures/feedreader.xml new file mode 100644 index 00000000000..8c85a4975ee --- /dev/null +++ b/tests/fixtures/feedreader.xml @@ -0,0 +1,20 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + + diff --git a/tests/fixtures/feedreader1.xml b/tests/fixtures/feedreader1.xml new file mode 100644 index 00000000000..ff856125779 --- /dev/null +++ b/tests/fixtures/feedreader1.xml @@ -0,0 +1,27 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 2 + Description 2 + http://www.example.com/link/2 + GUID 2 + Mon, 30 Apr 2018 15:11:00 +1000 + + + + diff --git a/tests/fixtures/feedreader2.xml b/tests/fixtures/feedreader2.xml new file mode 100644 index 00000000000..653a16e4561 --- /dev/null +++ b/tests/fixtures/feedreader2.xml @@ -0,0 +1,97 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Mon, 30 Apr 2018 15:00:00 +1000 + + + Title 2 + Mon, 30 Apr 2018 15:01:00 +1000 + + + Title 3 + Mon, 30 Apr 2018 15:02:00 +1000 + + + Title 4 + Mon, 30 Apr 2018 15:03:00 +1000 + + + Title 5 + Mon, 30 Apr 2018 15:04:00 +1000 + + + Title 6 + Mon, 30 Apr 2018 15:05:00 +1000 + + + Title 7 + Mon, 30 Apr 2018 15:06:00 +1000 + + + Title 8 + Mon, 30 Apr 2018 15:07:00 +1000 + + + Title 9 + Mon, 30 Apr 2018 15:08:00 +1000 + + + Title 10 + Mon, 30 Apr 2018 15:09:00 +1000 + + + Title 11 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 12 + Mon, 30 Apr 2018 15:11:00 +1000 + + + Title 13 + Mon, 30 Apr 2018 15:12:00 +1000 + + + Title 14 + Mon, 30 Apr 2018 15:13:00 +1000 + + + Title 15 + Mon, 30 Apr 2018 15:14:00 +1000 + + + Title 16 + Mon, 30 Apr 2018 15:15:00 +1000 + + + Title 17 + Mon, 30 Apr 2018 15:16:00 +1000 + + + Title 18 + Mon, 30 Apr 2018 15:17:00 +1000 + + + Title 19 + Mon, 30 Apr 2018 15:18:00 +1000 + + + Title 20 + Mon, 30 Apr 2018 15:19:00 +1000 + + + Title 21 + Mon, 30 Apr 2018 15:20:00 +1000 + + + + diff --git a/tests/fixtures/feedreader3.xml b/tests/fixtures/feedreader3.xml new file mode 100644 index 00000000000..7b28e067cfe --- /dev/null +++ b/tests/fixtures/feedreader3.xml @@ -0,0 +1,26 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 2 + Description 2 + http://www.example.com/link/2 + GUID 2 + + + + From 2e7b5dcd196782e2005577a6229ce4f4d168d814 Mon Sep 17 00:00:00 2001 From: Gerard Date: Tue, 15 May 2018 20:47:32 +0200 Subject: [PATCH 726/924] BMW code cleanup (#14424) * Some cleanup for BMW sensors * Changed dict sort * Updates based on review and Travis --- .../binary_sensor/bmw_connected_drive.py | 24 ++++++----- .../components/sensor/bmw_connected_drive.py | 40 ++++++++----------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index af3ebd53b80..e214610f46d 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -115,14 +115,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['lights_parking'] = vehicle_state.parking_lights.value elif self._attribute == 'condition_based_services': for report in vehicle_state.condition_based_services: - service_type = report.service_type.lower().replace('_', ' ') - result['{} status'.format(service_type)] = report.state.value - if report.due_date is not None: - result['{} date'.format(service_type)] = \ - report.due_date.strftime('%Y-%m-%d') - if report.due_distance is not None: - result['{} distance'.format(service_type)] = \ - '{} km'.format(report.due_distance) + result.update(self._format_cbs_report(report)) elif self._attribute == 'check_control_messages': check_control_messages = vehicle_state.check_control_messages if not check_control_messages: @@ -139,7 +132,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['connection_status'] = \ vehicle_state._attributes['connectionStatus'] - return result + return sorted(result.items()) def update(self): """Read new state data from the library.""" @@ -177,6 +170,19 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._state = (vehicle_state._attributes['connectionStatus'] == 'CONNECTED') + @staticmethod + def _format_cbs_report(report): + result = {} + service_type = report.service_type.lower().replace('_', ' ') + result['{} status'.format(service_type)] = report.state.value + if report.due_date is not None: + result['{} date'.format(service_type)] = \ + report.due_date.strftime('%Y-%m-%d') + if report.due_distance is not None: + result['{} distance'.format(service_type)] = \ + '{} km'.format(report.due_distance) + return result + def update_callback(self): """Schedule a state update.""" self.schedule_update_ha_state(True) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 8e06836b102..e3331cdc763 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -15,6 +15,17 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) +ATTR_TO_HA = { + 'mileage': ['mdi:speedometer', 'km'], + 'remaining_range_total': ['mdi:ruler', 'km'], + 'remaining_range_electric': ['mdi:ruler', 'km'], + 'remaining_range_fuel': ['mdi:ruler', 'km'], + 'max_range_electric': ['mdi:ruler', 'km'], + 'remaining_fuel': ['mdi:gas-station', 'l'], + 'charging_time_remaining': ['mdi:update', 'h'], + 'charging_status': ['mdi:battery-charging', None] +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -68,22 +79,12 @@ class BMWConnectedDriveSensor(Entity): charging_state = vehicle_state.charging_status in \ [ChargingState.CHARGING] - if self._attribute == 'mileage': - return 'mdi:speedometer' - elif self._attribute in ( - 'remaining_range_total', 'remaining_range_electric', - 'remaining_range_fuel', 'max_range_electric'): - return 'mdi:ruler' - elif self._attribute == 'remaining_fuel': - return 'mdi:gas-station' - elif self._attribute == 'charging_time_remaining': - return 'mdi:update' - elif self._attribute == 'charging_status': - return 'mdi:battery-charging' - elif self._attribute == 'charging_level_hv': + if self._attribute == 'charging_level_hv': return icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state) + icon, _ = ATTR_TO_HA.get(self._attribute, [None, None]) + return icon @property def state(self): @@ -97,17 +98,8 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - if self._attribute in ( - 'mileage', 'remaining_range_total', 'remaining_range_electric', - 'remaining_range_fuel', 'max_range_electric'): - return 'km' - elif self._attribute == 'remaining_fuel': - return 'l' - elif self._attribute == 'charging_time_remaining': - return 'h' - elif self._attribute == 'charging_level_hv': - return '%' - return None + _, unit = ATTR_TO_HA.get(self._attribute, [None, None]) + return unit @property def device_state_attributes(self): From df69680d2484c388b0a9ef1f722746afd7f86eb3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 May 2018 14:47:46 -0400 Subject: [PATCH 727/924] Don't add a url to built-in panels (#14456) * Don't add a url to built-in panels * Add url_path back * Lint * Frontend bump to 20180515.0 * Fix tests --- homeassistant/components/frontend/__init__.py | 71 ++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/test_frontend.py | 2 +- tests/components/test_panel_iframe.py | 7 -- 5 files changed, 31 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f60d095a682..68783a837cb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180510.1'] +REQUIREMENTS = ['home-assistant-frontend==20180515.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -147,21 +147,6 @@ class AbstractPanel: 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), index_view.get) - def to_response(self, hass, request): - """Panel as dictionary.""" - result = { - 'component_name': self.component_name, - 'icon': self.sidebar_icon, - 'title': self.sidebar_title, - 'url_path': self.frontend_url_path, - 'config': self.config, - } - if _is_latest(hass.data[DATA_JS_VERSION], request): - result['url'] = self.webcomponent_url_latest - else: - result['url'] = self.webcomponent_url_es5 - return result - class BuiltInPanel(AbstractPanel): """Panel that is part of hass_frontend.""" @@ -175,30 +160,15 @@ class BuiltInPanel(AbstractPanel): self.frontend_url_path = frontend_url_path or component_name self.config = config - @asyncio.coroutine - def async_finalize(self, hass, frontend_repository_path): - """Finalize this panel for usage. - - If frontend_repository_path is set, will be prepended to path of - built-in components. - """ - if frontend_repository_path is None: - import hass_frontend - import hass_frontend_es5 - - self.webcomponent_url_latest = \ - '/frontend_latest/panels/ha-panel-{}-{}.html'.format( - self.component_name, - hass_frontend.FINGERPRINTS[self.component_name]) - self.webcomponent_url_es5 = \ - '/frontend_es5/panels/ha-panel-{}-{}.html'.format( - self.component_name, - hass_frontend_es5.FINGERPRINTS[self.component_name]) - else: - # Dev mode - self.webcomponent_url_es5 = self.webcomponent_url_latest = \ - '/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format( - self.component_name, self.component_name) + def to_response(self, hass, request): + """Panel as dictionary.""" + return { + 'component_name': self.component_name, + 'icon': self.sidebar_icon, + 'title': self.sidebar_title, + 'config': self.config, + 'url_path': self.frontend_url_path, + } class ExternalPanel(AbstractPanel): @@ -244,6 +214,21 @@ class ExternalPanel(AbstractPanel): frontend_repository_path is None) self.REGISTERED_COMPONENTS.add(self.component_name) + def to_response(self, hass, request): + """Panel as dictionary.""" + result = { + 'component_name': self.component_name, + 'icon': self.sidebar_icon, + 'title': self.sidebar_title, + 'url_path': self.frontend_url_path, + 'config': self.config, + } + if _is_latest(hass.data[DATA_JS_VERSION], request): + result['url'] = self.webcomponent_url_latest + else: + result['url'] = self.webcomponent_url_es5 + return result + @bind_hass @asyncio.coroutine @@ -365,10 +350,10 @@ def async_setup(hass, config): index_view = IndexView(repo_path, js_version, client) hass.http.register_view(index_view) - @asyncio.coroutine - def finalize_panel(panel): + async def finalize_panel(panel): """Finalize setup of a panel.""" - yield from panel.async_finalize(hass, repo_path) + if hasattr(panel, 'async_finalize'): + await panel.async_finalize(hass, repo_path) panel.async_register_index_routes(hass.http.app.router, index_view) yield from asyncio.wait([ diff --git a/requirements_all.txt b/requirements_all.txt index a3f8aa13258..2aad7805b02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180510.1 +home-assistant-frontend==20180515.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3e2e16dc57..55f62f062fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180510.1 +home-assistant-frontend==20180515.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 973544495d7..657497b868b 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -57,7 +57,7 @@ def test_frontend_and_static(mock_http_client): # Test we can retrieve frontend.js frontendjs = re.search( - r'(?P\/frontend_es5\/frontend-[A-Za-z0-9]{32}.html)', text) + r'(?P\/frontend_es5\/app-[A-Za-z0-9]{32}.js)', text) assert frontendjs is not None resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 91a07511787..214eda04ad8 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -1,6 +1,5 @@ """The tests for the panel_iframe component.""" import unittest -from unittest.mock import patch from homeassistant import setup from homeassistant.components import frontend @@ -33,8 +32,6 @@ class TestPanelIframe(unittest.TestCase): 'panel_iframe': conf }) - @patch.dict('hass_frontend_es5.FINGERPRINTS', - {'iframe': 'md5md5'}) def test_correct_config(self): """Test correct config.""" assert setup.setup_component( @@ -70,7 +67,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', 'title': 'Router', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'router' } @@ -79,7 +75,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', 'title': 'Weather', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'weather', } @@ -88,7 +83,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': '/api'}, 'icon': 'mdi:weather', 'title': 'Api', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'api', } @@ -97,6 +91,5 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'ftp://some/ftp'}, 'icon': 'mdi:weather', 'title': 'FTP', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'ftp', } From 852ce9f990ca5c0cd16a8bd4abbfc3c88873e468 Mon Sep 17 00:00:00 2001 From: nordlead2005 Date: Tue, 15 May 2018 15:26:41 -0400 Subject: [PATCH 728/924] Added temperature (apparent) high/low, deprecated max/min (#12233) --- homeassistant/components/sensor/darksky.py | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index ac09de9c699..e75f36d59f7 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -33,6 +33,11 @@ DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' +DEPRECATED_SENSOR_TYPES = {'apparent_temperature_max', + 'apparent_temperature_min', + 'temperature_max', + 'temperature_min'} + # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { @@ -90,16 +95,28 @@ SENSOR_TYPES = { '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], + 'apparent_temperature_high': ["Daytime High Apparent Temperature", + '°C', '°F', '°C', '°C', '°C', + 'mdi:thermometer', ['daily']], 'apparent_temperature_min': ['Daily Low Apparent Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], + 'apparent_temperature_low': ['Overnight Low Apparent Temperature', + '°C', '°F', '°C', '°C', '°C', + 'mdi:thermometer', ['daily']], 'temperature_max': ['Daily High Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['daily']], + 'temperature_high': ['Daytime High Temperature', + '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', + ['daily']], 'temperature_min': ['Daily Low Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['daily']], + 'temperature_low': ['Overnight Low Temperature', + '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', + ['daily']], 'precip_intensity_max': ['Daily Max Precip Intensity', 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', 'mdi:thermometer', @@ -185,6 +202,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): forecast = config.get(CONF_FORECAST) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: + if variable in DEPRECATED_SENSOR_TYPES: + _LOGGER.warning("Monitored condition %s is deprecated.", + variable) sensors.append(DarkSkySensor(forecast_data, variable, name)) if forecast is not None and 'daily' in SENSOR_TYPES[variable][7]: for forecast_day in forecast: @@ -288,9 +308,13 @@ class DarkSkySensor(Entity): elif self.forecast_day > 0 or ( self.type in ['daily_summary', 'temperature_min', + 'temperature_low', 'temperature_max', + 'temperature_high', 'apparent_temperature_min', + 'apparent_temperature_low', 'apparent_temperature_max', + 'apparent_temperature_high', 'precip_intensity_max', 'precip_accumulation']): self.forecast_data.update_daily() From 6ba49e12a261fcd02bcfd4eafaed7e5d2879d857 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 16 May 2018 07:35:43 +0200 Subject: [PATCH 729/924] Improve handling of offline Sonos devices (#14479) --- homeassistant/components/media_player/sonos.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index cc10355abe8..06e5f3befe4 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -682,11 +682,15 @@ class SonosDevice(MediaPlayerDevice): if group: # New group information is pushed coordinator_uid, *slave_uids = group.split(',') - else: + elif self.soco.group: # Use SoCo cache for existing topology coordinator_uid = self.soco.group.coordinator.uid slave_uids = [p.uid for p in self.soco.group.members if p.uid != coordinator_uid] + else: + # Not yet in the cache, this can happen when a speaker boots + coordinator_uid = self.unique_id + slave_uids = [] if self.unique_id == coordinator_uid: sonos_group = [] From 5ff5c73e2bf1918113197a2a7a9ed29fe69bc4d3 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 15 May 2018 23:00:57 -0700 Subject: [PATCH 730/924] "unavailable" Media players should be considered off in Universal player (#14466) The Universal media player inherits the states of the first child player that is not in some sort of "Off" state (including idle.) It was not considering the "unavailable" state to be off. Now it does. --- homeassistant/components/media_player/universal.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index fa4f03f1179..03f847ae40c 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -30,7 +30,8 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, - SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP) + SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + SERVICE_MEDIA_STOP) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_call_from_config @@ -45,7 +46,7 @@ CONF_SERVICE_DATA = 'service_data' ATTR_DATA = 'data' CONF_STATE = 'state' -OFF_STATES = [STATE_IDLE, STATE_OFF] +OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) From 1533a68c064e6389a4bd9efe4b94b425004c7669 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 16 May 2018 03:58:49 -0400 Subject: [PATCH 731/924] Added option to invert aREST pin switch logic for active low relays (#14467) * Added option to invert aREST pin switch logic for active low relays * Fixed line lengths * Changed naming and set optional invert default value. * Fixed line length * Removed default from get --- homeassistant/components/switch/arest.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 6e31694fd2d..fd72d0728a0 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -18,11 +18,13 @@ _LOGGER = logging.getLogger(__name__) CONF_FUNCTIONS = 'functions' CONF_PINS = 'pins' +CONF_INVERT = 'invert' DEFAULT_NAME = 'aREST switch' PIN_FUNCTION_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT, default=False): cv.boolean, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -54,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for pinnum, pin in pins.items(): dev.append(ArestSwitchPin( resource, config.get(CONF_NAME, response.json()[CONF_NAME]), - pin.get(CONF_NAME), pinnum)) + pin.get(CONF_NAME), pinnum, pin.get(CONF_INVERT))) functions = config.get(CONF_FUNCTIONS) for funcname, func in functions.items(): @@ -152,10 +154,11 @@ class ArestSwitchFunction(ArestSwitchBase): class ArestSwitchPin(ArestSwitchBase): """Representation of an aREST switch. Based on digital I/O.""" - def __init__(self, resource, location, name, pin): + def __init__(self, resource, location, name, pin, invert): """Initialize the switch.""" super().__init__(resource, location, name) self._pin = pin + self.invert = invert request = requests.get( '{}/mode/{}/o'.format(self._resource, self._pin), timeout=10) @@ -165,8 +168,11 @@ class ArestSwitchPin(ArestSwitchBase): def turn_on(self, **kwargs): """Turn the device on.""" + turn_on_payload = int(not self.invert) request = requests.get( - '{}/digital/{}/1'.format(self._resource, self._pin), timeout=10) + '{}/digital/{}/{}'.format(self._resource, self._pin, + turn_on_payload), + timeout=10) if request.status_code == 200: self._state = True else: @@ -175,8 +181,11 @@ class ArestSwitchPin(ArestSwitchBase): def turn_off(self, **kwargs): """Turn the device off.""" + turn_off_payload = int(self.invert) request = requests.get( - '{}/digital/{}/0'.format(self._resource, self._pin), timeout=10) + '{}/digital/{}/{}'.format(self._resource, self._pin, + turn_off_payload), + timeout=10) if request.status_code == 200: self._state = False else: @@ -188,7 +197,8 @@ class ArestSwitchPin(ArestSwitchBase): try: request = requests.get( '{}/digital/{}'.format(self._resource, self._pin), timeout=10) - self._state = request.json()['return_value'] != 0 + status_value = int(self.invert) + self._state = request.json()['return_value'] != status_value self._available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) From e20f88c143ecedd18cf502896282f3bd3fb4bd78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 16 May 2018 10:01:48 +0200 Subject: [PATCH 732/924] Use "Returns" consistently to avoid being treated as section (#14448) Otherwise, by side effect, results in error D413 by recent pydocstyle. --- tests/components/test_shell_command.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index 6f993732c38..c7cef78a127 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -19,8 +19,7 @@ def mock_process_creator(error: bool = False) -> asyncio.coroutine: def communicate() -> Tuple[bytes, bytes]: """Mock a coroutine that runs a process when yielded. - Returns: - a tuple of (stdout, stderr). + Returns a tuple of (stdout, stderr). """ return b"I am stdout", b"I am stderr" From 25dcddfeefc6ad98a93ea78d3ab37713a5c00051 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Wed, 16 May 2018 07:15:59 -0400 Subject: [PATCH 733/924] Add HomeKit support for fans (#14351) --- homeassistant/components/homekit/__init__.py | 8 +- homeassistant/components/homekit/const.py | 7 + homeassistant/components/homekit/type_fans.py | 116 ++++++++++++++ .../components/homekit/type_lights.py | 6 +- .../homekit/test_get_accessories.py | 1 + tests/components/homekit/test_type_covers.py | 10 +- tests/components/homekit/test_type_fans.py | 149 ++++++++++++++++++ tests/components/homekit/test_type_lights.py | 6 +- .../homekit/test_type_thermostats.py | 6 +- 9 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/homekit/type_fans.py create mode 100644 tests/components/homekit/test_type_fans.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 028155593fb..41b0791a352 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -115,6 +115,9 @@ def get_accessory(hass, state, aid, config): elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): a_type = 'WindowCoveringBasic' + elif state.domain == 'fan': + a_type = 'Fan' + elif state.domain == 'light': a_type = 'Light' @@ -202,8 +205,9 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_lights, type_locks, type_security_systems, - type_sensors, type_switches, type_thermostats) + type_covers, type_fans, type_lights, type_locks, + type_security_systems, type_sensors, type_switches, + type_thermostats) for state in self.hass.states.all(): self.add_bridge_accessory(state) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ce46e84a2ef..adde13cc030 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -29,6 +29,7 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' +SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity SERV_LEAK_SENSOR = 'LeakSensor' @@ -46,6 +47,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition, PositionState # #### Characteristics #### +CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] @@ -77,9 +79,11 @@ CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' +CHAR_ROTATION_DIRECTION = 'RotationDirection' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' +CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] @@ -88,6 +92,9 @@ CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### +PROP_MAX_VALUE = 'maxValue' +PROP_MIN_VALUE = 'minValue' + PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Class #### diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py new file mode 100644 index 00000000000..a3ea027c07e --- /dev/null +++ b/homeassistant/components/homekit/type_fans.py @@ -0,0 +1,116 @@ +"""Class to hold all light accessories.""" +import logging + +from pyhap.const import CATEGORY_FAN + +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_OSCILLATING, + DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + SERVICE_TURN_OFF, SERVICE_TURN_ON) + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2) + +_LOGGER = logging.getLogger(__name__) + + +@TYPES.register('Fan') +class Fan(HomeAccessory): + """Generate a Fan accessory for a fan entity. + + Currently supports: state, speed, oscillate, direction. + """ + + def __init__(self, *args): + """Initialize a new Light accessory object.""" + super().__init__(*args, category=CATEGORY_FAN) + self._flag = {CHAR_ACTIVE: False, + CHAR_ROTATION_DIRECTION: False, + CHAR_SWING_MODE: False} + self._state = 0 + + self.chars = [] + features = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_DIRECTION: + self.chars.append(CHAR_ROTATION_DIRECTION) + if features & SUPPORT_OSCILLATE: + self.chars.append(CHAR_SWING_MODE) + + serv_fan = self.add_preload_service(SERV_FANV2, self.chars) + self.char_active = serv_fan.configure_char( + CHAR_ACTIVE, value=0, setter_callback=self.set_state) + + if CHAR_ROTATION_DIRECTION in self.chars: + self.char_direction = serv_fan.configure_char( + CHAR_ROTATION_DIRECTION, value=0, + setter_callback=self.set_direction) + + if CHAR_SWING_MODE in self.chars: + self.char_swing = serv_fan.configure_char( + CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._state == value: + return + + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + self._flag[CHAR_ACTIVE] = True + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_direction(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) + self._flag[CHAR_ROTATION_DIRECTION] = True + direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_DIRECTION: direction} + self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) + + def set_oscillating(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value) + self._flag[CHAR_SWING_MODE] = True + oscillating = True if value == 1 else False + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OSCILLATING: oscillating} + self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params) + + def update_state(self, new_state): + """Update fan after state change.""" + # Handle State + state = new_state.state + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ACTIVE] and \ + self.char_active.value != self._state: + self.char_active.set_value(self._state) + self._flag[CHAR_ACTIVE] = False + + # Handle Direction + if CHAR_ROTATION_DIRECTION in self.chars: + direction = new_state.attributes.get(ATTR_DIRECTION) + if not self._flag[CHAR_ROTATION_DIRECTION] and \ + direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): + hk_direction = 1 if direction == DIRECTION_REVERSE else 0 + if self.char_direction.value != hk_direction: + self.char_direction.set_value(hk_direction) + self._flag[CHAR_ROTATION_DIRECTION] = False + + # Handle Oscillating + if CHAR_SWING_MODE in self.chars: + oscillating = new_state.attributes.get(ATTR_OSCILLATING) + if not self._flag[CHAR_SWING_MODE] and \ + oscillating in (True, False): + hk_oscillating = 1 if oscillating else 0 + if self.char_swing.value != hk_oscillating: + self.char_swing.set_value(hk_oscillating) + self._flag[CHAR_SWING_MODE] = False diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index d8a205d7026..dae3579a97a 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -12,7 +12,8 @@ from . import TYPES from .accessories import HomeAccessory, debounce from .const import ( SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION, + PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -61,7 +62,8 @@ class Light(HomeAccessory): .attributes.get(ATTR_MAX_MIREDS, 500) self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, - properties={'minValue': min_mireds, 'maxValue': max_mireds}, + properties={PROP_MIN_VALUE: min_mireds, + PROP_MAX_VALUE: max_mireds}, setter_callback=self.set_color_temperature) if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index cdfb858b727..a72f50f6c6f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -37,6 +37,7 @@ def test_customize_options(config, name): @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {}), diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index dc4caeb35a6..7260ae40c1a 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -14,18 +14,18 @@ from tests.components.homekit.test_accessories import patch_debounce @pytest.fixture(scope='module') -def cls(request): +def cls(): """Patch debounce decorator during import of type_covers.""" patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_covers', fromlist=['GarageDoorOpener', 'WindowCovering,', 'WindowCoveringBasic']) - request.addfinalizer(patcher.stop) patcher_tuple = namedtuple('Cls', ['window', 'window_basic', 'garage']) - return patcher_tuple(window=_import.WindowCovering, - window_basic=_import.WindowCoveringBasic, - garage=_import.GarageDoorOpener) + yield patcher_tuple(window=_import.WindowCovering, + window_basic=_import.WindowCoveringBasic, + garage=_import.GarageDoorOpener) + patcher.stop() async def test_garage_door_open_close(hass, cls): diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py new file mode 100644 index 00000000000..fc504cc6cbd --- /dev/null +++ b/tests/components/homekit/test_type_fans.py @@ -0,0 +1,149 @@ +"""Test different accessory types: Fans.""" +from collections import namedtuple + +import pytest + +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_OSCILLATING, + DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + +from tests.common import async_mock_service +from tests.components.homekit.test_accessories import patch_debounce + + +@pytest.fixture(scope='module') +def cls(): + """Patch debounce decorator during import of type_fans.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_fans', + fromlist=['Fan']) + patcher_tuple = namedtuple('Cls', ['fan']) + yield patcher_tuple(fan=_import.Fan) + patcher.stop() + + +async def test_fan_basic(hass, cls): + """Test fan with char state.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.aid == 2 + assert acc.category == 3 # Fan + assert acc.char_active.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + + await hass.async_add_job(acc.char_active.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + + await hass.async_add_job(acc.char_active.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_fan_direction(hass, cls): + """Test fan with direction.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, + ATTR_DIRECTION: DIRECTION_FORWARD}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.char_direction.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_direction.value == 0 + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_DIRECTION: DIRECTION_REVERSE}) + await hass.async_block_till_done() + assert acc.char_direction.value == 1 + + # Set from HomeKit + call_set_direction = async_mock_service(hass, DOMAIN, + SERVICE_SET_DIRECTION) + + await hass.async_add_job(acc.char_direction.client_update_value, 0) + await hass.async_block_till_done() + assert call_set_direction[0] + assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_FORWARD + + await hass.async_add_job(acc.char_direction.client_update_value, 1) + await hass.async_block_till_done() + assert call_set_direction[1] + assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE + + +async def test_fan_oscillate(hass, cls): + """Test fan with oscillate.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.char_swing.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_swing.value == 0 + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_OSCILLATING: True}) + await hass.async_block_till_done() + assert acc.char_swing.value == 1 + + # Set from HomeKit + call_oscillate = async_mock_service(hass, DOMAIN, SERVICE_OSCILLATE) + + await hass.async_add_job(acc.char_swing.client_update_value, 0) + await hass.async_block_till_done() + assert call_oscillate[0] + assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id + assert call_oscillate[0].data[ATTR_OSCILLATING] is False + + await hass.async_add_job(acc.char_swing.client_update_value, 1) + await hass.async_block_till_done() + assert call_oscillate[1] + assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id + assert call_oscillate[1].data[ATTR_OSCILLATING] is True diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index d9602a6e41f..65a526edcc3 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -15,15 +15,15 @@ from tests.components.homekit.test_accessories import patch_debounce @pytest.fixture(scope='module') -def cls(request): +def cls(): """Patch debounce decorator during import of type_lights.""" patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_lights', fromlist=['Light']) - request.addfinalizer(patcher.stop) patcher_tuple = namedtuple('Cls', ['light']) - return patcher_tuple(light=_import.Light) + yield patcher_tuple(light=_import.Light) + patcher.stop() async def test_light_basic(hass, cls): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index ea592bd63dd..bc5b3219cdf 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -16,15 +16,15 @@ from tests.components.homekit.test_accessories import patch_debounce @pytest.fixture(scope='module') -def cls(request): +def cls(): """Patch debounce decorator during import of type_thermostats.""" patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_thermostats', fromlist=['Thermostat']) - request.addfinalizer(patcher.stop) patcher_tuple = namedtuple('Cls', ['thermostat']) - return patcher_tuple(thermostat=_import.Thermostat) + yield patcher_tuple(thermostat=_import.Thermostat) + patcher.stop() async def test_default_thermostat(hass, cls): From 105347311107788bec26e3737287fc52a2715fa0 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Wed, 16 May 2018 05:47:41 -0600 Subject: [PATCH 734/924] Add stdout and stderr to debug output for shell_command (#14465) --- homeassistant/components/shell_command.py | 18 ++++++++--- tests/components/test_shell_command.py | 38 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index ca33666d1f3..10a6c350b7c 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -68,8 +68,9 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: cmd, loop=hass.loop, stdin=None, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) else: # Template used. Break into list and use create_subprocess_exec # (which uses shell=False) for security @@ -80,12 +81,19 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: *shlexed_cmd, loop=hass.loop, stdin=None, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) process = yield from create_process - yield from process.communicate() + stdout_data, stderr_data = yield from process.communicate() + if stdout_data: + _LOGGER.debug("Stdout of command: `%s`, return code: %s:\n%s", + cmd, process.returncode, stdout_data) + if stderr_data: + _LOGGER.debug("Stderr of command: `%s`, return code: %s:\n%s", + cmd, process.returncode, stderr_data) if process.returncode != 0: _LOGGER.exception("Error running command: `%s`, return code: %s", cmd, process.returncode) diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index c7cef78a127..a1acffd62e5 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -148,3 +148,41 @@ class TestShellCommand(unittest.TestCase): self.assertEqual(1, mock_call.call_count) self.assertEqual(1, mock_error.call_count) self.assertFalse(os.path.isfile(path)) + + @patch('homeassistant.components.shell_command._LOGGER.debug') + def test_stdout_captured(self, mock_output): + """Test subprocess that has stdout.""" + test_phrase = "I have output" + self.assertTrue( + setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': "echo {}".format(test_phrase) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.hass.block_till_done() + self.assertEqual(1, mock_output.call_count) + self.assertEqual(test_phrase.encode() + b'\n', + mock_output.call_args_list[0][0][-1]) + + @patch('homeassistant.components.shell_command._LOGGER.debug') + def test_stderr_captured(self, mock_output): + """Test subprocess that has stderr.""" + test_phrase = "I have error" + self.assertTrue( + setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': ">&2 echo {}".format(test_phrase) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.hass.block_till_done() + self.assertEqual(1, mock_output.call_count) + self.assertEqual(test_phrase.encode() + b'\n', + mock_output.call_args_list[0][0][-1]) From 64223cea7216bf0c451e3d3ebc162d1a2a8886ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 May 2018 09:00:00 -0400 Subject: [PATCH 735/924] Update frontend to 20180516.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 68783a837cb..1818ecdd7bc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180515.0'] +REQUIREMENTS = ['home-assistant-frontend==20180516.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 2aad7805b02..a997597cdea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180515.0 +home-assistant-frontend==20180516.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55f62f062fe..5bbe3ff04a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180515.0 +home-assistant-frontend==20180516.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 3e7d4fc902d22bf10c21d8b6a54e57babc046261 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 May 2018 09:39:14 -0400 Subject: [PATCH 736/924] Bump frontend to 20180516.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1818ecdd7bc..3ea8594b2a0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180516.0'] +REQUIREMENTS = ['home-assistant-frontend==20180516.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a997597cdea..1181228eb20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.0 +home-assistant-frontend==20180516.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bbe3ff04a9..c49c7de7bd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.0 +home-assistant-frontend==20180516.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 298d31e42b8aa27dcf4677e37a1b697d1baa0407 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Thu, 17 May 2018 02:45:47 +0200 Subject: [PATCH 737/924] New Sensor FinTS (#14334) --- .coveragerc | 1 + homeassistant/components/sensor/fints.py | 285 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 289 insertions(+) create mode 100644 homeassistant/components/sensor/fints.py diff --git a/.coveragerc b/.coveragerc index 3ecf2411384..d361cf2ddad 100644 --- a/.coveragerc +++ b/.coveragerc @@ -599,6 +599,7 @@ omit = homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fedex.py homeassistant/components/sensor/filesize.py + homeassistant/components/sensor/fints.py homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/folder.py diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py new file mode 100644 index 00000000000..798f74bb654 --- /dev/null +++ b/homeassistant/components/sensor/fints.py @@ -0,0 +1,285 @@ +""" +Read the balance of your bank accounts via FinTS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.fints/ +""" + +from collections import namedtuple +from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['fints==0.2.1'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(hours=4) + +ICON = 'mdi:currency-eur' + +BankCredentials = namedtuple('BankCredentials', 'blz login pin url') + +CONF_BIN = 'bank_identification_number' +CONF_ACCOUNTS = 'accounts' +CONF_HOLDINGS = 'holdings' +CONF_ACCOUNT = 'account' + +ATTR_ACCOUNT = CONF_ACCOUNT +ATTR_BANK = 'bank' +ATTR_ACCOUNT_TYPE = 'account_type' + +SCHEMA_ACCOUNTS = vol.Schema({ + vol.Required(CONF_ACCOUNT): cv.string, + vol.Optional(CONF_NAME, default=None): vol.Any(None, cv.string), +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_BIN): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), + vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the sensors. + + Login to the bank and get a list of existing accounts. Create a + sensor for each account. + """ + credentials = BankCredentials(config[CONF_BIN], config[CONF_USERNAME], + config[CONF_PIN], config[CONF_URL]) + fints_name = config.get(CONF_NAME, config[CONF_BIN]) + + account_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] + for acc in config[CONF_ACCOUNTS]} + + holdings_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] + for acc in config[CONF_HOLDINGS]} + + client = FinTsClient(credentials, fints_name) + balance_accounts, holdings_accounts = client.detect_accounts() + accounts = [] + + for account in balance_accounts: + if config[CONF_ACCOUNTS] and account.iban not in account_config: + _LOGGER.info('skipping account %s for bank %s', + account.iban, fints_name) + continue + + account_name = account_config.get(account.iban) + if not account_name: + account_name = '{} - {}'.format(fints_name, account.iban) + accounts.append(FinTsAccount(client, account, account_name)) + _LOGGER.debug('Creating account %s for bank %s', + account.iban, fints_name) + + for account in holdings_accounts: + if config[CONF_HOLDINGS] and \ + account.accountnumber not in holdings_config: + _LOGGER.info('skipping holdings %s for bank %s', + account.accountnumber, fints_name) + continue + + account_name = holdings_config.get(account.accountnumber) + if not account_name: + account_name = '{} - {}'.format( + fints_name, account.accountnumber) + accounts.append(FinTsHoldingsAccount(client, account, account_name)) + _LOGGER.debug('Creating holdings %s for bank %s', + account.accountnumber, fints_name) + + add_devices(accounts, True) + + +class FinTsClient(object): + """Wrapper around the FinTS3PinTanClient. + + Use this class as Context Manager to get the FinTS3Client object. + """ + + def __init__(self, credentials: BankCredentials, name: str): + """Constructor for class FinTsClient.""" + self._credentials = credentials + self.name = name + + @property + def client(self): + """Get the client object. + + As the fints library is stateless, there is not benefit in caching + the client objects. If that ever changes, consider caching the client + object and also think about potential concurrency problems. + """ + from fints.client import FinTS3PinTanClient + return FinTS3PinTanClient( + self._credentials.blz, self._credentials.login, + self._credentials.pin, self._credentials.url) + + def detect_accounts(self): + """Identify the accounts of the bank.""" + from fints.dialog import FinTSDialogError + balance_accounts = [] + holdings_accounts = [] + for account in self.client.get_sepa_accounts(): + try: + self.client.get_balance(account) + balance_accounts.append(account) + except IndexError: + # account is not a balance account. + pass + except FinTSDialogError: + # account is not a balance account. + pass + try: + self.client.get_holdings(account) + holdings_accounts.append(account) + except FinTSDialogError: + # account is not a holdings account. + pass + + return balance_accounts, holdings_accounts + + +class FinTsAccount(Entity): + """Sensor for a FinTS balanc account. + + A balance account contains an amount of money (=balance). The amount may + also be negative. + """ + + def __init__(self, client: FinTsClient, account, name: str) -> None: + """Constructor for class FinTsAccount.""" + self._client = client # type: FinTsClient + self._account = account + self._name = name # type: str + self._balance = None # type: float + self._currency = None # type: str + + @property + def should_poll(self) -> bool: + """Data needs to be polled from the bank servers.""" + return True + + def update(self) -> None: + """Get the current balance and currency for the account.""" + bank = self._client.client + balance = bank.get_balance(self._account) + self._balance = balance.amount.amount + self._currency = balance.amount.currency + _LOGGER.debug('updated balance of account %s', self.name) + + @property + def name(self) -> str: + """Friendly name of the sensor.""" + return self._name + + @property + def state(self) -> float: + """Return the balance of the account as state.""" + return self._balance + + @property + def unit_of_measurement(self) -> str: + """Use the currency as unit of measurement.""" + return self._currency + + @property + def device_state_attributes(self) -> dict: + """Additional attributes of the sensor.""" + attributes = { + ATTR_ACCOUNT: self._account.iban, + ATTR_ACCOUNT_TYPE: 'balance', + } + if self._client.name: + attributes[ATTR_BANK] = self._client.name + return attributes + + @property + def icon(self) -> str: + """Set the icon for the sensor.""" + return ICON + + +class FinTsHoldingsAccount(Entity): + """Sensor for a FinTS holdings account. + + A holdings account does not contain money but rather some financial + instruments, e.g. stocks. + """ + + def __init__(self, client: FinTsClient, account, name: str) -> None: + """Constructor for class FinTsHoldingsAccount.""" + self._client = client # type: FinTsClient + self._name = name # type: str + self._account = account + self._holdings = [] + self._total = None # type: float + + @property + def should_poll(self) -> bool: + """Data needs to be polled from the bank servers.""" + return True + + def update(self) -> None: + """Get the current holdings for the account.""" + bank = self._client.client + self._holdings = bank.get_holdings(self._account) + self._total = sum(h.total_value for h in self._holdings) + + @property + def state(self) -> float: + """Return total market value as state.""" + return self._total + + @property + def icon(self) -> str: + """Set the icon for the sensor.""" + return ICON + + @property + def device_state_attributes(self) -> dict: + """Additional attributes of the sensor. + + Lists each holding of the account with the current value. + """ + attributes = { + ATTR_ACCOUNT: self._account.accountnumber, + ATTR_ACCOUNT_TYPE: 'holdings', + } + if self._client.name: + attributes[ATTR_BANK] = self._client.name + for holding in self._holdings: + total_name = '{} total'.format(holding.name) + attributes[total_name] = holding.total_value + pieces_name = '{} pieces'.format(holding.name) + attributes[pieces_name] = holding.pieces + price_name = '{} price'.format(holding.name) + attributes[price_name] = holding.market_value + + return attributes + + @property + def name(self) -> str: + """Friendly name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self) -> str: + """Get the unit of measurement. + + Hardcoded to EUR, as the library does not provide the currency for the + holdings. And as FinTS is only used in Germany, most accounts will be + in EUR anyways. + """ + return "EUR" diff --git a/requirements_all.txt b/requirements_all.txt index 1181228eb20..63af0d7b94f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,6 +308,9 @@ fedexdeliverymanager==1.0.6 # homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 +# homeassistant.components.sensor.fints +fints==0.2.1 + # homeassistant.components.sensor.fitbit fitbit==0.3.0 From 144524fbbb61c08474292cfffe0772f8b19e1d89 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 17 May 2018 11:44:01 -0600 Subject: [PATCH 738/924] Update hitron_coda.py (#14506) missed a typo that wasn't caught with testing since I don't have a Rogers router. --- homeassistant/components/device_tracker/hitron_coda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py index c9cd30cdb25..72817ca695c 100644 --- a/homeassistant/components/device_tracker/hitron_coda.py +++ b/homeassistant/components/device_tracker/hitron_coda.py @@ -55,7 +55,7 @@ class HitronCODADeviceScanner(DeviceScanner): if config.get(CONF_TYPE) == "shaw": self._type = 'pwd' else: - self.type = 'pws' + self._type = 'pws' self._userid = None From ed3efc871237f0749335e5f09689292513a5203d Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Thu, 17 May 2018 14:19:05 -0400 Subject: [PATCH 739/924] Konnected component follow up (#14491) * make device_discovered synchronous * small fixes from code review * use dispatcher to update sensor state * update switch state based on response from the device * interpolate entity_id into dispatcher signal * cleanup lint * change coroutine to callback --- .../components/binary_sensor/konnected.py | 22 +++++++--- homeassistant/components/konnected.py | 30 +++++++------ homeassistant/components/switch/konnected.py | 43 +++++++++++-------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py index c7e2b7c84fe..9a16ca5e1ab 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/binary_sensor/konnected.py @@ -4,13 +4,16 @@ Support for wired binary sensors attached to a Konnected device. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.konnected/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) +from homeassistant.components.konnected import ( + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE) from homeassistant.const import ( - CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_STATE) + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID, + ATTR_STATE) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) @@ -23,7 +26,7 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return - data = hass.data[DOMAIN] + data = hass.data[KONNECTED_DOMAIN] device_id = discovery_info['device_id'] sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) for pin_num, pin_data in @@ -43,7 +46,6 @@ class KonnectedBinarySensor(BinarySensorDevice): self._device_class = self._data.get(CONF_TYPE) self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( device_id, PIN_TO_ZONE[pin_num])) - self._data['entity'] = self _LOGGER.debug('Created new Konnected sensor: %s', self._name) @property @@ -66,9 +68,15 @@ class KonnectedBinarySensor(BinarySensorDevice): """Return the device class.""" return self._device_class - @asyncio.coroutine + async def async_added_to_hass(self): + """Store entity_id and register state change callback.""" + self._data[ATTR_ENTITY_ID] = self.entity_id + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id), + self.async_set_state) + + @callback def async_set_state(self, state): """Update the sensor's state.""" self._state = state - self._data[ATTR_STATE] = state self.async_schedule_update_ha_state() diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 8c5578f10e4..70b66f84ae9 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -19,7 +19,8 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, - ATTR_STATE) + ATTR_ENTITY_ID, ATTR_STATE) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv @@ -75,6 +76,7 @@ DEPENDENCIES = ['http', 'discovery'] ENDPOINT_ROOT = '/api/konnected' UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') +SIGNAL_SENSOR_UPDATE = 'konnected.{}.update' async def async_setup(hass, config): @@ -87,19 +89,19 @@ async def async_setup(hass, config): if DOMAIN not in hass.data: hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token} - async def async_device_discovered(service, info): + def device_discovered(service, info): """Call when a Konnected device has been discovered.""" _LOGGER.debug("Discovered a new Konnected device: %s", info) host = info.get(CONF_HOST) port = info.get(CONF_PORT) device = KonnectedDevice(hass, host, port, cfg) - await device.async_setup() + device.setup() discovery.async_listen( hass, SERVICE_KONNECTED, - async_device_discovered) + device_discovered) hass.http.register_view(KonnectedView(access_token)) @@ -121,17 +123,17 @@ class KonnectedDevice(object): self.status = self.client.get_status() _LOGGER.info('Initialized Konnected device %s', self.device_id) - async def async_setup(self): + def setup(self): """Set up a newly discovered Konnected device.""" user_config = self.config() if user_config: _LOGGER.debug('Configuring Konnected device %s', self.device_id) self.save_data() - await self.async_sync_device_config() - await discovery.async_load_platform( + self.sync_device_config() + discovery.load_platform( self.hass, 'binary_sensor', DOMAIN, {'device_id': self.device_id}) - await discovery.async_load_platform( + discovery.load_platform( self.hass, 'switch', DOMAIN, {'device_id': self.device_id}) @@ -225,7 +227,7 @@ class KonnectedDevice(object): def sensor_configuration(self): """Return the configuration map for syncing sensors.""" return [{'pin': p} for p in - self.stored_configuration[CONF_BINARY_SENSORS].keys()] + self.stored_configuration[CONF_BINARY_SENSORS]] def actuator_configuration(self): """Return the configuration map for syncing actuators.""" @@ -235,7 +237,7 @@ class KonnectedDevice(object): for p, data in self.stored_configuration[CONF_SWITCHES].items()] - async def async_sync_device_config(self): + def sync_device_config(self): """Sync the new pin configuration to the Konnected device.""" desired_sensor_configuration = self.sensor_configuration() current_sensor_configuration = [ @@ -306,10 +308,12 @@ class KonnectedView(HomeAssistantView): if pin_data is None: return self.json_message('unregistered sensor/actuator', status_code=HTTP_BAD_REQUEST) - entity = pin_data.get('entity') - if entity is None: + + entity_id = pin_data.get(ATTR_ENTITY_ID) + if entity_id is None: return self.json_message('uninitialized sensor/actuator', status_code=HTTP_INTERNAL_SERVER_ERROR) - await entity.async_set_state(state) + async_dispatcher_send( + hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) return self.json_message('ok') diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index e88f9826678..53c6406b28a 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -5,11 +5,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.konnected/ """ -import asyncio import logging from homeassistant.components.konnected import ( - DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, STATE_LOW, STATE_HIGH) + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, + STATE_LOW, STATE_HIGH) from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import (CONF_DEVICES, CONF_SWITCHES, ATTR_STATE) @@ -24,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return - data = hass.data[DOMAIN] + data = hass.data[KONNECTED_DOMAIN] device_id = discovery_info['device_id'] client = data[CONF_DEVICES][device_id]['client'] switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) @@ -41,12 +41,11 @@ class KonnectedSwitch(ToggleEntity): self._data = data self._device_id = device_id self._pin_num = pin_num - self._state = self._data.get(ATTR_STATE) self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) + self._state = self._boolean_state(self._data.get(ATTR_STATE)) self._name = self._data.get( 'name', 'Konnected {} Actuator {}'.format( device_id, PIN_TO_ZONE[pin_num])) - self._data['entity'] = self self._client = client _LOGGER.debug('Created new switch: %s', self._name) @@ -62,26 +61,34 @@ class KonnectedSwitch(ToggleEntity): def turn_on(self, **kwargs): """Send a command to turn on the switch.""" - self._client.put_device(self._pin_num, - int(self._activation == STATE_HIGH)) - self._set_state(True) + resp = self._client.put_device( + self._pin_num, int(self._activation == STATE_HIGH)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) def turn_off(self, **kwargs): """Send a command to turn off the switch.""" - self._client.put_device(self._pin_num, - int(self._activation == STATE_LOW)) - self._set_state(False) + resp = self._client.put_device( + self._pin_num, int(self._activation == STATE_LOW)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) + + def _boolean_state(self, int_state): + if int_state is None: + return False + if int_state == 0: + return self._activation == STATE_LOW + if int_state == 1: + return self._activation == STATE_HIGH def _set_state(self, state): self._state = state - self._data[ATTR_STATE] = state self.schedule_update_ha_state() _LOGGER.debug('Setting status of %s actuator pin %s to %s', self._device_id, self.name, state) - @asyncio.coroutine - def async_set_state(self, state): - """Update the switch's state.""" - self._state = state - self._data[ATTR_STATE] = state - self.async_schedule_update_ha_state() + async def async_added_to_hass(self): + """Store entity_id.""" + self._data['entity_id'] = self.entity_id From 9afc2634c6cfe29c96d4c0303fa28dc5a8c50070 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 17 May 2018 20:54:25 +0200 Subject: [PATCH 740/924] Adjust LimitlessLED properties for running effects (#14481) --- .../components/light/limitlessled.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index bb84b3a6fed..bd4fece89e3 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -142,10 +142,9 @@ def state(new_state): from limitlessled.pipeline import Pipeline pipeline = Pipeline() transition_time = DEFAULT_TRANSITION - # Stop any repeating pipeline. - if self.repeating: - self.repeating = False + if self._effect == EFFECT_COLORLOOP: self.group.stop() + self._effect = None # Set transition time. if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) @@ -183,11 +182,11 @@ class LimitlessLEDGroup(Light): self.group = group self.config = config - self.repeating = False self._is_on = False self._brightness = None self._temperature = None self._color = None + self._effect = None @asyncio.coroutine def async_added_to_hass(self): @@ -222,6 +221,9 @@ class LimitlessLEDGroup(Light): @property def brightness(self): """Return the brightness property.""" + if self._effect == EFFECT_NIGHT: + return 1 + return self._brightness @property @@ -242,6 +244,9 @@ class LimitlessLEDGroup(Light): @property def hs_color(self): """Return the color property.""" + if self._effect == EFFECT_NIGHT: + return None + return self._color @property @@ -249,6 +254,11 @@ class LimitlessLEDGroup(Light): """Flag supported features.""" return self._supported + @property + def effect(self): + """Return the current effect for this light.""" + return self._effect + @property def effect_list(self): """Return the list of supported effects for this light.""" @@ -270,6 +280,7 @@ class LimitlessLEDGroup(Light): if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT: if EFFECT_NIGHT in self._effect_list: pipeline.night_light() + self._effect = EFFECT_NIGHT return pipeline.on() @@ -314,7 +325,7 @@ class LimitlessLEDGroup(Light): if ATTR_EFFECT in kwargs and self._effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: from limitlessled.presets import COLORLOOP - self.repeating = True + self._effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: pipeline.white() From f06a0ba3738da05794a21d8e11f379c32f6ce7c0 Mon Sep 17 00:00:00 2001 From: thelittlefireman Date: Thu, 17 May 2018 23:06:39 +0200 Subject: [PATCH 741/924] Bump locationsharinglib to 2.0.2 (#14359) * Bump locationsharinglib to 2.0.2 * Bump locationsharinglib to 2.0.2 --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 1d0058ed229..7aaf02b0f4c 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==1.2.2'] +REQUIREMENTS = ['locationsharinglib==2.0.2'] CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' diff --git a/requirements_all.txt b/requirements_all.txt index 63af0d7b94f..54cd304b7e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==1.2.2 +locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From 1c3293ac855aa2193149d46c3ba32854d1172d01 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 May 2018 21:29:37 -0400 Subject: [PATCH 742/924] Update frontend to 20180518.0 (#14510) * Update frontend to 20180517.0 * Update requirements * Bump frontend to 20180518.0 --- homeassistant/components/frontend/__init__.py | 47 ++++++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3ea8594b2a0..13c8d826377 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180516.1'] +REQUIREMENTS = ['home-assistant-frontend==20180518.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -301,47 +301,28 @@ def async_setup(hass, config): hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION) if is_dev: - for subpath in ["src", "build-translations", "build-temp", "build", - "hass_frontend", "bower_components", "panels", - "hassio"]: - hass.http.register_static_path( - "/home-assistant-polymer/{}".format(subpath), - os.path.join(repo_path, subpath), - False) - - hass.http.register_static_path( - "/static/translations", - os.path.join(repo_path, "build-translations/output"), False) - sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js") - sw_path_latest = os.path.join(repo_path, "build/service_worker.js") - static_path = os.path.join(repo_path, 'hass_frontend') - frontend_es5_path = os.path.join(repo_path, 'build-es5') - frontend_latest_path = os.path.join(repo_path, 'build') + hass_frontend_path = os.path.join(repo_path, 'hass_frontend') + hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5') else: import hass_frontend import hass_frontend_es5 - sw_path_es5 = os.path.join(hass_frontend_es5.where(), - "service_worker.js") - sw_path_latest = os.path.join(hass_frontend.where(), - "service_worker.js") - # /static points to dir with files that are JS-type agnostic. - # ES5 files are served from /frontend_es5. - # ES6 files are served from /frontend_latest. - static_path = hass_frontend.where() - frontend_es5_path = hass_frontend_es5.where() - frontend_latest_path = static_path + hass_frontend_path = hass_frontend.where() + hass_frontend_es5_path = hass_frontend_es5.where() hass.http.register_static_path( - "/service_worker_es5.js", sw_path_es5, False) + "/service_worker_es5.js", + os.path.join(hass_frontend_es5_path, "service_worker.js"), False) hass.http.register_static_path( - "/service_worker.js", sw_path_latest, False) + "/service_worker.js", + os.path.join(hass_frontend_path, "service_worker.js"), False) hass.http.register_static_path( - "/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev) - hass.http.register_static_path("/static", static_path, not is_dev) + "/robots.txt", + os.path.join(hass_frontend_path, "robots.txt"), False) + hass.http.register_static_path("/static", hass_frontend_path, not is_dev) hass.http.register_static_path( - "/frontend_latest", frontend_latest_path, not is_dev) + "/frontend_latest", hass_frontend_path, not is_dev) hass.http.register_static_path( - "/frontend_es5", frontend_es5_path, not is_dev) + "/frontend_es5", hass_frontend_es5_path, not is_dev) local = hass.config.path('www') if os.path.isdir(local): diff --git a/requirements_all.txt b/requirements_all.txt index 54cd304b7e9..aaffe3068c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.1 +home-assistant-frontend==20180518.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c49c7de7bd1..f12fdd7ce77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.1 +home-assistant-frontend==20180518.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a3777c4ea85ccae7fb9071ec694a2d1eb4a5ec91 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 18 May 2018 15:25:08 +1000 Subject: [PATCH 743/924] Feedreader configurable update interval and max entries (#14487) --- homeassistant/components/feedreader.py | 37 +++++++++------ tests/components/test_feedreader.py | 65 ++++++++++++++++++++------ 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 61fbe9f3171..73ab9e8123c 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -4,7 +4,7 @@ Support for RSS/Atom feeds. For more details about this component, please refer to the documentation at https://home-assistant.io/components/feedreader/ """ -from datetime import datetime +from datetime import datetime, timedelta from logging import getLogger from os.path import exists from threading import Lock @@ -12,8 +12,8 @@ import pickle import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL +from homeassistant.helpers.event import track_time_interval import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['feedparser==5.2.1'] @@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1'] _LOGGER = getLogger(__name__) CONF_URLS = 'urls' +CONF_MAX_ENTRIES = 'max_entries' + +DEFAULT_MAX_ENTRIES = 20 +DEFAULT_SCAN_INTERVAL = timedelta(hours=1) DOMAIN = 'feedreader' EVENT_FEEDREADER = 'feedreader' -MAX_ENTRIES = 20 - CONFIG_SCHEMA = vol.Schema({ DOMAIN: { vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): + cv.positive_int } }, extra=vol.ALLOW_EXTRA) @@ -38,18 +44,23 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Feedreader component.""" urls = config.get(DOMAIN)[CONF_URLS] + scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) + max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) data_file = hass.config.path("{}.pickle".format(DOMAIN)) storage = StoredData(data_file) - feeds = [FeedManager(url, hass, storage) for url in urls] + feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for + url in urls] return len(feeds) > 0 class FeedManager(object): """Abstraction over Feedparser module.""" - def __init__(self, url, hass, storage): - """Initialize the FeedManager object, poll every hour.""" + def __init__(self, url, scan_interval, max_entries, hass, storage): + """Initialize the FeedManager object, poll as per scan interval.""" self._url = url + self._scan_interval = scan_interval + self._max_entries = max_entries self._feed = None self._hass = hass self._firstrun = True @@ -69,8 +80,8 @@ class FeedManager(object): def _init_regular_updates(self, hass): """Schedule regular updates at the top of the clock.""" - track_utc_time_change( - hass, lambda now: self._update(), minute=0, second=0) + track_time_interval(hass, lambda now: self._update(), + self._scan_interval) @property def last_update_successful(self): @@ -116,10 +127,10 @@ class FeedManager(object): def _filter_entries(self): """Filter the entries provided and return the ones to keep.""" - if len(self._feed.entries) > MAX_ENTRIES: + if len(self._feed.entries) > self._max_entries: _LOGGER.debug("Processing only the first %s entries " - "in feed %s", MAX_ENTRIES, self._url) - self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + "in feed %s", self._max_entries, self._url) + self._feed.entries = self._feed.entries[0:self._max_entries] def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py index 2288e21e37a..c20b297017c 100644 --- a/tests/components/test_feedreader.py +++ b/tests/components/test_feedreader.py @@ -1,6 +1,6 @@ """The tests for the feedreader component.""" import time -from datetime import datetime +from datetime import datetime, timedelta import unittest from genericpath import exists @@ -11,12 +11,12 @@ from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import CONF_URLS, FeedManager, \ - StoredData, EVENT_FEEDREADER -from homeassistant.const import EVENT_HOMEASSISTANT_START + StoredData, EVENT_FEEDREADER, DEFAULT_SCAN_INTERVAL, CONF_MAX_ENTRIES, \ + DEFAULT_MAX_ENTRIES +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, \ - load_fixture +from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) @@ -26,6 +26,18 @@ VALID_CONFIG_1 = { CONF_URLS: [URL] } } +VALID_CONFIG_2 = { + feedreader.DOMAIN: { + CONF_URLS: [URL], + CONF_SCAN_INTERVAL: 60 + } +} +VALID_CONFIG_3 = { + feedreader.DOMAIN: { + CONF_URLS: [URL], + CONF_MAX_ENTRIES: 100 + } +} class TestFeedreaderComponent(unittest.TestCase): @@ -45,11 +57,28 @@ class TestFeedreaderComponent(unittest.TestCase): def test_setup_one_feed(self): """Test the general setup of this component.""" - with assert_setup_component(1, 'feedreader'): + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_1)) + track_method.assert_called_once_with(self.hass, mock.ANY, + DEFAULT_SCAN_INTERVAL) - def setup_manager(self, feed_data): + def test_setup_scan_interval(self): + """Test the setup of this component with scan interval.""" + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_2)) + track_method.assert_called_once_with(self.hass, mock.ANY, + timedelta(seconds=60)) + + def test_setup_max_entries(self): + """Test the setup of this component with max entries.""" + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_3)) + + def setup_manager(self, feed_data, max_entries=DEFAULT_MAX_ENTRIES): """Generic test setup method.""" events = [] @@ -67,12 +96,13 @@ class TestFeedreaderComponent(unittest.TestCase): feedreader.DOMAIN)) storage = StoredData(data_file) with patch("homeassistant.components.feedreader." - "track_utc_time_change") as track_method: - manager = FeedManager(feed_data, self.hass, storage) + "track_time_interval") as track_method: + manager = FeedManager(feed_data, DEFAULT_SCAN_INTERVAL, + max_entries, self.hass, storage) # Can't use 'assert_called_once' here because it's not available # in Python 3.5 yet. - track_method.assert_called_once_with(self.hass, mock.ANY, minute=0, - second=0) + track_method.assert_called_once_with(self.hass, mock.ANY, + DEFAULT_SCAN_INTERVAL) # Artificially trigger update. self.hass.bus.fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -116,12 +146,18 @@ class TestFeedreaderComponent(unittest.TestCase): manager3, events3 = self.setup_manager(feed_data3) assert len(events3) == 0 - def test_feed_max_length(self): - """Test long feed beyond the 20 entry limit.""" + def test_feed_default_max_length(self): + """Test long feed beyond the default 20 entry limit.""" feed_data = load_fixture('feedreader2.xml') manager, events = self.setup_manager(feed_data) assert len(events) == 20 + def test_feed_max_length(self): + """Test long feed beyond a configured 5 entry limit.""" + feed_data = load_fixture('feedreader2.xml') + manager, events = self.setup_manager(feed_data, max_entries=5) + assert len(events) == 5 + def test_feed_without_publication_date(self): """Test simple feed with entry without publication date.""" feed_data = load_fixture('feedreader3.xml') @@ -141,7 +177,8 @@ class TestFeedreaderComponent(unittest.TestCase): data_file = self.hass.config.path("{}.pickle".format( feedreader.DOMAIN)) storage = StoredData(data_file) - manager = FeedManager("FEED DATA", self.hass, storage) + manager = FeedManager("FEED DATA", DEFAULT_SCAN_INTERVAL, + DEFAULT_MAX_ENTRIES, self.hass, storage) # Artificially trigger update. self.hass.bus.fire(EVENT_HOMEASSISTANT_START) # Collect events. From 97076aa3fd9d72e526a0f651dd647e4a61eb551e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 18 May 2018 06:48:16 +0100 Subject: [PATCH 744/924] Fix probability_threshold in binary_sensor.bayesian (#14512) (Closes: #14362) --- .../components/binary_sensor/bayesian.py | 2 +- .../components/binary_sensor/test_bayesian.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index f3dbc912ade..72110eb50c9 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice): @asyncio.coroutine def async_update(self): """Get the latest data and update the states.""" - self._deviation = bool(self.probability > self._probability_threshold) + self._deviation = bool(self.probability >= self._probability_threshold) diff --git a/tests/components/binary_sensor/test_bayesian.py b/tests/components/binary_sensor/test_bayesian.py index 3b403c3702f..c3242e09e78 100644 --- a/tests/components/binary_sensor/test_bayesian.py +++ b/tests/components/binary_sensor/test_bayesian.py @@ -154,6 +154,37 @@ class TestBayesianBinarySensor(unittest.TestCase): assert state.state == 'off' + def test_threshold(self): + """Test sensor on probabilty threshold limits.""" + config = { + 'binary_sensor': { + 'name': + 'Test_Binary', + 'platform': + 'bayesian', + 'observations': [{ + 'platform': 'state', + 'entity_id': 'sensor.test_monitored', + 'to_state': 'on', + 'prob_given_true': 1.0, + }], + 'prior': + 0.5, + 'probability_threshold': + 1.0, + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 'on') + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_binary') + self.assertAlmostEqual(1.0, state.attributes.get('probability')) + + assert state.state == 'on' + def test_multiple_observations(self): """Test sensor with multiple observations of same entity.""" config = { From 909f2448cae6d6e3da7ac510b3088ad4b8039a1e Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Fri, 18 May 2018 01:50:57 -0500 Subject: [PATCH 745/924] Flux bug fix (#14476) * Simplify conditionals. * Send white_value on service call. * Remove extra blank line * Further simplification of conditionals * Requested changes * Do not call getRgb if not needed * Update log message --- homeassistant/components/light/flux_led.py | 33 +++++++++++++--------- homeassistant/components/switch/flux.py | 3 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 6c7f2e98e37..fc85e05238f 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -222,27 +222,34 @@ class FluxLight(Light): effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) - # color change only - if rgb is not None: - self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) + # Show warning if effect set with rgb, brightness, or white level + if effect and (brightness or white or rgb): + _LOGGER.warning("RGB, brightness and white level are ignored when" + " an effect is specified for a flux bulb") - # brightness change only - elif brightness is not None: - (red, green, blue) = self._bulb.getRgb() - self._bulb.setRgb(red, green, blue, brightness=brightness) - - # random color effect - elif effect == EFFECT_RANDOM: + # Random color effect + if effect == EFFECT_RANDOM: self._bulb.setRgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + return - # effect selection + # Effect selection elif effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + return - # white change only - elif white is not None: + # Preserve current brightness on color/white level change + if brightness is None: + brightness = self.brightness + + # Preserve color on brightness/white level change + if rgb is None: + rgb = self._bulb.getRgb() + + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + + if white is not None: self._bulb.setWarmWhite255(white) def turn_off(self, **kwargs): diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index e0bfdeee030..21689dcca0f 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -72,7 +72,8 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): turn_on(hass, light, xy_color=[x_val, y_val], brightness=brightness, - transition=transition) + transition=transition, + white_value=brightness) def set_lights_temp(hass, lights, mired, brightness, transition): From cc5edf69e377ed3ccc48f5699089778a0d916f6f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 18 May 2018 09:04:47 +0200 Subject: [PATCH 746/924] Show warning if no locations are shared (fixes #14177) (#14511) --- .../components/device_tracker/google_maps.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 7aaf02b0f4c..3bf0cb0e126 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -12,14 +12,18 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify + +REQUIREMENTS = ['locationsharinglib==2.0.2'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==2.0.2'] +ATTR_ADDRESS = 'address' +ATTR_FULL_NAME = 'full_name' +ATTR_LAST_SEEN = 'last_seen' +ATTR_NICKNAME = 'nickname' CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' @@ -60,19 +64,23 @@ class GoogleMapsScanner(object): self.success_init = True except InvalidUser: - _LOGGER.error('You have specified invalid login credentials') + _LOGGER.error("You have specified invalid login credentials") self.success_init = False def _update_info(self, now=None): for person in self.service.get_all_people(): - dev_id = 'google_maps_{0}'.format(slugify(person.id)) + try: + dev_id = 'google_maps_{0}'.format(person.id) + except TypeError: + _LOGGER.warning("No location(s) shared with this account") + return attrs = { - 'id': person.id, - 'nickname': person.nickname, - 'full_name': person.full_name, - 'last_seen': person.datetime, - 'address': person.address + ATTR_ADDRESS: person.address, + ATTR_FULL_NAME: person.full_name, + ATTR_ID: person.id, + ATTR_LAST_SEEN: person.datetime, + ATTR_NICKNAME: person.nickname, } self.see( dev_id=dev_id, @@ -80,5 +88,5 @@ class GoogleMapsScanner(object): picture=person.picture_url, source_type=SOURCE_TYPE_GPS, gps_accuracy=person.accuracy, - attributes=attrs + attributes=attrs, ) From 4c328baaa6109a428c4dc0355ed5b77888649f55 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 May 2018 13:52:52 +0200 Subject: [PATCH 747/924] Add code to HomeKit lock (#14524) --- .../components/homekit/type_locks.py | 4 +++ homeassistant/components/homekit/util.py | 2 +- .../homekit/test_get_accessories.py | 2 +- tests/components/homekit/test_type_locks.py | 30 +++++++++++++++++-- tests/components/homekit/test_util.py | 7 +++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index b08ac5930bd..309f3072768 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -5,6 +5,7 @@ from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) +from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory @@ -32,6 +33,7 @@ class Lock(HomeAccessory): def __init__(self, *args): """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) + self._code = self.config.get(ATTR_CODE) self.flag_target_state = False serv_lock_mechanism = self.add_preload_service(SERV_LOCK) @@ -51,6 +53,8 @@ class Lock(HomeAccessory): service = STATE_TO_SERVICE[hass_value] params = {ATTR_ENTITY_ID: self.entity_id} + if self._code: + params[ATTR_CODE] = self._code self.hass.services.call('lock', service, params) def update_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 5ddef534202..5d86dbc4612 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -30,7 +30,7 @@ def validate_entity_config(values): domain, _ = split_entity_id(entity) - if domain == 'alarm_control_panel': + if domain in ('alarm_control_panel', 'lock'): code = config.get(ATTR_CODE) params[ATTR_CODE] = cv.string(code) if code else None diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index a72f50f6c6f..0ffc1ae4767 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -39,7 +39,7 @@ def test_customize_options(config, name): @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), - ('Lock', 'lock.test', 'locked', {}, {}), + ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 984d032a1d9..3b8cde47fcb 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,19 +1,23 @@ """Test different accessory types: Locks.""" +import pytest + from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) from tests.common import async_mock_service async def test_lock_unlock(hass): """Test if accessory and HA are updated accordingly.""" + code = '1234' + config = {ATTR_CODE: code} entity_id = 'lock.kitchen_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, None) + acc = Lock(hass, 'Lock', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -50,10 +54,32 @@ async def test_lock_unlock(hass): await hass.async_block_till_done() assert call_lock assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert call_lock[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 1 await hass.async_add_job(acc.char_target_state.client_update_value, 0) await hass.async_block_till_done() assert call_unlock assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id + assert call_unlock[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 0 + + +@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) +async def test_no_code(hass, config): + """Test accessory if lock doesn't require a code.""" + entity_id = 'lock.kitchen_door' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Lock(hass, 'Lock', entity_id, 2, config) + + # Set from HomeKit + call_lock = async_mock_service(hass, DOMAIN, 'lock') + + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_lock + assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert ATTR_CODE not in call_lock[0].data + assert acc.char_target_state.value == 1 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0b3a5475f7e..f3ce35ee06b 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -30,9 +30,16 @@ def test_validate_entity_config(): assert vec({}) == {} assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ {'demo.test': {CONF_NAME: 'Name'}} + + assert vec({'alarm_control_panel.demo': {}}) == \ + {'alarm_control_panel.demo': {ATTR_CODE: None}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} + assert vec({'lock.demo': {}}) == {'lock.demo': {ATTR_CODE: None}} + assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ + {'lock.demo': {ATTR_CODE: '1234'}} + def test_convert_to_float(): """Test convert_to_float method.""" From e929f45ab88681cd3b0f9bcece84759c38fc0624 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 18 May 2018 15:42:41 +0200 Subject: [PATCH 748/924] Set pytz to >=2018.04 (#14520) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f6666c829e0..7bc4fe5761a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ requests==2.18.4 pyyaml>=3.11,<4 -pytz>=2017.02 +pytz>=2018.04 pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 diff --git a/requirements_all.txt b/requirements_all.txt index aaffe3068c2..6797be02b7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,7 +1,7 @@ # Home Assistant core requests==2.18.4 pyyaml>=3.11,<4 -pytz>=2017.02 +pytz>=2018.04 pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 diff --git a/setup.py b/setup.py index 8a68617afd9..6875230b7ab 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'requests==2.18.4', 'pyyaml>=3.11,<4', - 'pytz>=2017.02', + 'pytz>=2018.04', 'pip>=8.0.3', 'jinja2>=2.10', 'voluptuous==0.11.1', From d36996c8f057b6267bbda6e0e9f14c5e27117b1a Mon Sep 17 00:00:00 2001 From: hanzoh Date: Fri, 18 May 2018 16:20:30 +0200 Subject: [PATCH 749/924] Add Homematic IP RotaryHandleSensor support (#14522) * Add Homematic IP RotaryHandleSensor support HmIP-SRH was in the RotaryHandleSensor class and threw errors that LOWBAT and ERROR could not be found (they are LOW_BAT and SABOTAGE). * Revert REQUIREMENTS change --- homeassistant/components/homematic/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index aa19875d43a..e0f0fafe5b5 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -71,7 +71,7 @@ HM_DEVICE_TYPES = { 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', - 'IPWeatherSensor'], + 'IPWeatherSensor', 'RotaryHandleSensorIP'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -98,6 +98,7 @@ HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], + 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], 'BATTERY_STATE': ['battery', {}], From 12e76ef7c159caca8356bfc62762b92d3f38d38f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 May 2018 16:32:57 +0200 Subject: [PATCH 750/924] Update HAP-python to 2.1.0 (#14528) --- homeassistant/components/homekit/__init__.py | 9 ++-- .../components/homekit/accessories.py | 25 ++++++----- homeassistant/components/homekit/util.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_accessories.py | 43 ++++++++++--------- tests/components/homekit/test_homekit.py | 11 +++-- tests/components/homekit/test_util.py | 8 ++-- 8 files changed, 56 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 41b0791a352..ce5f30d7bf2 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,7 +29,7 @@ from .util import show_setup_message, validate_entity_config TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.0.0'] +REQUIREMENTS = ['HAP-python==2.1.0'] # #### Driver Status #### STATUS_READY = 0 @@ -185,7 +185,8 @@ class HomeKit(): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) + self.driver = HomeDriver(self.hass, self.bridge, port=self._port, + address=ip_addr, persist_file=path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -213,8 +214,8 @@ class HomeKit(): self.add_bridge_accessory(state) self.bridge.set_driver(self.driver) - if not self.bridge.paired: - show_setup_message(self.hass, self.bridge) + if not self.driver.state.paired: + show_setup_message(self.hass, self.driver.state.pincode) _LOGGER.debug('Driver start') self.hass.add_job(self.driver.start) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 7ec1fb542c9..ff835659221 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -115,20 +115,23 @@ class HomeBridge(Bridge): """Prevent print of pyhap setup message to terminal.""" pass - def add_paired_client(self, client_uuid, client_public): - """Override super function to dismiss setup message if paired.""" - super().add_paired_client(client_uuid, client_public) - dismiss_setup_message(self.hass) - - def remove_paired_client(self, client_uuid): - """Override super function to show setup message if unpaired.""" - super().remove_paired_client(client_uuid) - show_setup_message(self.hass, self) - class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, *args, **kwargs): + def __init__(self, hass, *args, **kwargs): """Initialize a AccessoryDriver object.""" super().__init__(*args, **kwargs) + self.hass = hass + + def pair(self, client_uuid, client_public): + """Override super function to dismiss setup message if paired.""" + value = super().pair(client_uuid, client_public) + if value: + dismiss_setup_message(self.hass) + return value + + def unpair(self, client_uuid): + """Override super function to show setup message if unpaired.""" + super().unpair(client_uuid) + show_setup_message(self.hass, self.state.pincode) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 5d86dbc4612..447257f9e8f 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -38,9 +38,9 @@ def validate_entity_config(values): return entities -def show_setup_message(hass, bridge): +def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" - pin = bridge.pincode.decode() + pin = pincode.decode() _LOGGER.info('Pincode: %s', pin) message = 'To setup Home Assistant in the Home App, enter the ' \ 'following code:\n### {}'.format(pin) diff --git a/requirements_all.txt b/requirements_all.txt index 6797be02b7f..ffcbc6001cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.0.0 +HAP-python==2.1.0 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f12fdd7ce77..4baab1c79e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5 # homeassistant.components.homekit -HAP-python==2.0.0 +HAP-python==2.1.0 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index f12b80632b6..1b06e245734 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -116,26 +116,6 @@ def test_home_bridge(): # setup_message bridge.setup_message() - # add_paired_client - with patch('pyhap.accessory.Accessory.add_paired_client') \ - as mock_add_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'dismiss_setup_message') as mock_dissmiss_msg: - bridge.add_paired_client('client_uuid', 'client_public') - - mock_add_paired_client.assert_called_with('client_uuid', 'client_public') - mock_dissmiss_msg.assert_called_with('hass') - - # remove_paired_client - with patch('pyhap.accessory.Accessory.remove_paired_client') \ - as mock_remove_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'show_setup_message') as mock_show_msg: - bridge.remove_paired_client('client_uuid') - - mock_remove_paired_client.assert_called_with('client_uuid') - mock_show_msg.assert_called_with('hass', bridge) - def test_home_driver(): """Test HomeDriver class.""" @@ -143,9 +123,30 @@ def test_home_driver(): ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' + pin = b'123-45-678' with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - HomeDriver(bridge, ip_address, port, path) + driver = HomeDriver('hass', bridge, ip_address, port, path) mock_driver.assert_called_with(bridge, ip_address, port, path) + driver.state = Mock(pincode=pin) + + # pair + with patch('pyhap.accessory_driver.AccessoryDriver.pair') as mock_pair, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + driver.pair('client_uuid', 'client_public') + + mock_pair.assert_called_with('client_uuid', 'client_public') + mock_dissmiss_msg.assert_called_with('hass') + + # unpair + with patch('pyhap.accessory_driver.AccessoryDriver.unpair') \ + as mock_unpair, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + driver.unpair('client_uuid') + + mock_unpair.assert_called_with('client_uuid') + mock_show_msg.assert_called_with('hass', pin) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 23f117b15a0..b22a7f63cda 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -107,7 +107,8 @@ async def test_homekit_setup(hass): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path) + hass, homekit.bridge, port=DEFAULT_PORT, + address=IP_ADDRESS, persist_file=path) # Test if stop listener is setup assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 @@ -119,7 +120,8 @@ async def test_homekit_setup_ip_address(hass): with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: await hass.async_add_job(homekit.setup) - mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) + mock_driver.assert_called_with( + hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) async def test_homekit_add_accessory(hass): @@ -167,9 +169,10 @@ async def test_homekit_entity_filter(hass): async def test_homekit_start(hass, debounce_patcher): """Test HomeKit start method.""" + pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) homekit.bridge = HomeBridge(hass) - homekit.driver = Mock() + homekit.driver = Mock(state=Mock(paired=False, pincode=pin)) hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] @@ -180,7 +183,7 @@ async def test_homekit_start(hass, debounce_patcher): await hass.async_add_job(homekit.start) mock_add_acc.assert_called_with(state) - mock_setup_msg.assert_called_with(hass, homekit.bridge) + mock_setup_msg.assert_called_with(hass, pin) assert homekit.driver.start.called is True assert homekit.status == STATUS_RUNNING diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f3ce35ee06b..42f81387960 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,7 +2,6 @@ import pytest import voluptuous as vol -from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( show_setup_message, dismiss_setup_message, convert_to_float, @@ -10,7 +9,7 @@ from homeassistant.components.homekit.util import ( from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - DOMAIN, ATTR_NOTIFICATION_ID) + DOMAIN, ATTR_MESSAGE, ATTR_NOTIFICATION_ID) from homeassistant.const import ( ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) @@ -74,16 +73,17 @@ def test_density_to_air_quality(): async def test_show_setup_msg(hass): """Test show setup message as persistence notification.""" - bridge = HomeBridge(hass) + pincode = b'123-45-678' call_create_notification = async_mock_service(hass, DOMAIN, 'create') - await hass.async_add_job(show_setup_message, hass, bridge) + await hass.async_add_job(show_setup_message, hass, pincode) await hass.async_block_till_done() assert call_create_notification assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == \ HOMEKIT_NOTIFY_ID + assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE] async def test_dismiss_setup_msg(hass): From d7640e6ec3c8c35fba648e29c4895d065158b3c1 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Fri, 18 May 2018 08:42:09 -0700 Subject: [PATCH 751/924] Fix some ISY sensors not getting detected as binary sensors (#14497) Sensors that were defined via sensor_string were not getting properly identified as binary sensors when they had a uom defining them as binary (the other three methods of detecting binary sensors worked though.) --- homeassistant/components/isy994.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 48a9499d1a9..ecabcd36a85 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -202,7 +202,7 @@ def _check_for_uom_id(hass: HomeAssistant, node, node_uom = set(map(str.lower, node.uom)) if uom_list: - if node_uom.intersection(NODE_FILTERS[single_domain]['uom']): + if node_uom.intersection(uom_list): hass.data[ISY994_NODES][single_domain].append(node) return True else: From 25970027c6c294978cd7b312d4140ed978d6e552 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 18 May 2018 13:37:43 -0400 Subject: [PATCH 752/924] Update mychevy to 0.4.0 (#14372) After 2 months of being offline, the my.chevy website seems to be working again. Some data structures changed in the mean time. The new library will handle multiple cars. This involves a breaking change in slug urls for devices where these now include the car make, model, and year in them. Discovery has to be delayed until after the initial site login to get the car metadata. --- .../components/binary_sensor/mychevy.py | 20 ++++++++---- homeassistant/components/mychevy.py | 25 +++++++++++---- homeassistant/components/sensor/mychevy.py | 32 ++++++++++++------- requirements_all.txt | 2 +- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py index a89395ed86f..905e60c34d9 100644 --- a/homeassistant/components/binary_sensor/mychevy.py +++ b/homeassistant/components/binary_sensor/mychevy.py @@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = [] hub = hass.data[MYCHEVY_DOMAIN] for sconfig in SENSORS: - sensors.append(EVBinarySensor(hub, sconfig)) + for car in hub.cars: + sensors.append(EVBinarySensor(hub, sconfig, car.vid)) async_add_devices(sensors) @@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice): """ - def __init__(self, connection, config): + def __init__(self, connection, config, car_vid): """Initialize sensor with car connection.""" self._conn = connection self._name = config.name self._attr = config.attr self._type = config.device_class self._is_on = None - + self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + '{}_{}_{}'.format(MYCHEVY_DOMAIN, + slugify(self._car.name), + slugify(self._name))) @property def name(self): @@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice): """Return if on.""" return self._is_on + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" @@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice): @callback def async_update_callback(self): """Update state.""" - if self._conn.car is not None: - self._is_on = getattr(self._conn.car, self._attr, None) + if self._car is not None: + self._is_on = getattr(self._car, self._attr, None) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy.py index 678cdf10c56..3531c6b4919 100644 --- a/homeassistant/components/mychevy.py +++ b/homeassistant/components/mychevy.py @@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ["mychevy==0.1.1"] +REQUIREMENTS = ["mychevy==0.4.0"] DOMAIN = 'mychevy' UPDATE_TOPIC = DOMAIN @@ -73,9 +73,6 @@ def setup(hass, base_config): hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass) hass.data[DOMAIN].start() - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - return True @@ -98,8 +95,9 @@ class MyChevyHub(threading.Thread): super().__init__() self._client = client self.hass = hass - self.car = None + self.cars = [] self.status = None + self.ready = False @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -109,7 +107,22 @@ class MyChevyHub(threading.Thread): (like 2 to 3 minutes long time) """ - self.car = self._client.data() + self._client.login() + self._client.get_cars() + self.cars = self._client.cars + if self.ready is not True: + discovery.load_platform(self.hass, 'sensor', DOMAIN, {}, {}) + discovery.load_platform(self.hass, 'binary_sensor', DOMAIN, {}, {}) + self.ready = True + self.cars = self._client.update_cars() + + def get_car(self, vid): + """Compatibility to work with one car.""" + if self.cars: + for car in self.cars: + if car.vid == vid: + return car + return None def run(self): """Thread run loop.""" diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index bdbffc46ca8..ef7c7ba8608 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -17,14 +17,15 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify -BATTERY_SENSOR = "percent" +BATTERY_SENSOR = "batteryLevel" SENSORS = [ - EVSensorConfig("Mileage", "mileage", "miles", "mdi:speedometer"), - EVSensorConfig("Range", "range", "miles", "mdi:speedometer"), - EVSensorConfig("Charging", "charging"), - EVSensorConfig("Charge Mode", "charge_mode"), - EVSensorConfig("EVCharge", BATTERY_SENSOR, "%", "mdi:battery") + EVSensorConfig("Mileage", "totalMiles", "miles", "mdi:speedometer"), + EVSensorConfig("Electric Range", "electricRange", "miles", + "mdi:speedometer"), + EVSensorConfig("Charged By", "estimatedFullChargeBy"), + EVSensorConfig("Charge Mode", "chargeMode"), + EVSensorConfig("Battery Level", BATTERY_SENSOR, "%", "mdi:battery") ] _LOGGER = logging.getLogger(__name__) @@ -38,7 +39,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hub = hass.data[MYCHEVY_DOMAIN] sensors = [MyChevyStatus()] for sconfig in SENSORS: - sensors.append(EVSensor(hub, sconfig)) + for car in hub.cars: + sensors.append(EVSensor(hub, sconfig, car.vid)) add_devices(sensors) @@ -112,7 +114,7 @@ class EVSensor(Entity): """ - def __init__(self, connection, config): + def __init__(self, connection, config, car_vid): """Initialize sensor with car connection.""" self._conn = connection self._name = config.name @@ -120,9 +122,12 @@ class EVSensor(Entity): self._unit_of_measurement = config.unit_of_measurement self._icon = config.icon self._state = None + self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + '{}_{}_{}'.format(MYCHEVY_DOMAIN, + slugify(self._car.name), + slugify(self._name))) @asyncio.coroutine def async_added_to_hass(self): @@ -130,6 +135,11 @@ class EVSensor(Entity): self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + @property def icon(self): """Return the icon.""" @@ -145,8 +155,8 @@ class EVSensor(Entity): @callback def async_update_callback(self): """Update state.""" - if self._conn.car is not None: - self._state = getattr(self._conn.car, self._attr, None) + if self._car is not None: + self._state = getattr(self._car, self._attr, None) self.async_schedule_update_ha_state() @property diff --git a/requirements_all.txt b/requirements_all.txt index ffcbc6001cc..78efe76dce6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ motorparts==1.0.2 mutagen==1.40.0 # homeassistant.components.mychevy -mychevy==0.1.1 +mychevy==0.4.0 # homeassistant.components.mycroft mycroftapi==2.0 From c1127133eafd8e7439e4700d4881a9fd3ac3fbf4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 00:14:40 +0200 Subject: [PATCH 753/924] Set certifi to >=2018.04.16 (#14536) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7bc4fe5761a..4a7df44ee5e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6.1 -certifi>=2017.4.17 +certifi>=2018.04.16 attrs==18.1.0 # Breaks Python 3.6 and is not needed for our supported Python versions diff --git a/requirements_all.txt b/requirements_all.txt index 78efe76dce6..6cb1cdefe4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6.1 -certifi>=2017.4.17 +certifi>=2018.04.16 attrs==18.1.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index 6875230b7ab..2469f32d77e 100755 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ REQUIRES = [ 'aiohttp==3.1.3', 'async_timeout==2.0.1', 'astral==1.6.1', - 'certifi>=2017.4.17', + 'certifi>=2018.04.16', 'attrs==18.1.0', ] From 8d06469efe21ad0c09d659f926fc7e96fcd5d10c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 May 2018 18:12:25 -0400 Subject: [PATCH 754/924] Bump frontend to 20180518.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 13c8d826377..8cc3c8ea473 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180518.0'] +REQUIREMENTS = ['home-assistant-frontend==20180518.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6cb1cdefe4b..5d71464c08a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.0 +home-assistant-frontend==20180518.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4baab1c79e9..4a0db70f7d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.0 +home-assistant-frontend==20180518.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f2dfc84d52c5308a597cb98efb861e63a194a060 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 May 2018 19:31:16 -0400 Subject: [PATCH 755/924] Version bump to 0.70.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acc30bcd57c..73c5ee00bc6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 54dfe045b211b6c65bac67be01217c8dcb323fdd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:04:00 +0200 Subject: [PATCH 756/924] Upgrade aiohttp to 3.2.1 (#14517) * Upgrade aiohttp to 3.2.1 * Upgrade async_timeout to 3.0.0 * Update the order of the requirements --- homeassistant/package_constraints.txt | 20 ++++++++++---------- requirements_all.txt | 20 ++++++++++---------- setup.py | 20 ++++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4a7df44ee5e..e76dc24d9dd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,15 +1,15 @@ -requests==2.18.4 -pyyaml>=3.11,<4 -pytz>=2018.04 -pip>=8.0.3 -jinja2>=2.10 -voluptuous==0.11.1 -typing>=3,<4 -aiohttp==3.1.3 -async_timeout==2.0.1 +aiohttp==3.2.1 astral==1.6.1 -certifi>=2018.04.16 +async_timeout==3.0.0 attrs==18.1.0 +certifi>=2018.04.16 +jinja2>=2.10 +pip>=8.0.3 +pytz>=2018.04 +pyyaml>=3.11,<4 +requests==2.18.4 +typing>=3,<4 +voluptuous==0.11.1 # Breaks Python 3.6 and is not needed for our supported Python versions enum34==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5d71464c08a..9bcf496df09 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,16 +1,16 @@ # Home Assistant core -requests==2.18.4 -pyyaml>=3.11,<4 -pytz>=2018.04 -pip>=8.0.3 -jinja2>=2.10 -voluptuous==0.11.1 -typing>=3,<4 -aiohttp==3.1.3 -async_timeout==2.0.1 +aiohttp==3.2.1 astral==1.6.1 -certifi>=2018.04.16 +async_timeout==3.0.0 attrs==18.1.0 +certifi>=2018.04.16 +jinja2>=2.10 +pip>=8.0.3 +pytz>=2018.04 +pyyaml>=3.11,<4 +requests==2.18.4 +typing>=3,<4 +voluptuous==0.11.1 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 diff --git a/setup.py b/setup.py index 2469f32d77e..4390b980f9e 100755 --- a/setup.py +++ b/setup.py @@ -42,18 +42,18 @@ DOWNLOAD_URL = '{}/archive/{}.zip'.format(GITHUB_URL, hass_const.__version__) PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ - 'requests==2.18.4', - 'pyyaml>=3.11,<4', - 'pytz>=2018.04', - 'pip>=8.0.3', - 'jinja2>=2.10', - 'voluptuous==0.11.1', - 'typing>=3,<4', - 'aiohttp==3.1.3', - 'async_timeout==2.0.1', + 'aiohttp==3.2.1', 'astral==1.6.1', - 'certifi>=2018.04.16', + 'async_timeout==3.0.0', 'attrs==18.1.0', + 'certifi>=2018.04.16', + 'jinja2>=2.10', + 'pip>=8.0.3', + 'pytz>=2018.04', + 'pyyaml>=3.11,<4', + 'requests==2.18.4', + 'typing>=3,<4', + 'voluptuous==0.11.1', ] MIN_PY_VERSION = '.'.join(map(str, hass_const.REQUIRED_PYTHON_VER)) From daf8143d01faf20a72d29c4911057ff2505a0da6 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:04:20 +0200 Subject: [PATCH 757/924] Upgrade youtube_dl to 2018.05.18 (#14519) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 89cc296111b..bef02d7113f 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.05.09'] +REQUIREMENTS = ['youtube_dl==2018.05.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9bcf496df09..37b51bc7fef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1376,7 +1376,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.09 +youtube_dl==2018.05.18 # homeassistant.components.light.zengge zengge==0.2 From 46dc9322a277ad654476f3ce3986f792c6f91944 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:04:42 +0200 Subject: [PATCH 758/924] Upgrade keyring to 12.2.1 (#14521) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 11e337a76b5..e02305b5fbb 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.2.0', 'keyrings.alt==3.1'] +REQUIREMENTS = ['keyring==12.2.1', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 37b51bc7fef..030ab842164 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -462,7 +462,7 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.2.0 +keyring==12.2.1 # homeassistant.scripts.keyring keyrings.alt==3.1 From 8deb4624719c22ec0e6bc6ba3eab305dabb31fe9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 10:05:02 +0200 Subject: [PATCH 759/924] Upgrade restrictedpython to 4.0b4 (#14537) --- homeassistant/components/python_script.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/python_script.py b/homeassistant/components/python_script.py index 1d33740d4a4..bbc6e07f2b0 100644 --- a/homeassistant/components/python_script.py +++ b/homeassistant/components/python_script.py @@ -18,7 +18,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename import homeassistant.util.dt as dt_util -REQUIREMENTS = ['restrictedpython==4.0b3'] +REQUIREMENTS = ['restrictedpython==4.0b4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 030ab842164..f2f9ceff9ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ raincloudy==0.0.4 regenmaschine==0.4.1 # homeassistant.components.python_script -restrictedpython==4.0b3 +restrictedpython==4.0b4 # homeassistant.components.rflink rflink==0.0.37 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a0db70f7d7..da12798b8f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ pyupnp-async==0.1.0.2 pywebpush==1.6.0 # homeassistant.components.python_script -restrictedpython==4.0b3 +restrictedpython==4.0b4 # homeassistant.components.rflink rflink==0.0.37 From aa51bb6cb9df15e432581bd251f3f7b5f6a7cb0b Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Sat, 19 May 2018 09:49:52 +0100 Subject: [PATCH 760/924] Bump pyvera version (improve stability of poll loop). (#14540) --- homeassistant/components/vera.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vera.py b/homeassistant/components/vera.py index 5cc4de0d5ca..ebe92a2dcc2 100644 --- a/homeassistant/components/vera.py +++ b/homeassistant/components/vera.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, CONF_LIGHTS, CONF_EXCLUDE) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyvera==0.2.42'] +REQUIREMENTS = ['pyvera==0.2.43'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f2f9ceff9ec..193deab8a87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1090,7 +1090,7 @@ pyupnp-async==0.1.0.2 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.2.42 +pyvera==0.2.43 # homeassistant.components.switch.vesync pyvesync==0.1.1 From 74f1f08ab51ea96b340ed6592402a9ffcc85e34c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 May 2018 10:44:54 -0400 Subject: [PATCH 761/924] Bump frontend to 20180519.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8cc3c8ea473..d4700e5edd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180518.1'] +REQUIREMENTS = ['home-assistant-frontend==20180519.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 193deab8a87..b22ba13dcb5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da12798b8f7..5d4d91b1037 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From b0e850ba5d55de5d646d286057aeba0d502d1307 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 May 2018 10:44:54 -0400 Subject: [PATCH 762/924] Bump frontend to 20180519.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8cc3c8ea473..d4700e5edd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180518.1'] +REQUIREMENTS = ['home-assistant-frontend==20180519.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 5d71464c08a..924f2297311 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a0db70f7d7..75a50d09a3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 8854efd685950e22afc0030cefa5218554750434 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 May 2018 10:45:18 -0400 Subject: [PATCH 763/924] Version bump to 0.70.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73c5ee00bc6..68f4443d12e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e88fc33eef861490a158a424d3e8d4fae187e964 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 17:14:53 +0200 Subject: [PATCH 764/924] Fix sensor name (fixes #14535) (#14541) --- homeassistant/components/sensor/bom.py | 47 ++++++++++++++++---------- tests/components/sensor/test_bom.py | 26 +++++++------- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index d6764e5e994..5cec528d26a 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -28,6 +28,12 @@ import homeassistant.helpers.config_validation as cv _RESOURCE = 'http://www.bom.gov.au/fwo/{}/{}.{}.json' _LOGGER = logging.getLogger(__name__) +ATTR_LAST_UPDATE = 'last_update' +ATTR_SENSOR_ID = 'sensor_id' +ATTR_STATION_ID = 'station_id' +ATTR_STATION_NAME = 'station_name' +ATTR_ZONE_ID = 'zone_id' + CONF_ATTRIBUTION = "Data provided by the Australian Bureau of Meteorology" CONF_STATION = 'station' CONF_ZONE_ID = 'zone_id' @@ -35,7 +41,6 @@ CONF_WMO_ID = 'wmo_id' MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=35) -# Sensor types are defined like: Name, units SENSOR_TYPES = { 'wmo': ['wmo', None], 'name': ['Station Name', None], @@ -70,7 +75,7 @@ SENSOR_TYPES = { 'weather': ['Weather', None], 'wind_dir': ['Wind Direction', None], 'wind_spd_kmh': ['Wind Speed kmh', 'km/h'], - 'wind_spd_kt': ['Wind Direction kt', 'kt'] + 'wind_spd_kt': ['Wind Speed kt', 'kt'] } @@ -98,6 +103,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BOM sensor.""" station = config.get(CONF_STATION) zone_id, wmo_id = config.get(CONF_ZONE_ID), config.get(CONF_WMO_ID) + if station is not None: if zone_id and wmo_id: _LOGGER.warning( @@ -111,17 +117,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hass.config.config_dir) if station is None: _LOGGER.error("Could not get BOM weather station from lat/lon") - return False + return bom_data = BOMCurrentData(hass, station) + try: bom_data.update() except ValueError as err: - _LOGGER.error("Received error from BOM_Current: %s", err) - return False + _LOGGER.error("Received error from BOM Current: %s", err) + return + add_devices([BOMCurrentSensor(bom_data, variable, config.get(CONF_NAME)) for variable in config[CONF_MONITORED_CONDITIONS]]) - return True class BOMCurrentSensor(Entity): @@ -150,14 +157,17 @@ class BOMCurrentSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the device.""" - attr = {} - attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.bom_data.latest_data['history_product'] - attr['Station Id'] = self.bom_data.latest_data['wmo'] - attr['Station Name'] = self.bom_data.latest_data['name'] - attr['Last Update'] = datetime.datetime.strptime(str( - self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') - attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + attr = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_LAST_UPDATE: datetime.datetime.strptime( + str(self.bom_data.latest_data['local_date_time_full']), + '%Y%m%d%H%M%S'), + ATTR_SENSOR_ID: self._condition, + ATTR_STATION_ID: self.bom_data.latest_data['wmo'], + ATTR_STATION_NAME: self.bom_data.latest_data['name'], + ATTR_ZONE_ID: self.bom_data.latest_data['history_product'], + } + return attr @property @@ -180,8 +190,9 @@ class BOMCurrentData(object): self._data = None def _build_url(self): + """Build the URL for the requests.""" url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) - _LOGGER.info("BOM URL %s", url) + _LOGGER.debug("BOM URL: %s", url) return url @property @@ -200,7 +211,7 @@ class BOMCurrentData(object): for the latest value that is not `-`. Iterators are used in this method to avoid iterating needlessly - iterating through the entire BOM provided dataset + iterating through the entire BOM provided dataset. """ condition_readings = (entry[condition] for entry in self._data) return next((x for x in condition_readings if x != '-'), None) @@ -257,7 +268,7 @@ def _get_bom_stations(): def bom_stations(cache_dir): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config. - Results from internet requests are cached as compressed json, making + Results from internet requests are cached as compressed JSON, making subsequent calls very much faster. """ cache_file = os.path.join(cache_dir, '.bom-stations.json.gz') @@ -277,7 +288,7 @@ def closest_station(lat, lon, cache_dir): stations = bom_stations(cache_dir) def comparable_dist(wmo_id): - """Create a psudeo-distance from lat/lon.""" + """Create a psudeo-distance from latitude/longitude.""" station_lat, station_lon = stations[wmo_id] return (lat - station_lat) ** 2 + (lon - station_lon) ** 2 diff --git a/tests/components/sensor/test_bom.py b/tests/components/sensor/test_bom.py index 06a7089e052..5e5a829662a 100644 --- a/tests/components/sensor/test_bom.py +++ b/tests/components/sensor/test_bom.py @@ -1,16 +1,16 @@ """The tests for the BOM Weather sensor platform.""" +import json import re import unittest -import json -import requests from unittest.mock import patch from urllib.parse import urlparse -from homeassistant.setup import setup_component -from homeassistant.components import sensor - +import requests from tests.common import ( - get_test_home_assistant, assert_setup_component, load_fixture) + assert_setup_component, get_test_home_assistant, load_fixture) + +from homeassistant.components import sensor +from homeassistant.setup import setup_component VALID_CONFIG = { 'platform': 'bom', @@ -89,9 +89,11 @@ class TestBOMWeatherSensor(unittest.TestCase): self.assertTrue(setup_component( self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) - self.assertEqual('Fine', self.hass.states.get( - 'sensor.bom_fake_weather').state) - self.assertEqual('1021.7', self.hass.states.get( - 'sensor.bom_fake_pressure_mb').state) - self.assertEqual('25.0', self.hass.states.get( - 'sensor.bom_fake_feels_like_c').state) + weather = self.hass.states.get('sensor.bom_fake_weather').state + self.assertEqual('Fine', weather) + + pressure = self.hass.states.get('sensor.bom_fake_pressure_mb').state + self.assertEqual('1021.7', pressure) + + feels_like = self.hass.states.get('sensor.bom_fake_feels_like_c').state + self.assertEqual('25.0', feels_like) From c316d5b0b93a51a49750d8df0503df6a34c70189 Mon Sep 17 00:00:00 2001 From: Kerwin Bryant Date: Sun, 20 May 2018 04:36:47 +0800 Subject: [PATCH 765/924] Add support to ignore a xiaomi aqara gateway (#14428) --- homeassistant/components/xiaomi_aqara.py | 4 +++- requirements_all.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 2cbf977443c..27bd496a3f0 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.3'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.4'] _LOGGER = logging.getLogger(__name__) @@ -36,6 +36,7 @@ CONF_DISCOVERY_RETRY = 'discovery_retry' CONF_GATEWAYS = 'gateways' CONF_INTERFACE = 'interface' CONF_KEY = 'key' +CONF_DISABLE = 'disable' DOMAIN = 'xiaomi_aqara' @@ -73,6 +74,7 @@ GATEWAY_CONFIG = vol.Schema({ vol.All(cv.string, vol.Length(min=16, max=16)), vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=9898): cv.port, + vol.Optional(CONF_DISABLE, default=False): cv.boolean, }) diff --git a/requirements_all.txt b/requirements_all.txt index b22ba13dcb5..8b5fb8c48be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.3 +PyXiaomiGateway==0.9.4 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From c8a53c564a9c29e6958791c570bbee2a1837a1f9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 20 May 2018 00:33:52 +0200 Subject: [PATCH 766/924] Wait for future mysensors gateway ready (#14398) * Wait for future mysensors gateway ready * Add an asyncio future that is done when the gateway reports the gateway ready message, I_GATEWAY_READY. * This will make sure that the gateway is ready before home assistant fires the home assistant start event. Automations can now send messages to the gateway when home assistant is started. * Use async timeout to wait max 15 seconds for ready gateway. * Address comments --- homeassistant/components/mysensors.py | 46 +++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 6721669a026..1e7e252bd9d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,13 +12,14 @@ import socket import sys from timeit import default_timer as timer +import async_timeout import voluptuous as vol from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) + ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, + STATE_OFF, STATE_ON) from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -57,9 +58,11 @@ DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = '1.4' DOMAIN = 'mysensors' +GATEWAY_READY_TIMEOUT = 15.0 MQTT_COMPONENT = 'mqtt' MYSENSORS_GATEWAYS = 'mysensors_gateways' MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' PLATFORM = 'platform' SCHEMA = 'schema' SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' @@ -353,12 +356,12 @@ async def async_setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = await setup_gateway( + gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if ready_gateway is not None: - ready_gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(ready_gateway)] = ready_gateway + if gateway is not None: + gateway.nodes_config = gway.get(CONF_NODES) + gateways[id(gateway)] = gateway if not gateways: _LOGGER.error( @@ -395,6 +398,35 @@ async def gw_start(hass, gateway): await gateway.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + if gateway.device == 'mqtt': + # Gatways connected via mqtt doesn't send gateway ready message. + return + gateway_ready = asyncio.Future() + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) + hass.data[gateway_ready_key] = gateway_ready + + try: + with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + await gateway_ready + except asyncio.TimeoutError: + _LOGGER.warning( + "Gateway %s not ready after %s secs so continuing with setup", + gateway.device, GATEWAY_READY_TIMEOUT) + finally: + hass.data.pop(gateway_ready_key, None) + + +@callback +def set_gateway_ready(hass, msg): + """Set asyncio future result if gateway is ready.""" + if (msg.type != msg.gateway.const.MessageType.internal or + msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): + return + gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( + id(msg.gateway))) + if gateway_ready is None or gateway_ready.cancelled(): + return + gateway_ready.set_result(True) def validate_child(gateway, node_id, child): @@ -495,6 +527,8 @@ def gw_callback_factory(hass): _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) + set_gateway_ready(hass, msg) + try: child = msg.gateway.sensors[msg.node_id].children[msg.child_id] except KeyError: From c050eb4100368b272ecbeab06c4ffaf1982f4bcd Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 20 May 2018 09:50:12 +0200 Subject: [PATCH 767/924] Pushed to version 0.7.2 of denonavr (#14551) --- homeassistant/components/media_player/denonavr.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/denonavr.py b/homeassistant/components/media_player/denonavr.py index fe8fc46c24b..74d3c5a0785 100644 --- a/homeassistant/components/media_player/denonavr.py +++ b/homeassistant/components/media_player/denonavr.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_NAME, STATE_ON, CONF_ZONE, CONF_TIMEOUT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['denonavr==0.6.1'] +REQUIREMENTS = ['denonavr==0.7.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8b5fb8c48be..7d4b4bb3f41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -246,7 +246,7 @@ defusedxml==0.5.0 deluge-client==1.4.0 # homeassistant.components.media_player.denonavr -denonavr==0.6.1 +denonavr==0.7.2 # homeassistant.components.media_player.directv directpy==0.2 From c8ad9c4daa7cbc2f596a2a3c00278d6539cd79b3 Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Sun, 20 May 2018 20:53:57 +0200 Subject: [PATCH 768/924] Add auto discovery for nanoleaf aurora lights (#14301) * auto discovery added for nanoleaf aurora lights * changes requested by review * visual indentation * line too long * hide autocreated config --- homeassistant/components/discovery.py | 1 + .../components/light/nanoleaf_aurora.py | 47 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index a24e82da106..69447b81cd4 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -83,6 +83,7 @@ SERVICE_HANDLERS = { 'songpal': ('media_player', 'songpal'), 'kodi': ('media_player', 'kodi'), 'volumio': ('media_player', 'volumio'), + 'nanoleaf_aurora': ('light', 'nanoleaf_aurora'), } OPTIONAL_SERVICE_HANDLERS = { diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 99c07166037..8b0b7c053c8 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -17,6 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.util import color as color_util from homeassistant.util.color import \ color_temperature_mired_to_kelvin as mired_to_kelvin +from homeassistant.util.json import load_json, save_json REQUIREMENTS = ['nanoleaf==0.4.1'] @@ -24,6 +25,10 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Aurora' +DATA_NANOLEAF_AURORA = 'nanoleaf_aurora' + +CONFIG_FILE = '.nanoleaf_aurora.conf' + ICON = 'mdi:triangle-outline' SUPPORT_AURORA = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT | @@ -39,31 +44,59 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Nanoleaf Aurora device.""" import nanoleaf - host = config.get(CONF_HOST) - name = config.get(CONF_NAME) - token = config.get(CONF_TOKEN) + import nanoleaf.setup + if DATA_NANOLEAF_AURORA not in hass.data: + hass.data[DATA_NANOLEAF_AURORA] = dict() + + token = '' + if discovery_info is not None: + host = discovery_info['host'] + name = discovery_info['hostname'] + # if device already exists via config, skip discovery setup + if host in hass.data[DATA_NANOLEAF_AURORA]: + return + _LOGGER.info("Discovered a new Aurora: %s", discovery_info) + conf = load_json(hass.config.path(CONFIG_FILE)) + if conf.get(host, {}).get('token'): + token = conf[host]['token'] + else: + host = config[CONF_HOST] + name = config[CONF_NAME] + token = config[CONF_TOKEN] + + if not token: + token = nanoleaf.setup.generate_auth_token(host) + if not token: + _LOGGER.error("Could not generate the auth token, did you press " + "and hold the power button on %s" + "for 5-7 seconds?", name) + return + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {'token': token} + save_json(hass.config.path(CONFIG_FILE), conf) + aurora_light = nanoleaf.Aurora(host, token) - aurora_light.hass_name = name if aurora_light.on is None: _LOGGER.error( "Could not connect to Nanoleaf Aurora: %s on %s", name, host) return - add_devices([AuroraLight(aurora_light)], True) + hass.data[DATA_NANOLEAF_AURORA][host] = aurora_light + add_devices([AuroraLight(aurora_light, name)], True) class AuroraLight(Light): """Representation of a Nanoleaf Aurora.""" - def __init__(self, light): + def __init__(self, light, name): """Initialize an Aurora light.""" self._brightness = None self._color_temp = None self._effect = None self._effects_list = None self._light = light - self._name = light.hass_name + self._name = name self._hs_color = None self._state = None From 4395217031f41b86ed54e1083998b03448ab9222 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 20 May 2018 19:00:51 -0400 Subject: [PATCH 769/924] zha: Don't poll switch devices (#14560) --- homeassistant/components/binary_sensor/zha.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index d3b31188760..0fd9db19d1a 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -211,6 +211,11 @@ class Switch(zha.Entity, BinarySensorDevice): general.LevelControl.cluster_id: self.LevelListener(self), } + @property + def should_poll(self) -> bool: + """Let zha handle polling.""" + return False + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" From b489519930750bbee1acb1f2d1e98c1ff9241be0 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 20 May 2018 19:01:56 -0400 Subject: [PATCH 770/924] zha: Add metering sensor (#14562) --- homeassistant/components/sensor/zha.py | 22 ++++++++++++++++++++++ homeassistant/components/zha/const.py | 1 + 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 3ca908a679d..abb4c651e78 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -34,6 +34,7 @@ def make_sensor(discovery_info): from zigpy.zcl.clusters.measurement import ( RelativeHumidity, TemperatureMeasurement, PressureMeasurement ) + from zigpy.zcl.clusters.smartenergy import Metering in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) @@ -41,6 +42,8 @@ def make_sensor(discovery_info): sensor = TemperatureSensor(**discovery_info) elif PressureMeasurement.cluster_id in in_clusters: sensor = PressureSensor(**discovery_info) + elif Metering.cluster_id in in_clusters: + sensor = MeteringSensor(**discovery_info) else: sensor = Sensor(**discovery_info) @@ -143,3 +146,22 @@ class PressureSensor(Sensor): return None return round(float(self._state)) + + +class MeteringSensor(Sensor): + """ZHA Metering sensor.""" + + value_attribute = 1024 + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'W' + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state)) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 1c083c3ca93..71f0ea17490 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -48,6 +48,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.smartenergy.Metering: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) From ee7e59fe687cf73163fa18ea02c74626eb6062e2 Mon Sep 17 00:00:00 2001 From: damarco Date: Mon, 21 May 2018 01:14:18 +0200 Subject: [PATCH 771/924] zha: Set default binary_sensor state to false (#14553) --- homeassistant/components/binary_sensor/zha.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 0fd9db19d1a..6931355ca0e 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -203,8 +203,8 @@ class Switch(zha.Entity, BinarySensorDevice): def __init__(self, **kwargs): """Initialize Switch.""" super().__init__(**kwargs) - self._state = True - self._level = 255 + self._state = False + self._level = 0 from zigpy.zcl.clusters import general self._out_listeners = { general.OnOff.cluster_id: self.OnOffListener(self), From 0589379de5462567ba3f8b7edd31d22a5f08f41b Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 May 2018 04:25:53 +0200 Subject: [PATCH 772/924] Homekit style cleanup (#14556) * Style cleanup * Sorted imports * Harmonized service calls * Test improvements * Small update --- homeassistant/components/homekit/__init__.py | 11 ++-- .../components/homekit/accessories.py | 5 +- homeassistant/components/homekit/const.py | 35 +++++------ .../components/homekit/type_covers.py | 25 ++++---- homeassistant/components/homekit/type_fans.py | 13 ++-- .../components/homekit/type_lights.py | 42 ++++++------- .../components/homekit/type_locks.py | 7 +-- .../homekit/type_security_systems.py | 12 ++-- .../components/homekit/type_sensors.py | 38 ++++++------ .../components/homekit/type_switches.py | 4 +- .../components/homekit/type_thermostats.py | 60 ++++++++++--------- tests/components/homekit/common.py | 8 +++ tests/components/homekit/test_accessories.py | 39 ++++++------ .../homekit/test_get_accessories.py | 15 ++--- tests/components/homekit/test_homekit.py | 49 +++++++-------- tests/components/homekit/test_type_covers.py | 4 +- tests/components/homekit/test_type_fans.py | 12 ++-- tests/components/homekit/test_type_lights.py | 6 +- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 10 ++-- tests/components/homekit/test_type_sensors.py | 8 +-- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 8 +-- tests/components/homekit/test_util.py | 8 +-- 24 files changed, 209 insertions(+), 214 deletions(-) create mode 100644 tests/components/homekit/common.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ce5f30d7bf2..202f9694689 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -13,17 +13,18 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, - DEFAULT_AUTO_START, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, + DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, + SERVICE_HOMEKIT_START) from .util import show_setup_message, validate_entity_config TYPES = Registry() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ff835659221..ded4526b008 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -84,20 +84,21 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.entity_id, self.update_state_callback) + @ha_callback def update_state_callback(self, entity_id=None, old_state=None, new_state=None): """Callback from state change listener.""" _LOGGER.debug('New_state: %s', new_state) if new_state is None: return - self.update_state(new_state) + self.hass.async_add_job(self.update_state, new_state) def update_state(self, new_state): """Method called on state change to update HomeKit value. Overridden by accessory types. """ - pass + raise NotImplementedError() class HomeBridge(Bridge): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index adde13cc030..21cad2d9cf7 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,23 +1,23 @@ """Constants used be the HomeKit component.""" -# #### MISC #### +# #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 -# #### CONFIG #### +# #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' CONF_FILTER = 'filter' -# #### CONFIG DEFAULTS #### +# #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 -# #### HOMEKIT COMPONENT SERVICES #### +# #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' -# #### STRING CONSTANTS #### +# #### String Constants #### BRIDGE_MODEL = 'Bridge' BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' @@ -31,10 +31,10 @@ SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity +SERV_HUMIDITY_SENSOR = 'HumiditySensor' SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' -SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' @@ -44,13 +44,12 @@ SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' -CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' @@ -61,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] -CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent +CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' -CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_HUE = 'Hue' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -77,16 +76,16 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' -CHAR_ON = 'On' # boolean +CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' -CHAR_SATURATION = 'Saturation' # percent +CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] +CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' @@ -94,21 +93,17 @@ CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### PROP_MAX_VALUE = 'maxValue' PROP_MIN_VALUE = 'minValue' - PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} -# #### Device Class #### +# #### Device Classes #### DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' -DEVICE_CLASS_HUMIDITY = 'humidity' -DEVICE_CLASS_LIGHT = 'light' DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' -DEVICE_CLASS_TEMPERATURE = 'temperature' DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index a32ba0370ec..cf0620a4e30 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,21 +1,21 @@ """Class to hold all cover accessories.""" import logging -from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER +from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, + SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) @@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: self.char_current_state.set_value(3) - self.hass.components.cover.open_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params) elif value == 1: self.char_current_state.set_value(2) - self.hass.components.cover.close_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params) def update_state(self, new_state): """Update cover state after state changed.""" @@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory): else: service, position = (SERVICE_CLOSE_COVER, 0) - self.hass.services.call(DOMAIN, service, - {ATTR_ENTITY_ID: self.entity_id}) + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index a3ea027c07e..bf0d4da6a59 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,12 +4,12 @@ import logging from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, + SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, - SERVICE_TURN_OFF, SERVICE_TURN_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON) from . import TYPES from .accessories import HomeAccessory @@ -71,8 +71,7 @@ class Fan(HomeAccessory): _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) self._flag[CHAR_ROTATION_DIRECTION] = True direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_DIRECTION: direction} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) def set_oscillating(self, value): diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index dae3579a97a..da012799602 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -4,16 +4,18 @@ import logging from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_OFF, STATE_ON) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION, - PROP_MAX_VALUE, PROP_MIN_VALUE) + CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, + CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -79,28 +81,27 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - - if value == 1: - self.hass.components.light.turn_on(self.entity_id) - elif value == 0: - self.hass.components.light.turn_off(self.entity_id) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - if value != 0: - self.hass.components.light.turn_on( - self.entity_id, brightness_pct=value) - else: - self.hass.components.light.turn_off(self.entity_id) + if value == 0: + self.set_state(0) # Turn off light + return + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True - self.hass.components.light.turn_on(self.entity_id, color_temp=value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -118,15 +119,14 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle Color if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: color = (self._hue, self._saturation) _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self.hass.components.light.turn_on( - self.entity_id, hs_color=color) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 309f3072768..05ab6c6f822 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -4,13 +4,12 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( - ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory -from .const import ( - SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) +from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) @@ -55,7 +54,7 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code - self.hass.services.call('lock', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update lock after state changed.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index bd29453e10a..bbf8b3f17cb 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -3,16 +3,16 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) + ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, + SERV_SECURITY_SYSTEM) _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self.hass.services.call('alarm_control_panel', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 0005c6184ee..373c1188f2d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,26 +4,26 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, - ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + TEMP_CELSIUS) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, - SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, - CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, - SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, - DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, - DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, - CHAR_CARBON_MONOXIDE_DETECTED, - DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, - DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, - DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, - DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, - DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, - DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) + CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, + CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, + CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, + DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, + SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, + SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, + SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, + SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if density is not None: + if density: self.char_density.set_value(density) self.char_quality.set_value(density_to_air_quality(density)) _LOGGER.debug('%s: Set to %d', self.entity_id, density) @@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" co2 = convert_to_float(new_state.state) - if co2 is not None: + if co2: self.char_co2.set_value(co2) if co2 > self.char_peak.value: self.char_peak.set_value(co2) @@ -157,7 +157,7 @@ class LightSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance is not None: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index ff4bf1611b8..5754266587c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -33,9 +33,9 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(self._domain, service, - {ATTR_ENTITY_ID: self.entity_id}) + self.hass.services.call(self._domain, service, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ab4d7faf875..d6555d5056d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,22 +4,23 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, - CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, - CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, - CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) + CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, + CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -99,12 +100,13 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.hass.services.call('climate', 'turn_off', params) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) return else: - self.hass.services.call('climate', 'turn_on', params) - self.hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OPERATION_MODE: hass_value} + self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) @debounce def set_cooling_threshold(self, value): @@ -113,11 +115,11 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.coolingthresh_flag_target_state = True low = self.char_heating_thresh_temp.value - low = temperature_to_states(low, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=value, - target_temp_low=low) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_heating_threshold(self, value): @@ -125,13 +127,12 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value - high = temperature_to_states(high, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=high, - target_temp_low=value) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_target_temperature(self, value): @@ -139,9 +140,10 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - temperature=value, entity_id=self.entity_id) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TEMPERATURE: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py new file mode 100644 index 00000000000..915759f22d6 --- /dev/null +++ b/tests/components/homekit/common.py @@ -0,0 +1,8 @@ +"""Collection of fixtures and functions for the HomeKit tests.""" +from unittest.mock import patch + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 1b06e245734..3d1c335f8ae 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -5,22 +5,18 @@ This includes tests for all mock object types. from datetime import datetime, timedelta from unittest.mock import patch, Mock +import pytest + from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, - CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, - CHAR_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -def patch_debounce(): - """Return patch for debounce method.""" - return patch('homeassistant.components.homekit.accessories.debounce', - lambda f: lambda *args, **kwargs: f(*args, **kwargs)) - - async def test_debounce(hass): """Test add_timeout decorator function.""" def demo_func(*args): @@ -74,20 +70,23 @@ async def test_home_accessory(hass): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ 'homekit.accessory' - hass.states.async_set('homekit.accessory', 'on') - await hass.async_block_till_done() - await hass.async_add_job(acc.run) - hass.states.async_set('homekit.accessory', 'off') + hass.states.async_set(entity_id, 'on') await hass.async_block_till_done() + with patch('homeassistant.components.homekit.accessories.' + 'HomeAccessory.update_state') as mock_update_state: + await hass.async_add_job(acc.run) + state = hass.states.get(entity_id) + mock_update_state.assert_called_with(state) - entity_id = 'test_model.demo' - hass.states.async_set(entity_id, None) - await hass.async_block_till_done() + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert mock_update_state.call_count == 1 - acc = HomeAccessory('hass', 'test_name', entity_id, 2, None) - assert acc.display_name == 'test_name' - assert acc.aid == 2 - assert len(acc.services) == 1 + with pytest.raises(NotImplementedError): + acc.update_state('new_state') + + # Test model name from domain + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 0ffc1ae4767..25a0dd3f1cb 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -4,13 +4,13 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -40,14 +40,12 @@ def test_customize_options(config, name): ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), - + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), - - ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, - {ATTR_CODE: '1234'}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" @@ -83,22 +81,17 @@ def test_type_covers(type_name, entity_id, state, attrs): ('BinarySensor', 'binary_sensor.opening', 'on', {ATTR_DEVICE_CLASS: 'opening'}), ('BinarySensor', 'device_tracker.someone', 'not_home', {}), - ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), ('AirQualitySensor', 'sensor.air_quality', '40', {ATTR_DEVICE_CLASS: 'pm25'}), - ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), ('CarbonDioxideSensor', 'sensor.airmeter', '500', {ATTR_DEVICE_CLASS: 'co2'}), - ('HumiditySensor', 'sensor.humidity', '20', {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), - ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), - ('TemperatureSensor', 'sensor.temperature', '23', {ATTR_DEVICE_CLASS: 'temperature'}), ('TemperatureSensor', 'sensor.temperature', '23', diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b22a7f63cda..31337088b33 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -6,29 +6,28 @@ import pytest from homeassistant import setup from homeassistant.core import State from homeassistant.components.homekit import ( - HomeKit, generate_aid, - STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, + STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, - DEFAULT_PORT, SERVICE_HOMEKIT_START) + CONF_AUTO_START, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -@pytest.fixture('module') -def debounce_patcher(request): +@pytest.fixture(scope='module') +def debounce_patcher(): """Patch debounce method.""" patcher = patch_debounce() - patcher.start() - request.addfinalizer(patcher.stop) + yield patcher.start() + patcher.stop() def test_generate_aid(): @@ -124,27 +123,25 @@ async def test_homekit_setup_ip_address(hass): hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) -async def test_homekit_add_accessory(hass): +async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(hass) + homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.bridge = mock_bridge = Mock() - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 363398124, {}) - assert mock_add_acc.called is False + mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 294192020, {}) - assert mock_add_acc.called is True + mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 429982757, {}) - mock_add_acc.assert_called_with('acc') + mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_bridge.add_accessory.assert_called_with('acc') async def test_homekit_entity_filter(hass): @@ -171,8 +168,8 @@ async def test_homekit_start(hass, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(hass) - homekit.driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = Mock() + homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] @@ -184,13 +181,13 @@ async def test_homekit_start(hass, debounce_patcher): mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert homekit.driver.start.called is True + assert mock_driver.start.called is True assert homekit.status == STATUS_RUNNING # Test start() if already started - homekit.driver.reset_mock() + mock_driver.reset_mock() await hass.async_add_job(homekit.start) - assert homekit.driver.start.called is False + assert mock_driver.start.called is False async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 7260ae40c1a..8138d1c506b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,13 +4,13 @@ from collections import namedtuple import pytest from homeassistant.components.cover import ( - DOMAIN, ATTR_CURRENT_POSITION, ATTR_POSITION, SUPPORT_STOP) + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index fc504cc6cbd..f96fe19d603 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, + STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 65a526edcc3..7a1db7b3f71 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,14 +4,14 @@ from collections import namedtuple import pytest from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3b8cde47fcb..f4698b1380b 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 577d2f2175d..7b72404cdaa 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -2,12 +2,12 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN -from homeassistant.components.homekit.type_security_systems import ( - SecuritySystem) +from homeassistant.components.homekit.type_security_systems import \ + SecuritySystem from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 56742bada92..e36ae67da12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,11 +1,11 @@ """Test different accessory types: Sensors.""" from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, - LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) + AirQualitySensor, BinarySensor, CarbonDioxideSensor, HumiditySensor, + LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, - STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_NOT_HOME, + STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) async def test_temperature(hass): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 399a8bd84c8..5fc0b6ce1b9 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -3,7 +3,7 @@ import pytest from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index bc5b3219cdf..337ad23ad05 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.climate import ( - DOMAIN, ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, - ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) + ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 42f81387960..0755e8f54d4 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,14 +4,14 @@ import voluptuous as vol from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, density_to_air_quality) + convert_to_float, density_to_air_quality, dismiss_setup_message, + show_setup_message, temperature_to_homekit, temperature_to_states) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - DOMAIN, ATTR_MESSAGE, ATTR_NOTIFICATION_ID) + ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service From a183043d5dc19a6d4b5fd028e769672aad934313 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 21 May 2018 00:56:41 -0400 Subject: [PATCH 773/924] Add IlluminanceMeasurementSensor to ZHA (#14563) * add IlluminanceMeasurementSensor * address review comment * Fix whitespace error during merge --- homeassistant/components/sensor/zha.py | 19 ++++++++++++++++++- homeassistant/components/zha/const.py | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index abb4c651e78..72368bdb3ba 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -32,7 +32,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def make_sensor(discovery_info): """Create ZHA sensors factory.""" from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement, PressureMeasurement + RelativeHumidity, TemperatureMeasurement, PressureMeasurement, + IlluminanceMeasurement ) from zigpy.zcl.clusters.smartenergy import Metering in_clusters = discovery_info['in_clusters'] @@ -42,6 +43,8 @@ def make_sensor(discovery_info): sensor = TemperatureSensor(**discovery_info) elif PressureMeasurement.cluster_id in in_clusters: sensor = PressureSensor(**discovery_info) + elif IlluminanceMeasurement.cluster_id in in_clusters: + sensor = IlluminanceMeasurementSensor(**discovery_info) elif Metering.cluster_id in in_clusters: sensor = MeteringSensor(**discovery_info) else: @@ -148,6 +151,20 @@ class PressureSensor(Sensor): return round(float(self._state)) +class IlluminanceMeasurementSensor(Sensor): + """ZHA lux sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'lx' + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + class MeteringSensor(Sensor): """ZHA Metering sensor.""" diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 71f0ea17490..087b19c6693 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -48,6 +48,7 @@ def populate_data(): zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', zcl.clusters.measurement.PressureMeasurement: 'sensor', + zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', zcl.clusters.smartenergy.Metering: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', From 9791c6b21bd2b86bfc411b9c4a2b6a558014ec3b Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sun, 20 May 2018 21:57:09 -0700 Subject: [PATCH 774/924] zha: Bump to zigpy-xbee 0.1.1 (#14566) --- homeassistant/components/zha/__init__.py | 7 ++++++- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 3ea95ff1dd1..030e342847d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -18,7 +18,7 @@ from homeassistant.util import slugify REQUIREMENTS = [ 'bellows==0.6.0', 'zigpy==0.1.0', - 'zigpy-xbee==0.1.0', + 'zigpy-xbee==0.1.1', ] DOMAIN = 'zha' @@ -151,6 +151,11 @@ class ApplicationListener: # Wait for device_initialized, instead pass + def raw_device_initialized(self, device): + """Handle a device initialization without quirks loaded.""" + # Wait for device_initialized, instead + pass + def device_initialized(self, device): """Handle device joined and basic information discovered.""" self._hass.async_add_job(self.async_device_initialized(device, True)) diff --git a/requirements_all.txt b/requirements_all.txt index 7d4b4bb3f41..80fe842b50f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ zeroconf==0.20.0 ziggo-mediabox-xl==1.0.0 # homeassistant.components.zha -zigpy-xbee==0.1.0 +zigpy-xbee==0.1.1 # homeassistant.components.zha zigpy==0.1.0 From 2ff61786bc85c81ecd5004f7c02fdc3fb2622310 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 May 2018 11:01:35 -0400 Subject: [PATCH 775/924] Update frontend to 20180521.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d4700e5edd3..04e4e0dae48 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180519.0'] +REQUIREMENTS = ['home-assistant-frontend==20180521.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 80fe842b50f..5794f54b457 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5d4d91b1037..a12cba141a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From ba9bb90cf73d7c609510ddfd98dcfbab55170567 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 May 2018 11:01:35 -0400 Subject: [PATCH 776/924] Update frontend to 20180521.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d4700e5edd3..04e4e0dae48 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180519.0'] +REQUIREMENTS = ['home-assistant-frontend==20180521.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 924f2297311..b86398bb3e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75a50d09a3d..a4fe31b0639 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6e941af9b21f47307033bbd1d78042f6e784d41f Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Mon, 21 May 2018 17:02:50 +0200 Subject: [PATCH 777/924] fix nanoleaf aurora lights min and max temperature (#14571) * fixed nanoleaf aurora lights min and max temperature * review changes --- homeassistant/components/light/nanoleaf_aurora.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 8b0b7c053c8..6a0d3c36e9f 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -125,6 +125,16 @@ class AuroraLight(Light): """Return the list of supported effects.""" return self._effects_list + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 154 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 833 + @property def name(self): """Return the display name of this light.""" From cfdea8d20f30b6494637ee65fa965f494b2b0022 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 20 May 2018 00:33:52 +0200 Subject: [PATCH 778/924] Wait for future mysensors gateway ready (#14398) * Wait for future mysensors gateway ready * Add an asyncio future that is done when the gateway reports the gateway ready message, I_GATEWAY_READY. * This will make sure that the gateway is ready before home assistant fires the home assistant start event. Automations can now send messages to the gateway when home assistant is started. * Use async timeout to wait max 15 seconds for ready gateway. * Address comments --- homeassistant/components/mysensors.py | 46 +++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 6721669a026..1e7e252bd9d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,13 +12,14 @@ import socket import sys from timeit import default_timer as timer +import async_timeout import voluptuous as vol from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) + ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, + STATE_OFF, STATE_ON) from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -57,9 +58,11 @@ DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = '1.4' DOMAIN = 'mysensors' +GATEWAY_READY_TIMEOUT = 15.0 MQTT_COMPONENT = 'mqtt' MYSENSORS_GATEWAYS = 'mysensors_gateways' MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' PLATFORM = 'platform' SCHEMA = 'schema' SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' @@ -353,12 +356,12 @@ async def async_setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = await setup_gateway( + gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if ready_gateway is not None: - ready_gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(ready_gateway)] = ready_gateway + if gateway is not None: + gateway.nodes_config = gway.get(CONF_NODES) + gateways[id(gateway)] = gateway if not gateways: _LOGGER.error( @@ -395,6 +398,35 @@ async def gw_start(hass, gateway): await gateway.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + if gateway.device == 'mqtt': + # Gatways connected via mqtt doesn't send gateway ready message. + return + gateway_ready = asyncio.Future() + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) + hass.data[gateway_ready_key] = gateway_ready + + try: + with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + await gateway_ready + except asyncio.TimeoutError: + _LOGGER.warning( + "Gateway %s not ready after %s secs so continuing with setup", + gateway.device, GATEWAY_READY_TIMEOUT) + finally: + hass.data.pop(gateway_ready_key, None) + + +@callback +def set_gateway_ready(hass, msg): + """Set asyncio future result if gateway is ready.""" + if (msg.type != msg.gateway.const.MessageType.internal or + msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): + return + gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( + id(msg.gateway))) + if gateway_ready is None or gateway_ready.cancelled(): + return + gateway_ready.set_result(True) def validate_child(gateway, node_id, child): @@ -495,6 +527,8 @@ def gw_callback_factory(hass): _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) + set_gateway_ready(hass, msg) + try: child = msg.gateway.sensors[msg.node_id].children[msg.child_id] except KeyError: From 2f8865d6cb64b252254874589a722cb886c405b8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 May 2018 04:25:53 +0200 Subject: [PATCH 779/924] Homekit style cleanup (#14556) * Style cleanup * Sorted imports * Harmonized service calls * Test improvements * Small update --- homeassistant/components/homekit/__init__.py | 11 ++-- .../components/homekit/accessories.py | 5 +- homeassistant/components/homekit/const.py | 35 +++++------ .../components/homekit/type_covers.py | 25 ++++---- homeassistant/components/homekit/type_fans.py | 13 ++-- .../components/homekit/type_lights.py | 42 ++++++------- .../components/homekit/type_locks.py | 7 +-- .../homekit/type_security_systems.py | 12 ++-- .../components/homekit/type_sensors.py | 38 ++++++------ .../components/homekit/type_switches.py | 4 +- .../components/homekit/type_thermostats.py | 60 ++++++++++--------- tests/components/homekit/common.py | 8 +++ tests/components/homekit/test_accessories.py | 39 ++++++------ .../homekit/test_get_accessories.py | 15 ++--- tests/components/homekit/test_homekit.py | 49 +++++++-------- tests/components/homekit/test_type_covers.py | 4 +- tests/components/homekit/test_type_fans.py | 12 ++-- tests/components/homekit/test_type_lights.py | 6 +- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 10 ++-- tests/components/homekit/test_type_sensors.py | 8 +-- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 8 +-- tests/components/homekit/test_util.py | 8 +-- 24 files changed, 209 insertions(+), 214 deletions(-) create mode 100644 tests/components/homekit/common.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ce5f30d7bf2..202f9694689 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -13,17 +13,18 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, - DEFAULT_AUTO_START, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, + DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, + SERVICE_HOMEKIT_START) from .util import show_setup_message, validate_entity_config TYPES = Registry() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ff835659221..ded4526b008 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -84,20 +84,21 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.entity_id, self.update_state_callback) + @ha_callback def update_state_callback(self, entity_id=None, old_state=None, new_state=None): """Callback from state change listener.""" _LOGGER.debug('New_state: %s', new_state) if new_state is None: return - self.update_state(new_state) + self.hass.async_add_job(self.update_state, new_state) def update_state(self, new_state): """Method called on state change to update HomeKit value. Overridden by accessory types. """ - pass + raise NotImplementedError() class HomeBridge(Bridge): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index adde13cc030..21cad2d9cf7 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,23 +1,23 @@ """Constants used be the HomeKit component.""" -# #### MISC #### +# #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 -# #### CONFIG #### +# #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' CONF_FILTER = 'filter' -# #### CONFIG DEFAULTS #### +# #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 -# #### HOMEKIT COMPONENT SERVICES #### +# #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' -# #### STRING CONSTANTS #### +# #### String Constants #### BRIDGE_MODEL = 'Bridge' BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' @@ -31,10 +31,10 @@ SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity +SERV_HUMIDITY_SENSOR = 'HumiditySensor' SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' -SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' @@ -44,13 +44,12 @@ SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' -CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' @@ -61,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] -CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent +CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' -CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_HUE = 'Hue' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -77,16 +76,16 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' -CHAR_ON = 'On' # boolean +CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' -CHAR_SATURATION = 'Saturation' # percent +CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] +CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' @@ -94,21 +93,17 @@ CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### PROP_MAX_VALUE = 'maxValue' PROP_MIN_VALUE = 'minValue' - PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} -# #### Device Class #### +# #### Device Classes #### DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' -DEVICE_CLASS_HUMIDITY = 'humidity' -DEVICE_CLASS_LIGHT = 'light' DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' -DEVICE_CLASS_TEMPERATURE = 'temperature' DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index a32ba0370ec..cf0620a4e30 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,21 +1,21 @@ """Class to hold all cover accessories.""" import logging -from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER +from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, + SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) @@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: self.char_current_state.set_value(3) - self.hass.components.cover.open_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params) elif value == 1: self.char_current_state.set_value(2) - self.hass.components.cover.close_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params) def update_state(self, new_state): """Update cover state after state changed.""" @@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory): else: service, position = (SERVICE_CLOSE_COVER, 0) - self.hass.services.call(DOMAIN, service, - {ATTR_ENTITY_ID: self.entity_id}) + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index a3ea027c07e..bf0d4da6a59 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,12 +4,12 @@ import logging from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, + SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, - SERVICE_TURN_OFF, SERVICE_TURN_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON) from . import TYPES from .accessories import HomeAccessory @@ -71,8 +71,7 @@ class Fan(HomeAccessory): _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) self._flag[CHAR_ROTATION_DIRECTION] = True direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_DIRECTION: direction} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) def set_oscillating(self, value): diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index dae3579a97a..da012799602 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -4,16 +4,18 @@ import logging from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_OFF, STATE_ON) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION, - PROP_MAX_VALUE, PROP_MIN_VALUE) + CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, + CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -79,28 +81,27 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - - if value == 1: - self.hass.components.light.turn_on(self.entity_id) - elif value == 0: - self.hass.components.light.turn_off(self.entity_id) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - if value != 0: - self.hass.components.light.turn_on( - self.entity_id, brightness_pct=value) - else: - self.hass.components.light.turn_off(self.entity_id) + if value == 0: + self.set_state(0) # Turn off light + return + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True - self.hass.components.light.turn_on(self.entity_id, color_temp=value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -118,15 +119,14 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle Color if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: color = (self._hue, self._saturation) _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self.hass.components.light.turn_on( - self.entity_id, hs_color=color) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 309f3072768..05ab6c6f822 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -4,13 +4,12 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( - ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory -from .const import ( - SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) +from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) @@ -55,7 +54,7 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code - self.hass.services.call('lock', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update lock after state changed.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index bd29453e10a..bbf8b3f17cb 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -3,16 +3,16 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) + ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, + SERV_SECURITY_SYSTEM) _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self.hass.services.call('alarm_control_panel', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 0005c6184ee..373c1188f2d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,26 +4,26 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, - ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + TEMP_CELSIUS) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, - SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, - CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, - SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, - DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, - DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, - CHAR_CARBON_MONOXIDE_DETECTED, - DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, - DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, - DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, - DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, - DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, - DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) + CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, + CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, + CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, + DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, + SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, + SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, + SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, + SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if density is not None: + if density: self.char_density.set_value(density) self.char_quality.set_value(density_to_air_quality(density)) _LOGGER.debug('%s: Set to %d', self.entity_id, density) @@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" co2 = convert_to_float(new_state.state) - if co2 is not None: + if co2: self.char_co2.set_value(co2) if co2 > self.char_peak.value: self.char_peak.set_value(co2) @@ -157,7 +157,7 @@ class LightSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance is not None: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index ff4bf1611b8..5754266587c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -33,9 +33,9 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(self._domain, service, - {ATTR_ENTITY_ID: self.entity_id}) + self.hass.services.call(self._domain, service, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ab4d7faf875..d6555d5056d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,22 +4,23 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, - CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, - CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, - CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) + CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, + CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -99,12 +100,13 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.hass.services.call('climate', 'turn_off', params) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) return else: - self.hass.services.call('climate', 'turn_on', params) - self.hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OPERATION_MODE: hass_value} + self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) @debounce def set_cooling_threshold(self, value): @@ -113,11 +115,11 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.coolingthresh_flag_target_state = True low = self.char_heating_thresh_temp.value - low = temperature_to_states(low, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=value, - target_temp_low=low) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_heating_threshold(self, value): @@ -125,13 +127,12 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value - high = temperature_to_states(high, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=high, - target_temp_low=value) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_target_temperature(self, value): @@ -139,9 +140,10 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - temperature=value, entity_id=self.entity_id) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TEMPERATURE: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py new file mode 100644 index 00000000000..915759f22d6 --- /dev/null +++ b/tests/components/homekit/common.py @@ -0,0 +1,8 @@ +"""Collection of fixtures and functions for the HomeKit tests.""" +from unittest.mock import patch + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 1b06e245734..3d1c335f8ae 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -5,22 +5,18 @@ This includes tests for all mock object types. from datetime import datetime, timedelta from unittest.mock import patch, Mock +import pytest + from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, - CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, - CHAR_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -def patch_debounce(): - """Return patch for debounce method.""" - return patch('homeassistant.components.homekit.accessories.debounce', - lambda f: lambda *args, **kwargs: f(*args, **kwargs)) - - async def test_debounce(hass): """Test add_timeout decorator function.""" def demo_func(*args): @@ -74,20 +70,23 @@ async def test_home_accessory(hass): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ 'homekit.accessory' - hass.states.async_set('homekit.accessory', 'on') - await hass.async_block_till_done() - await hass.async_add_job(acc.run) - hass.states.async_set('homekit.accessory', 'off') + hass.states.async_set(entity_id, 'on') await hass.async_block_till_done() + with patch('homeassistant.components.homekit.accessories.' + 'HomeAccessory.update_state') as mock_update_state: + await hass.async_add_job(acc.run) + state = hass.states.get(entity_id) + mock_update_state.assert_called_with(state) - entity_id = 'test_model.demo' - hass.states.async_set(entity_id, None) - await hass.async_block_till_done() + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert mock_update_state.call_count == 1 - acc = HomeAccessory('hass', 'test_name', entity_id, 2, None) - assert acc.display_name == 'test_name' - assert acc.aid == 2 - assert len(acc.services) == 1 + with pytest.raises(NotImplementedError): + acc.update_state('new_state') + + # Test model name from domain + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 0ffc1ae4767..25a0dd3f1cb 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -4,13 +4,13 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -40,14 +40,12 @@ def test_customize_options(config, name): ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), - + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), - - ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, - {ATTR_CODE: '1234'}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" @@ -83,22 +81,17 @@ def test_type_covers(type_name, entity_id, state, attrs): ('BinarySensor', 'binary_sensor.opening', 'on', {ATTR_DEVICE_CLASS: 'opening'}), ('BinarySensor', 'device_tracker.someone', 'not_home', {}), - ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), ('AirQualitySensor', 'sensor.air_quality', '40', {ATTR_DEVICE_CLASS: 'pm25'}), - ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), ('CarbonDioxideSensor', 'sensor.airmeter', '500', {ATTR_DEVICE_CLASS: 'co2'}), - ('HumiditySensor', 'sensor.humidity', '20', {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), - ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), - ('TemperatureSensor', 'sensor.temperature', '23', {ATTR_DEVICE_CLASS: 'temperature'}), ('TemperatureSensor', 'sensor.temperature', '23', diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b22a7f63cda..31337088b33 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -6,29 +6,28 @@ import pytest from homeassistant import setup from homeassistant.core import State from homeassistant.components.homekit import ( - HomeKit, generate_aid, - STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, + STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, - DEFAULT_PORT, SERVICE_HOMEKIT_START) + CONF_AUTO_START, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -@pytest.fixture('module') -def debounce_patcher(request): +@pytest.fixture(scope='module') +def debounce_patcher(): """Patch debounce method.""" patcher = patch_debounce() - patcher.start() - request.addfinalizer(patcher.stop) + yield patcher.start() + patcher.stop() def test_generate_aid(): @@ -124,27 +123,25 @@ async def test_homekit_setup_ip_address(hass): hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) -async def test_homekit_add_accessory(hass): +async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(hass) + homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.bridge = mock_bridge = Mock() - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 363398124, {}) - assert mock_add_acc.called is False + mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 294192020, {}) - assert mock_add_acc.called is True + mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 429982757, {}) - mock_add_acc.assert_called_with('acc') + mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_bridge.add_accessory.assert_called_with('acc') async def test_homekit_entity_filter(hass): @@ -171,8 +168,8 @@ async def test_homekit_start(hass, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(hass) - homekit.driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = Mock() + homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] @@ -184,13 +181,13 @@ async def test_homekit_start(hass, debounce_patcher): mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert homekit.driver.start.called is True + assert mock_driver.start.called is True assert homekit.status == STATUS_RUNNING # Test start() if already started - homekit.driver.reset_mock() + mock_driver.reset_mock() await hass.async_add_job(homekit.start) - assert homekit.driver.start.called is False + assert mock_driver.start.called is False async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 7260ae40c1a..8138d1c506b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,13 +4,13 @@ from collections import namedtuple import pytest from homeassistant.components.cover import ( - DOMAIN, ATTR_CURRENT_POSITION, ATTR_POSITION, SUPPORT_STOP) + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index fc504cc6cbd..f96fe19d603 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, + STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 65a526edcc3..7a1db7b3f71 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,14 +4,14 @@ from collections import namedtuple import pytest from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3b8cde47fcb..f4698b1380b 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 577d2f2175d..7b72404cdaa 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -2,12 +2,12 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN -from homeassistant.components.homekit.type_security_systems import ( - SecuritySystem) +from homeassistant.components.homekit.type_security_systems import \ + SecuritySystem from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 56742bada92..e36ae67da12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,11 +1,11 @@ """Test different accessory types: Sensors.""" from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, - LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) + AirQualitySensor, BinarySensor, CarbonDioxideSensor, HumiditySensor, + LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, - STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_NOT_HOME, + STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) async def test_temperature(hass): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 399a8bd84c8..5fc0b6ce1b9 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -3,7 +3,7 @@ import pytest from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index bc5b3219cdf..337ad23ad05 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.climate import ( - DOMAIN, ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, - ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) + ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 42f81387960..0755e8f54d4 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,14 +4,14 @@ import voluptuous as vol from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, density_to_air_quality) + convert_to_float, density_to_air_quality, dismiss_setup_message, + show_setup_message, temperature_to_homekit, temperature_to_states) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - DOMAIN, ATTR_MESSAGE, ATTR_NOTIFICATION_ID) + ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service From 1bc916927ca7138ac136e722a41a6ac2f65a9c25 Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Mon, 21 May 2018 17:02:50 +0200 Subject: [PATCH 780/924] fix nanoleaf aurora lights min and max temperature (#14571) * fixed nanoleaf aurora lights min and max temperature * review changes --- homeassistant/components/light/nanoleaf_aurora.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 99c07166037..c26766d8deb 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -92,6 +92,16 @@ class AuroraLight(Light): """Return the list of supported effects.""" return self._effects_list + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 154 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 833 + @property def name(self): """Return the display name of this light.""" From 4671bd95c612cc55c05ef3284f697b8fbeb76f27 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 May 2018 11:10:53 -0400 Subject: [PATCH 781/924] Version bump to 0.70.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 68f4443d12e..73dc0db7aac 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 23afdec76700edcba3d1743c4fcd9e789a08db70 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 21 May 2018 12:00:01 -0700 Subject: [PATCH 782/924] Fix ISY moisure sensors showing unknown until a leak is detected (#14496) * Fix ISY leak sensors always showing UNKNOWN until a leak is detected Added some logic that handles both moisture sensors and door/window sensors * Handle edge case of leak sensor status update after ISY reboot If a leak sensor is unknown, due to a recent reboot of the ISY, the status will get updated to dry upon the first heartbeat. This status update is the only way that a leak sensor's status changes without an accompanying Control event, so we need to watch for it. * Fixes from overnight testing State was checking the incorrect parameter, and wasn't calling schedule update * Remove leftover debug log line * Remove unnecessary pylint instruction * Remove access of protected property We can't cast _.status directly to a bool for some unknown reason (possibly with the VarEvents library), but casting to an int then bool does work. --- .../components/binary_sensor/isy994.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index fb86244acf3..09f1739cba7 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None + self._status_was_unknown = True else: self._computed_state = bool(self._node.status._val) + self._status_was_unknown = False @asyncio.coroutine def async_added_to_hass(self) -> None: @@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is - # in use for this device. Therefore, we cannot determine the state - # of the sensor until we receive our first ON event. - self._computed_state = None + # in use for this device. Next we need to check to see if the + # negative and positive nodes disagree on the state (both ON or + # both OFF). + if self._negative_node.status._val == self._node.status._val: + # The states disagree, therefore we cannot determine the state + # of the sensor until we receive our first ON event. + self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" @@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self.schedule_update_ha_state() self._heartbeat() - # pylint: disable=unused-argument def on_update(self, event: object) -> None: - """Ignore primary node status updates. + """Primary node status updates. - We listen directly to the Control events on all nodes for this - device. + We MOSTLY ignore these updates, as we listen directly to the Control + events on all nodes for this device. However, there is one edge case: + If a leak sensor is unknown, due to a recent reboot of the ISY, the + status will get updated to dry upon the first heartbeat. This status + update is the only way that a leak sensor's status changes without + an accompanying Control event, so we need to watch for it. """ - pass + if self._status_was_unknown and self._computed_state is None: + self._computed_state = bool(int(self._node.status)) + self._status_was_unknown = False + self.schedule_update_ha_state() + self._heartbeat() @property def value(self) -> object: From 0d9b3bea1029964d2e6c7e8204fc930a06423107 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 21 May 2018 19:46:20 -0400 Subject: [PATCH 783/924] Bump insteonplm version to fix device hanging (#14582) * Update inteonplm to 0.9.2 * Change to force Travis CI * Change to force Travis CI --- homeassistant/components/insteon_plm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 246e84ec71f..b86f80cbee7 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.1'] +REQUIREMENTS = ['insteonplm==0.9.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 5794f54b457..45cf3bc1e1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.1 +insteonplm==0.9.2 # homeassistant.components.verisure jsonpath==0.75 From 118c49ecaa37a4d0e29d4a4d91ebfd59be298a7e Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 22 May 2018 01:50:08 +0200 Subject: [PATCH 784/924] Update pyhomematic to 0.1.43 (#14583) * Update __init__.py * Update requirements_all.txt --- homeassistant/components/homematic/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index e0f0fafe5b5..29303b551e2 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -20,7 +20,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass -REQUIREMENTS = ['pyhomematic==0.1.42'] +REQUIREMENTS = ['pyhomematic==0.1.43'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 45cf3bc1e1d..ef0aa86ee6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -805,7 +805,7 @@ pyhik==0.1.8 pyhiveapi==0.2.14 # homeassistant.components.homematic -pyhomematic==0.1.42 +pyhomematic==0.1.43 # homeassistant.components.sensor.hydroquebec pyhydroquebec==2.2.2 From 2753dd0c5e341fc2a3753430dc03a99ebd237ee5 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Tue, 22 May 2018 02:19:45 -0400 Subject: [PATCH 785/924] Removed attribute current_time from Raincloudy sensors to avoid being triggered by recorder component (#14584) --- homeassistant/components/raincloud.py | 1 - homeassistant/components/switch/raincloud.py | 1 - 2 files changed, 2 deletions(-) diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py index 505c3a7b2b0..308a945e942 100644 --- a/homeassistant/components/raincloud.py +++ b/homeassistant/components/raincloud.py @@ -168,7 +168,6 @@ class RainCloudEntity(Entity): """Return the state attributes.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'current_time': self.data.current_time, 'identifier': self.data.serial, } diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py index 8a5c4347cf7..a4bac8fee1c 100644 --- a/homeassistant/components/switch/raincloud.py +++ b/homeassistant/components/switch/raincloud.py @@ -88,7 +88,6 @@ class RainCloudSwitch(RainCloudEntity, SwitchDevice): """Return the state attributes.""" return { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - 'current_time': self.data.current_time, 'default_manual_timer': self._default_watering_timer, 'identifier': self.data.serial } From 72a1b7ae3fc5340b44c7978ae6d1551c47aef4d7 Mon Sep 17 00:00:00 2001 From: Julian Knauer Date: Tue, 22 May 2018 09:25:10 +0200 Subject: [PATCH 786/924] Lagute LW-12 Wifi LED control (#13307) * Added platform lw12wifi for Lagute LW-12 Wifi Lights Supported features: * RGB colors * Variable brightness * 29 effects * Changing transitions speed for animated effects * Added lw12wifi to the list of omitted files to test * Added lw12 module as new requirement for lw12wifi platform * Added configuration example docstring for platform lw12wifi * Updating code according to review in PR: * Removed unused imports: enum, socket. * Unused and not imported feature SUPPORT_FLASH was removed. * Unused import lw12 in setup_platform method removed. * Fixed indention for valuptuous. * Changed check if effect is None. * Removed personal debug output. * Blocking function are not async anymore. * Further improvements to satisfy PR. * Unused import asyncio removed. * Fixed: Return value and docstring no match up for `assumed_state`. * Check if the set effect is supported, otherwise revert to normal light. * Added describing missing docstrings to all functions. * Adopted code to work with HS color setting. * Syntactical change in comment. * Removed redefinition of DOMAIN. * Refactored lw12 controller setup: removed requirement for host and port in LW12Wifi class. * Rewritten supported feature setup to a more static expression. * Removed unused rgb_color property * Fixed typo in comment for set_light_option * Changed RGB option validation schema * Removed instance properties as config options * Removed optional settings to be more inline with code style. * Removed unused option from config example * Removal of unused import * Added property to disable state polling for this entity. * Raise an exception if an unknown effect was selected. * Fixed an issue with the check for known effects. * As we do not need to set a default, use simple accessing by key. * Log if an unknown effect was selected. * Added link to future documentation. --- .coveragerc | 1 + homeassistant/components/light/lw12wifi.py | 158 +++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 162 insertions(+) create mode 100644 homeassistant/components/light/lw12wifi.py diff --git a/.coveragerc b/.coveragerc index d361cf2ddad..a31af5f296c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -443,6 +443,7 @@ omit = homeassistant/components/light/lifx_legacy.py homeassistant/components/light/lifx.py homeassistant/components/light/limitlessled.py + homeassistant/components/light/lw12wifi.py homeassistant/components/light/mystrom.py homeassistant/components/light/nanoleaf_aurora.py homeassistant/components/light/osramlightify.py diff --git a/homeassistant/components/light/lw12wifi.py b/homeassistant/components/light/lw12wifi.py new file mode 100644 index 00000000000..f81d8368f98 --- /dev/null +++ b/homeassistant/components/light/lw12wifi.py @@ -0,0 +1,158 @@ +""" +Support for Lagute LW-12 WiFi LED Controller. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.lw12wifi/ +""" + +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, + Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_EFFECT, + SUPPORT_COLOR, SUPPORT_TRANSITION +) +from homeassistant.const import ( + CONF_HOST, CONF_NAME, CONF_PORT +) +import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util + + +REQUIREMENTS = ['lw12==0.9.2'] + +_LOGGER = logging.getLogger(__name__) + + +DEFAULT_NAME = 'LW-12 FC' +DEFAULT_PORT = 5000 + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup LW-12 WiFi LED Controller platform.""" + import lw12 + + # Assign configuration variables. + name = config.get(CONF_NAME) + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + # Add devices + lw12_light = lw12.LW12Controller(host, port) + add_devices([LW12WiFi(name, lw12_light)]) + + +class LW12WiFi(Light): + """LW-12 WiFi LED Controller.""" + + def __init__(self, name, lw12_light): + """Initialisation of LW-12 WiFi LED Controller. + + Args: + name: Friendly name for this platform to use. + lw12_light: Instance of the LW12 controller. + """ + self._light = lw12_light + self._name = name + self._state = None + self._effect = None + self._rgb_color = [255, 255, 255] + self._brightness = 255 + # Setup feature list + self._supported_features = SUPPORT_BRIGHTNESS | SUPPORT_EFFECT \ + | SUPPORT_COLOR | SUPPORT_TRANSITION + + @property + def name(self): + """Return the display name of the controlled light.""" + return self._name + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Read back the hue-saturation of the light.""" + return color_util.color_RGB_to_hs(*self._rgb_color) + + @property + def effect(self): + """Return current light effect.""" + if self._effect is None: + return None + return self._effect.replace('_', ' ').title() + + @property + def is_on(self): + """Return true if light is on.""" + return self._state + + @property + def supported_features(self): + """Return a list of supported features.""" + return self._supported_features + + @property + def effect_list(self): + """Return a list of available effects. + + Use the Enum element name for display. + """ + import lw12 + return [effect.name.replace('_', ' ').title() + for effect in lw12.LW12_EFFECT] + + @property + def assumed_state(self) -> bool: + """Return True if unable to access real state of the entity.""" + return True + + @property + def shoud_poll(self) -> bool: + """Return False to not poll the state of this entity.""" + return False + + def turn_on(self, **kwargs): + """Instruct the light to turn on.""" + import lw12 + self._light.light_on() + if ATTR_HS_COLOR in kwargs: + self._rgb_color = color_util.color_hs_to_RGB( + *kwargs[ATTR_HS_COLOR]) + self._light.set_color(*self._rgb_color) + self._effect = None + if ATTR_BRIGHTNESS in kwargs: + self._brightness = kwargs.get(ATTR_BRIGHTNESS) + brightness = int(self._brightness / 255 * 100) + self._light.set_light_option(lw12.LW12_LIGHT.BRIGHTNESS, + brightness) + if ATTR_EFFECT in kwargs: + self._effect = kwargs[ATTR_EFFECT].replace(' ', '_').upper() + # Check if a known and supported effect was selected. + if self._effect in [eff.name for eff in lw12.LW12_EFFECT]: + # Selected effect is supported and will be applied. + self._light.set_effect(lw12.LW12_EFFECT[self._effect]) + else: + # Unknown effect was set, recover by disabling the effect + # mode and log an error. + _LOGGER.error("Unknown effect selected: %s", self._effect) + self._effect = None + if ATTR_TRANSITION in kwargs: + transition_speed = int(kwargs[ATTR_TRANSITION]) + self._light.set_light_option(lw12.LW12_LIGHT.FLASH, + transition_speed) + self._state = True + + def turn_off(self, **kwargs): + """Instruct the light to turn off.""" + self._light.light_off() + self._state = False diff --git a/requirements_all.txt b/requirements_all.txt index ef0aa86ee6b..016e2f9ce2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,6 +514,9 @@ locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 +# homeassistant.components.light.lw12wifi +lw12==0.9.2 + # homeassistant.components.sensor.lyft lyft_rides==0.2 From a2decdaaa32846ac06f53f93e939138de1fb659a Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 22 May 2018 17:34:02 +1000 Subject: [PATCH 787/924] NUT sensor enhancements (#14570) (Fixes #14324) * removed default value from required parameter; raising PlatformNotReady when connection to nut unavailable; output human-readable state name by default * removed superfluous sensor name part; showing human-readable form and raw value of current status in more info dialog * introduced a new virtual sensor type based on the raw status value but used to display a human-readable form of the status * renamed method * format string instead of concatenation * revert the change to the device state attributes - only output the human-readable status without the raw value --- homeassistant/components/sensor/nut.py | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/nut.py b/homeassistant/components/sensor/nut.py index b8917080efc..bf440728a2e 100644 --- a/homeassistant/components/sensor/nut.py +++ b/homeassistant/components/sensor/nut.py @@ -14,6 +14,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, CONF_NAME, CONF_USERNAME, CONF_PASSWORD, TEMP_CELSIUS, CONF_RESOURCES, CONF_ALIAS, ATTR_STATE, STATE_UNKNOWN) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -26,10 +27,12 @@ DEFAULT_HOST = 'localhost' DEFAULT_PORT = 3493 KEY_STATUS = 'ups.status' +KEY_STATUS_DISPLAY = 'ups.status.display' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SENSOR_TYPES = { + 'ups.status.display': ['Status', '', 'mdi:information-outline'], 'ups.status': ['Status Data', '', 'mdi:information-outline'], 'ups.alarm': ['Alarms', '', 'mdi:alarm'], 'ups.time': ['Internal Time', '', 'mdi:calendar-clock'], @@ -130,7 +133,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ALIAS): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, - vol.Required(CONF_RESOURCES, default=[]): + vol.Required(CONF_RESOURCES): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), }) @@ -148,7 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if data.status is None: _LOGGER.error("NUT Sensor has no data, unable to setup") - return False + raise PlatformNotReady _LOGGER.debug('NUT Sensors Available: %s', data.status) @@ -157,7 +160,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for resource in config[CONF_RESOURCES]: sensor_type = resource.lower() - if sensor_type in data.status: + # Display status is a special case that falls back to the status value + # of the UPS instead. + if sensor_type in data.status or (sensor_type == KEY_STATUS_DISPLAY + and KEY_STATUS in data.status): entities.append(NUTSensor(name, data, sensor_type)) else: _LOGGER.warning( @@ -169,7 +175,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): except data.pynuterror as err: _LOGGER.error("Failure while testing NUT status retrieval. " "Cannot continue setup: %s", err) - return False + raise PlatformNotReady add_entities(entities, True) @@ -209,11 +215,11 @@ class NUTSensor(Entity): def device_state_attributes(self): """Return the sensor attributes.""" attr = dict() - attr[ATTR_STATE] = self.opp_state() + attr[ATTR_STATE] = self.display_state() return attr - def opp_state(self): - """Return UPS operating state.""" + def display_state(self): + """Return UPS display state.""" if self._data.status is None: return STATE_TYPES['OFF'] else: @@ -230,7 +236,11 @@ class NUTSensor(Entity): self._state = None return - if self.type not in self._data.status: + # In case of the display status sensor, keep a human-readable form + # as the sensor state. + if self.type == KEY_STATUS_DISPLAY: + self._state = self.display_state() + elif self.type not in self._data.status: self._state = None else: self._state = self._data.status[self.type] @@ -288,5 +298,5 @@ class PyNUTData(object): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): - """Fetch the latest status from APCUPSd.""" + """Fetch the latest status from NUT.""" self._status = self._get_status() From a2f9fdf3394542eeb7e4007ca49d7d707108e6ad Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 May 2018 10:06:14 +0200 Subject: [PATCH 788/924] Add new transmission sensor types (#14530) --- .../components/sensor/transmission.py | 104 +++++++++++------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/sensor/transmission.py b/homeassistant/components/sensor/transmission.py index 678d9afb81d..4dac411d224 100644 --- a/homeassistant/components/sensor/transmission.py +++ b/homeassistant/components/sensor/transmission.py @@ -4,23 +4,23 @@ Support for monitoring the Transmission BitTorrent client API. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.transmission/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, - CONF_MONITORED_VARIABLES, STATE_IDLE) + CONF_HOST, CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, STATE_IDLE) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.exceptions import PlatformNotReady REQUIREMENTS = ['transmissionrpc==0.11'] _LOGGER = logging.getLogger(__name__) -_THROTTLED_REFRESH = None DEFAULT_NAME = 'Transmission' DEFAULT_PORT = 9091 @@ -29,12 +29,16 @@ SENSOR_TYPES = { 'active_torrents': ['Active Torrents', None], 'current_status': ['Status', None], 'download_speed': ['Down Speed', 'MB/s'], + 'paused_torrents': ['Paused Torrents', None], + 'total_torrents': ['Total Torrents', None], 'upload_speed': ['Up Speed', 'MB/s'], } +SCAN_INTERVAL = timedelta(minutes=2) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=[]): + vol.Optional(CONF_MONITORED_VARIABLES, default=['torrents']): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -43,7 +47,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -# pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Transmission sensors.""" import transmissionrpc @@ -56,39 +59,37 @@ def setup_platform(hass, config, add_devices, discovery_info=None): port = config.get(CONF_PORT) try: - transmission_api = transmissionrpc.Client( + transmission = transmissionrpc.Client( host, port=port, user=username, password=password) - transmission_api.session_stats() + transmission_api = TransmissionData(transmission) except TransmissionError as error: - _LOGGER.error( - "Connection to Transmission API failed on %s:%s with message %s", - host, port, error.original - ) - return False + if str(error).find("401: Unauthorized"): + _LOGGER.error("Credentials for Transmission client are not valid") + return - # pylint: disable=global-statement - global _THROTTLED_REFRESH - _THROTTLED_REFRESH = Throttle(timedelta(seconds=1))( - transmission_api.session_stats) + _LOGGER.warning( + "Unable to connect to Transmission client: %s:%s", host, port) + raise PlatformNotReady dev = [] for variable in config[CONF_MONITORED_VARIABLES]: dev.append(TransmissionSensor(variable, transmission_api, name)) - add_devices(dev) + add_devices(dev, True) class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" - def __init__(self, sensor_type, transmission_client, client_name): + def __init__(self, sensor_type, transmission_api, client_name): """Initialize the sensor.""" self._name = SENSOR_TYPES[sensor_type][0] - self.tm_client = transmission_client - self.type = sensor_type - self.client_name = client_name self._state = None + self._transmission_api = transmission_api self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._data = None + self.client_name = client_name + self.type = sensor_type @property def name(self): @@ -105,25 +106,20 @@ class TransmissionSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - # pylint: disable=no-self-use - def refresh_transmission_data(self): - """Call the throttled Transmission refresh method.""" - from transmissionrpc.error import TransmissionError - - if _THROTTLED_REFRESH is not None: - try: - _THROTTLED_REFRESH() - except TransmissionError: - _LOGGER.error("Connection to Transmission API failed") + @property + def available(self): + """Could the device be accessed during the last update call.""" + return self._transmission_api.available def update(self): """Get the latest data from Transmission and updates the state.""" - self.refresh_transmission_data() + self._transmission_api.update() + self._data = self._transmission_api.data if self.type == 'current_status': - if self.tm_client.session: - upload = self.tm_client.session.uploadSpeed - download = self.tm_client.session.downloadSpeed + if self._data: + upload = self._data.uploadSpeed + download = self._data.downloadSpeed if upload > 0 and download > 0: self._state = 'Up/Down' elif upload > 0 and download == 0: @@ -135,14 +131,40 @@ class TransmissionSensor(Entity): else: self._state = None - if self.tm_client.session: + if self._data: if self.type == 'download_speed': - mb_spd = float(self.tm_client.session.downloadSpeed) + mb_spd = float(self._data.downloadSpeed) mb_spd = mb_spd / 1024 / 1024 self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1) elif self.type == 'upload_speed': - mb_spd = float(self.tm_client.session.uploadSpeed) + mb_spd = float(self._data.uploadSpeed) mb_spd = mb_spd / 1024 / 1024 self._state = round(mb_spd, 2 if mb_spd < 0.1 else 1) elif self.type == 'active_torrents': - self._state = self.tm_client.session.activeTorrentCount + self._state = self._data.activeTorrentCount + elif self.type == 'paused_torrents': + self._state = self._data.pausedTorrentCount + elif self.type == 'total_torrents': + self._state = self._data.torrentCount + + +class TransmissionData(object): + """Get the latest data and update the states.""" + + def __init__(self, api): + """Initialize the Transmission data object.""" + self.data = None + self.available = True + self._api = api + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Transmission instance.""" + from transmissionrpc.error import TransmissionError + + try: + self.data = self._api.session_stats() + self.available = True + except TransmissionError: + self.available = False + _LOGGER.error("Unable to connect to Transmission client") From 82770faad71c8d2a34d427fe864686f8b213b3f0 Mon Sep 17 00:00:00 2001 From: SchumyHao Date: Tue, 22 May 2018 16:40:11 +0800 Subject: [PATCH 789/924] Add Xiaomi Aqara Lock support (#14419) --- homeassistant/components/lock/xiaomi_aqara.py | 92 +++++++++++++++++++ homeassistant/components/xiaomi_aqara.py | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lock/xiaomi_aqara.py diff --git a/homeassistant/components/lock/xiaomi_aqara.py b/homeassistant/components/lock/xiaomi_aqara.py new file mode 100644 index 00000000000..9b084a2bc55 --- /dev/null +++ b/homeassistant/components/lock/xiaomi_aqara.py @@ -0,0 +1,92 @@ +""" +Support for Xiaomi Aqara Lock. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/lock.xiaomi_aqara/ +""" +import logging +from homeassistant.components.xiaomi_aqara import (PY_XIAOMI_GATEWAY, + XiaomiDevice) +from homeassistant.components.lock import LockDevice +from homeassistant.const import (STATE_LOCKED, STATE_UNLOCKED) +from homeassistant.helpers.event import async_call_later +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +FINGER_KEY = 'fing_verified' +PASSWORD_KEY = 'psw_verified' +CARD_KEY = 'card_verified' +VERIFIED_WRONG_KEY = 'verified_wrong' + +ATTR_VERIFIED_WRONG_TIMES = 'verified_wrong_times' + +UNLOCK_MAINTAIN_TIME = 5 + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Perform the setup for Xiaomi devices.""" + devices = [] + + for gateway in hass.data[PY_XIAOMI_GATEWAY].gateways.values(): + for device in gateway.devices['lock']: + model = device['model'] + if model == 'lock.aq1': + devices.append(XiaomiAqaraLock(device, 'Lock', gateway)) + async_add_devices(devices) + + +class XiaomiAqaraLock(LockDevice, XiaomiDevice): + """Representation of a XiaomiAqaraLock.""" + + def __init__(self, device, name, xiaomi_hub): + """Initialize the XiaomiAqaraLock.""" + self._changed_by = 0 + self._verified_wrong_times = 0 + + super().__init__(device, name, xiaomi_hub) + + @property + def is_locked(self) -> bool: + """Return true if lock is locked.""" + if self._state is not None: + return self._state == STATE_LOCKED + + @property + def changed_by(self) -> int: + """Last change triggered by.""" + return self._changed_by + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + attributes = { + ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times, + } + return attributes + + @callback + def clear_unlock_state(self, _): + """Clear unlock state automatically.""" + self._state = STATE_LOCKED + self.async_schedule_update_ha_state() + + def parse_data(self, data, raw_data): + """Parse data sent by gateway.""" + value = data.get(VERIFIED_WRONG_KEY) + if value is not None: + self._verified_wrong_times = int(value) + return True + + for key in (FINGER_KEY, PASSWORD_KEY, CARD_KEY): + value = data.get(key) + if value is not None: + self._changed_by = int(value) + self._verified_wrong_times = 0 + self._state = STATE_UNLOCKED + async_call_later(self.hass, UNLOCK_MAINTAIN_TIME, + self.clear_unlock_state) + return True + + return False diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 27bd496a3f0..ae3a4e0be72 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -139,7 +139,8 @@ def setup(hass, config): xiaomi.listen() _LOGGER.debug("Gateways discovered. Listening for broadcasts") - for component in ['binary_sensor', 'sensor', 'switch', 'light', 'cover']: + for component in ['binary_sensor', 'sensor', 'switch', 'light', 'cover', + 'lock']: discovery.load_platform(hass, component, DOMAIN, {}, config) def stop_xiaomi(event): From ad4994220124e5823e4924f275a6728b08f99800 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 23 May 2018 16:47:58 +0200 Subject: [PATCH 790/924] Upgrade TwitterAPI to 2.5.3 (#14596) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index 9489e05cfa5..f81a83325ce 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.5.0'] +REQUIREMENTS = ['TwitterAPI==2.5.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 016e2f9ce2d..0879c539ea8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,7 +61,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.5.0 +TwitterAPI==2.5.3 # homeassistant.components.sensor.waze_travel_time WazeRouteCalculator==0.5 From c13ebacce16bf49c0d5117b56c44ab99bc228bce Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 23 May 2018 14:36:51 -0400 Subject: [PATCH 791/924] Remove nma component (#14594) * Remove nma component * Update .coveragerc --- .coveragerc | 1 - homeassistant/components/notify/nma.py | 65 -------------------------- 2 files changed, 66 deletions(-) delete mode 100644 homeassistant/components/notify/nma.py diff --git a/.coveragerc b/.coveragerc index a31af5f296c..eb73cb66c30 100644 --- a/.coveragerc +++ b/.coveragerc @@ -534,7 +534,6 @@ omit = homeassistant/components/notify/message_bird.py homeassistant/components/notify/mycroft.py homeassistant/components/notify/nfandroidtv.py - homeassistant/components/notify/nma.py homeassistant/components/notify/prowl.py homeassistant/components/notify/pushbullet.py homeassistant/components/notify/pushetta.py diff --git a/homeassistant/components/notify/nma.py b/homeassistant/components/notify/nma.py deleted file mode 100644 index e81dc457a81..00000000000 --- a/homeassistant/components/notify/nma.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -NMA (Notify My Android) notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.nma/ -""" -import logging -import xml.etree.ElementTree as ET - -import requests -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) -_RESOURCE = 'https://www.notifymyandroid.com/publicapi/' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the NMA notification service.""" - parameters = { - 'apikey': config[CONF_API_KEY], - } - response = requests.get( - '{}{}'.format(_RESOURCE, 'verify'), params=parameters, timeout=5) - tree = ET.fromstring(response.content) - - if tree[0].tag == 'error': - _LOGGER.error("Wrong API key supplied: %s", tree[0].text) - return None - - return NmaNotificationService(config[CONF_API_KEY]) - - -class NmaNotificationService(BaseNotificationService): - """Implement the notification service for NMA.""" - - def __init__(self, api_key): - """Initialize the service.""" - self._api_key = api_key - - def send_message(self, message="", **kwargs): - """Send a message to a user.""" - data = { - 'apikey': self._api_key, - 'application': 'home-assistant', - 'event': kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT), - 'description': message, - 'priority': 0, - } - - response = requests.get( - '{}{}'.format(_RESOURCE, 'notify'), params=data, timeout=5) - tree = ET.fromstring(response.content) - - if tree[0].tag == 'error': - _LOGGER.exception( - "Unable to perform request. Error: %s", tree[0].text) From 349823444834d145cc055a76ce578bb81e3f9555 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 23 May 2018 12:40:33 -0700 Subject: [PATCH 792/924] Add Nest away binary sensor and eta sensor (#14406) --- .../components/binary_sensor/nest.py | 24 ++++++++++++-- homeassistant/components/nest.py | 25 ++++++++++---- homeassistant/components/sensor/nest.py | 33 ++++++++++++++----- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 4089f3a2eaf..2a1732cd9f0 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -29,6 +29,16 @@ CAMERA_BINARY_TYPES = [ 'person_detected', ] +STRUCTURE_BINARY_TYPES = [ + 'away', + # 'security_state', # wait for pending python-nest update +] + +STRUCTURE_BINARY_STATE_MAP = { + 'away': {'away': True, 'home': False}, + 'security_state': {'deter': True, 'ok': False}, +} + _BINARY_TYPES_DEPRECATED = [ 'hvac_ac_state', 'hvac_aux_heater_state', @@ -41,7 +51,7 @@ _BINARY_TYPES_DEPRECATED = [ ] _VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ - + CAMERA_BINARY_TYPES + + CAMERA_BINARY_TYPES + STRUCTURE_BINARY_TYPES _LOGGER = logging.getLogger(__name__) @@ -68,6 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error(wstr) sensors = [] + for structure in nest.structures(): + sensors += [NestBinarySensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_BINARY_TYPES] device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) @@ -88,7 +102,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors += [NestActivityZoneSensor(structure, device, activity_zone)] - add_devices(sensors, True) @@ -102,7 +115,12 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): def update(self): """Retrieve latest state.""" - self._state = bool(getattr(self.device, self.variable)) + value = getattr(self.device, self.variable) + if self.variable in STRUCTURE_BINARY_TYPES: + self._state = bool(STRUCTURE_BINARY_STATE_MAP + [self.variable][value]) + else: + self._state = bool(value) class NestActivityZoneSensor(NestBinarySensor): diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index e7d2ba90438..2500755d495 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -168,6 +168,19 @@ class NestDevice(object): self.local_structure = conf[CONF_STRUCTURE] _LOGGER.debug("Structures to include: %s", self.local_structure) + def structures(self): + """Generate a list of structures.""" + try: + for structure in self.nest.structures: + if structure.name in self.local_structure: + yield structure + else: + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) + except socket.error: + _LOGGER.error( + "Connection error logging into the nest web service.") + def thermostats(self): """Generate a list of thermostats and their location.""" try: @@ -188,10 +201,10 @@ class NestDevice(object): for structure in self.nest.structures: if structure.name in self.local_structure: for device in structure.smoke_co_alarms: - yield(structure, device) + yield (structure, device) else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, self.local_structure) + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") @@ -202,10 +215,10 @@ class NestDevice(object): for structure in self.nest.structures: if structure.name in self.local_structure: for device in structure.cameras: - yield(structure, device) + yield (structure, device) else: - _LOGGER.info("Ignoring structure %s, not in %s", - structure.name, self.local_structure) + _LOGGER.debug("Ignoring structure %s, not in %s", + structure.name, self.local_structure) except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 9ce50dc61e5..0de2e2e0cdb 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -36,10 +36,15 @@ PROTECT_VARS_DEPRECATED = ['battery_level'] SENSOR_TEMP_TYPES = ['temperature', 'target'] +STRUCTURE_SENSOR_TYPES = ['eta'] + +VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} + _SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \ + list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED -_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS +_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS \ + + STRUCTURE_SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -73,6 +78,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error(wstr) all_sensors = [] + for structure in nest.structures(): + all_sensors += [NestBasicSensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_SENSOR_TYPES] for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): sensors = [NestBasicSensor(structure, device, variable) for variable in conditions @@ -94,13 +103,20 @@ class NestSensor(Entity): def __init__(self, structure, device, variable): """Initialize the sensor.""" self.structure = structure - self.device = device self.variable = variable - # device specific - self._location = self.device.where - self._name = "{} {}".format(self.device.name_long, - self.variable.replace("_", " ")) + if device is not None: + # device specific + self.device = device + self._location = self.device.where + self._name = "{} {}".format(self.device.name_long, + self.variable.replace('_', ' ')) + else: + # structure only + self.device = structure + self._name = "{} {}".format(self.structure.name, + self.variable.replace('_', ' ')) + self._state = None self._unit = None @@ -127,8 +143,9 @@ class NestBasicSensor(NestSensor): """Retrieve latest state.""" self._unit = SENSOR_UNITS.get(self.variable, None) - if self.variable == 'operation_mode': - self._state = getattr(self.device, "mode") + if self.variable in VARIABLE_NAME_MAPPING: + self._state = getattr(self.device, + VARIABLE_NAME_MAPPING[self.variable]) else: self._state = getattr(self.device, self.variable) From 5205354cb7dd947de219b322f0e12880dee55dc5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 23 May 2018 23:58:35 -0600 Subject: [PATCH 793/924] Adds a device class of 'garage' to MyQ covers (#14602) --- homeassistant/components/cover/myq.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/cover/myq.py b/homeassistant/components/cover/myq.py index f07d3849fae..1e2ec43181c 100644 --- a/homeassistant/components/cover/myq.py +++ b/homeassistant/components/cover/myq.py @@ -69,6 +69,11 @@ class MyQDevice(CoverDevice): self._name = device['name'] self._status = STATE_CLOSED + @property + def device_class(self): + """Define this cover as a garage door.""" + return 'garage' + @property def should_poll(self): """Poll for state.""" From 36da82aa8dab846f3aaec966909d17d975e47ef2 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Thu, 24 May 2018 03:25:27 -0400 Subject: [PATCH 794/924] Add Iperf3 client sensor (#14213) --- .coveragerc | 1 + Dockerfile | 1 + homeassistant/components/sensor/iperf3.py | 178 +++++++++++++++++++++ requirements_all.txt | 3 + virtualization/Docker/Dockerfile.dev | 1 + virtualization/Docker/scripts/iperf3 | 11 ++ virtualization/Docker/setup_docker_prereqs | 5 + 7 files changed, 200 insertions(+) create mode 100644 homeassistant/components/sensor/iperf3.py create mode 100755 virtualization/Docker/scripts/iperf3 diff --git a/.coveragerc b/.coveragerc index eb73cb66c30..3ccfdeb3569 100644 --- a/.coveragerc +++ b/.coveragerc @@ -619,6 +619,7 @@ omit = homeassistant/components/sensor/imap_email_content.py homeassistant/components/sensor/imap.py homeassistant/components/sensor/influxdb.py + homeassistant/components/sensor/iperf3.py homeassistant/components/sensor/irish_rail_transport.py homeassistant/components/sensor/kwb.py homeassistant/components/sensor/lacrosse.py diff --git a/Dockerfile b/Dockerfile index 5081b4ba721..75d9e9eb716 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py new file mode 100644 index 00000000000..1a209faf17f --- /dev/null +++ b/homeassistant/components/sensor/iperf3.py @@ -0,0 +1,178 @@ +""" +Support for Iperf3 network measurement tool. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.iperf3/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA +from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, + CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['iperf3==0.1.10'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_PROTOCOL = 'Protocol' +ATTR_REMOTE_HOST = 'Remote Server' +ATTR_REMOTE_PORT = 'Remote Port' +ATTR_VERSION = 'Version' + +CONF_ATTRIBUTION = 'Data retrieved using Iperf3' +CONF_DURATION = 'duration' + +DEFAULT_DURATION = 10 +DEFAULT_PORT = 5201 + +IPERF3_DATA = 'iperf3' + +SCAN_INTERVAL = timedelta(minutes=30) + +SERVICE_NAME = 'iperf3_update' + +ICON = 'mdi:speedometer' + +SENSOR_TYPES = { + 'download': ['Download', 'Mbit/s'], + 'upload': ['Upload', 'Mbit/s'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(list(SENSOR_TYPES))]), + vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), +}) + + +SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Iperf3 sensor.""" + if hass.data.get(IPERF3_DATA) is None: + hass.data[IPERF3_DATA] = {} + hass.data[IPERF3_DATA]['sensors'] = [] + + dev = [] + for sensor in config[CONF_MONITORED_CONDITIONS]: + dev.append( + Iperf3Sensor(config[CONF_HOST], + config[CONF_PORT], + config[CONF_DURATION], + sensor)) + + hass.data[IPERF3_DATA]['sensors'].extend(dev) + add_devices(dev) + + def _service_handler(service): + """Update service for manual updates.""" + entity_id = service.data.get('entity_id') + all_iperf3_sensors = hass.data[IPERF3_DATA]['sensors'] + + for sensor in all_iperf3_sensors: + if entity_id is not None: + if sensor.entity_id == entity_id: + sensor.update() + sensor.schedule_update_ha_state() + break + else: + sensor.update() + sensor.schedule_update_ha_state() + + for sensor in dev: + hass.services.register(DOMAIN, SERVICE_NAME, _service_handler, + schema=SERVICE_SCHEMA) + + +class Iperf3Sensor(Entity): + """A Iperf3 sensor implementation.""" + + def __init__(self, server, port, duration, sensor_type): + """Initialize the sensor.""" + self._attrs = { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + } + self._name = \ + "{} {}".format(SENSOR_TYPES[sensor_type][0], server) + self._state = None + self._sensor_type = sensor_type + self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._port = port + self._server = server + self._duration = duration + self.result = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.result is not None: + self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + self._attrs[ATTR_PROTOCOL] = self.result.protocol + self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host + self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port + self._attrs[ATTR_VERSION] = self.result.version + return self._attrs + + def update(self): + """Get the latest data and update the states.""" + import iperf3 + client = iperf3.Client() + client.duration = self._duration + client.server_hostname = self._server + client.port = self._port + client.verbose = False + + # when testing download bandwith, reverse must be True + if self._sensor_type == 'download': + client.reverse = True + + try: + self.result = client.run() + except (OSError, AttributeError) as error: + self.result = None + _LOGGER.error("Iperf3 sensor error: %s", error) + return + + if self.result is not None and \ + hasattr(self.result, 'error') and \ + self.result.error is not None: + _LOGGER.error("Iperf3 sensor error: %s", self.result.error) + self.result = None + return + + if self._sensor_type == 'download': + self._state = round(self.result.received_Mbps, 2) + + elif self._sensor_type == 'upload': + self._state = round(self.result.sent_Mbps, 2) + + @property + def icon(self): + """Return icon.""" + return ICON diff --git a/requirements_all.txt b/requirements_all.txt index 0879c539ea8..7a40a1aa48e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -451,6 +451,9 @@ insteonlocal==0.53 # homeassistant.components.insteon_plm insteonplm==0.9.2 +# homeassistant.components.sensor.iperf3 +iperf3==0.1.10 + # homeassistant.components.verisure jsonpath==0.75 diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 06676140702..d0599c2e74c 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -13,6 +13,7 @@ LABEL maintainer="Paulus Schoutsen " #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP no #ENV INSTALL_SSOCR no +#ENV INSTALL_IPERF3 no VOLUME /config diff --git a/virtualization/Docker/scripts/iperf3 b/virtualization/Docker/scripts/iperf3 new file mode 100755 index 00000000000..2d9d5a33761 --- /dev/null +++ b/virtualization/Docker/scripts/iperf3 @@ -0,0 +1,11 @@ +#!/bin/bash +# Sets up iperf3. + +# Stop on errors +set -e + +PACKAGES=( + iperf3 +) + +apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 302dfba2e1d..3bb4136c991 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -10,6 +10,7 @@ INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" +INSTALL_IPERF3="${INSTALL_IPERF3:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -64,6 +65,10 @@ if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi +if [ "$INSTALL_IPERF3" == "yes" ]; then + virtualization/Docker/scripts/iperf3 +fi + # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From 3a487e54a2ee7e21438d71d2c2deb86c169fc270 Mon Sep 17 00:00:00 2001 From: Robert Beal Date: Thu, 24 May 2018 16:16:35 +0100 Subject: [PATCH 795/924] Upgrade linode-api to 4.1.9b1 (#13863) (#14610) --- homeassistant/components/linode.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linode.py b/homeassistant/components/linode.py index 9e87c002482..962e30774b8 100644 --- a/homeassistant/components/linode.py +++ b/homeassistant/components/linode.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['linode-api==4.1.4b2'] +REQUIREMENTS = ['linode-api==4.1.9b1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 7a40a1aa48e..42e512f08e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -502,7 +502,7 @@ lightify==1.0.6.1 limitlessled==1.1.0 # homeassistant.components.linode -linode-api==4.1.4b2 +linode-api==4.1.9b1 # homeassistant.components.media_player.liveboxplaytv liveboxplaytv==2.0.2 From 4fb4838bdece49d0575a59d1efa95218abb68a4b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 13:08:12 -0400 Subject: [PATCH 796/924] Update frontend to 20180524.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 04e4e0dae48..8ee6ce549a4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180521.0'] +REQUIREMENTS = ['home-assistant-frontend==20180524.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 42e512f08e2..c755f15f35f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a12cba141a1..275a1974104 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From fa9b9105a813285dce9cdb12638e83188285b3bb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 14:24:14 -0400 Subject: [PATCH 797/924] Fix hue discovery popping up (#14614) * Fix hue discovery popping up * Fix result * Fix tests --- homeassistant/auth.py | 3 +++ homeassistant/config_entries.py | 15 +++++++++------ homeassistant/data_entry_flow.py | 8 ++++---- tests/test_config_entries.py | 20 ++++++++++++++++++++ tests/test_data_entry_flow.py | 3 ++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 7c01776b7b1..5e434b74ca8 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -347,6 +347,9 @@ class AuthManager: async def _async_finish_login_flow(self, result): """Result of a credential login flow.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + auth_provider = self._providers[result['handler']] return await auth_provider.async_get_or_create_credentials( result['data']) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1350cd7d76a..8a73e424fb5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -347,6 +347,15 @@ class ConfigEntries: async def _async_finish_flow(self, result): """Finish a config flow and add an entry.""" + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + entry = ConfigEntry( version=result['version'], domain=result['handler'], @@ -370,12 +379,6 @@ class ConfigEntries: if result['source'] not in DISCOVERY_SOURCES: return entry - # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent - in self.hass.config_entries.flow.async_progress()): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID) - return entry async def _async_create_flow(self, handler, *, source, data): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e9580aba273..5095297e795 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -110,11 +110,11 @@ class FlowManager: # Abort and Success results both finish the flow self._progress.pop(flow.flow_id) - if result['type'] == RESULT_TYPE_ABORT: - return result - # We pass a copy of the result because we're mutating our version - result['result'] = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(dict(result)) + + if result['type'] == RESULT_TYPE_CREATE_ENTRY: + result['result'] = entry return result diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1518706db55..84bd0771542 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -284,3 +284,23 @@ async def test_discovery_notification(hass): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_discovery_notification_not_created(hass): + """Test that we not create a notification when discovery is aborted.""" + loader.set_component(hass, 'test', MockModule('test')) + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + return self.async_abort(reason='test') + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 6d3e41436c5..894fd4d7194 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -21,7 +21,8 @@ def manager(): return handler() async def async_add_entry(result): - entries.append(result) + if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + entries.append(result) manager = data_entry_flow.FlowManager( None, async_create_flow, async_add_entry) From 45d1d30a8b698a5c8b7495845512c1a345432216 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 13:08:12 -0400 Subject: [PATCH 798/924] Update frontend to 20180524.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 04e4e0dae48..8ee6ce549a4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180521.0'] +REQUIREMENTS = ['home-assistant-frontend==20180524.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index b86398bb3e8..94f49fb2454 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4fe31b0639..57224aa4233 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a4d45c46e8859268b185a311c64282fe824e5348 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 21 May 2018 12:00:01 -0700 Subject: [PATCH 799/924] Fix ISY moisure sensors showing unknown until a leak is detected (#14496) * Fix ISY leak sensors always showing UNKNOWN until a leak is detected Added some logic that handles both moisture sensors and door/window sensors * Handle edge case of leak sensor status update after ISY reboot If a leak sensor is unknown, due to a recent reboot of the ISY, the status will get updated to dry upon the first heartbeat. This status update is the only way that a leak sensor's status changes without an accompanying Control event, so we need to watch for it. * Fixes from overnight testing State was checking the incorrect parameter, and wasn't calling schedule update * Remove leftover debug log line * Remove unnecessary pylint instruction * Remove access of protected property We can't cast _.status directly to a bool for some unknown reason (possibly with the VarEvents library), but casting to an int then bool does work. --- .../components/binary_sensor/isy994.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index fb86244acf3..09f1739cba7 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None + self._status_was_unknown = True else: self._computed_state = bool(self._node.status._val) + self._status_was_unknown = False @asyncio.coroutine def async_added_to_hass(self) -> None: @@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is - # in use for this device. Therefore, we cannot determine the state - # of the sensor until we receive our first ON event. - self._computed_state = None + # in use for this device. Next we need to check to see if the + # negative and positive nodes disagree on the state (both ON or + # both OFF). + if self._negative_node.status._val == self._node.status._val: + # The states disagree, therefore we cannot determine the state + # of the sensor until we receive our first ON event. + self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" @@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self.schedule_update_ha_state() self._heartbeat() - # pylint: disable=unused-argument def on_update(self, event: object) -> None: - """Ignore primary node status updates. + """Primary node status updates. - We listen directly to the Control events on all nodes for this - device. + We MOSTLY ignore these updates, as we listen directly to the Control + events on all nodes for this device. However, there is one edge case: + If a leak sensor is unknown, due to a recent reboot of the ISY, the + status will get updated to dry upon the first heartbeat. This status + update is the only way that a leak sensor's status changes without + an accompanying Control event, so we need to watch for it. """ - pass + if self._status_was_unknown and self._computed_state is None: + self._computed_state = bool(int(self._node.status)) + self._status_was_unknown = False + self.schedule_update_ha_state() + self._heartbeat() @property def value(self) -> object: From 69e86c29a6ede65f7baa61846435bf18bca24682 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 21 May 2018 19:46:20 -0400 Subject: [PATCH 800/924] Bump insteonplm version to fix device hanging (#14582) * Update inteonplm to 0.9.2 * Change to force Travis CI * Change to force Travis CI --- homeassistant/components/insteon_plm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 246e84ec71f..b86f80cbee7 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.1'] +REQUIREMENTS = ['insteonplm==0.9.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 94f49fb2454..0bd40c03fae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.1 +insteonplm==0.9.2 # homeassistant.components.verisure jsonpath==0.75 From ef35b8d42879d8209f7e56abc660353a0df1a810 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 14:24:14 -0400 Subject: [PATCH 801/924] Fix hue discovery popping up (#14614) * Fix hue discovery popping up * Fix result * Fix tests --- homeassistant/auth.py | 3 +++ homeassistant/config_entries.py | 15 +++++++++------ homeassistant/data_entry_flow.py | 8 ++++---- tests/test_config_entries.py | 20 ++++++++++++++++++++ tests/test_data_entry_flow.py | 3 ++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 7c01776b7b1..5e434b74ca8 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -347,6 +347,9 @@ class AuthManager: async def _async_finish_login_flow(self, result): """Result of a credential login flow.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + auth_provider = self._providers[result['handler']] return await auth_provider.async_get_or_create_credentials( result['data']) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1350cd7d76a..8a73e424fb5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -347,6 +347,15 @@ class ConfigEntries: async def _async_finish_flow(self, result): """Finish a config flow and add an entry.""" + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + entry = ConfigEntry( version=result['version'], domain=result['handler'], @@ -370,12 +379,6 @@ class ConfigEntries: if result['source'] not in DISCOVERY_SOURCES: return entry - # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent - in self.hass.config_entries.flow.async_progress()): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID) - return entry async def _async_create_flow(self, handler, *, source, data): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e9580aba273..5095297e795 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -110,11 +110,11 @@ class FlowManager: # Abort and Success results both finish the flow self._progress.pop(flow.flow_id) - if result['type'] == RESULT_TYPE_ABORT: - return result - # We pass a copy of the result because we're mutating our version - result['result'] = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(dict(result)) + + if result['type'] == RESULT_TYPE_CREATE_ENTRY: + result['result'] = entry return result diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1518706db55..84bd0771542 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -284,3 +284,23 @@ async def test_discovery_notification(hass): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_discovery_notification_not_created(hass): + """Test that we not create a notification when discovery is aborted.""" + loader.set_component(hass, 'test', MockModule('test')) + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + return self.async_abort(reason='test') + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 6d3e41436c5..894fd4d7194 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -21,7 +21,8 @@ def manager(): return handler() async def async_add_entry(result): - entries.append(result) + if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + entries.append(result) manager = data_entry_flow.FlowManager( None, async_create_flow, async_add_entry) From 43d2e436b9750fcaf5392062b252d686a6ca30a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 14:25:15 -0400 Subject: [PATCH 802/924] Version bump to 0.70.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73dc0db7aac..dafe7e90db5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 2cd127921a5c65e135fcfd63c1aa16c075dfe622 Mon Sep 17 00:00:00 2001 From: Gregory Benner Date: Fri, 25 May 2018 00:39:41 -0400 Subject: [PATCH 803/924] Update pyrainbird (#14617) --- homeassistant/components/rainbird.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainbird.py b/homeassistant/components/rainbird.py index 76dda6fd366..bbce7f752af 100644 --- a/homeassistant/components/rainbird.py +++ b/homeassistant/components/rainbird.py @@ -11,7 +11,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.const import (CONF_HOST, CONF_PASSWORD) -REQUIREMENTS = ['pyrainbird==0.1.3'] +REQUIREMENTS = ['pyrainbird==0.1.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index c755f15f35f..d50fe829712 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ pypollencom==1.1.2 pyqwikswitch==0.8 # homeassistant.components.rainbird -pyrainbird==0.1.3 +pyrainbird==0.1.6 # homeassistant.components.sabnzbd pysabnzbd==1.0.1 From 6e5c541a001a27a2428dc779c9d41f8fbb5e78bb Mon Sep 17 00:00:00 2001 From: bastshoes Date: Fri, 25 May 2018 10:58:53 +0300 Subject: [PATCH 804/924] Add support container status for Glances on RPi3 (#14529) * Add support container status for Glances on RPi3 Glances on RPi3 return different container status. ``` "containers": [ { "Status": "Up 2 hours", "name": "HASS", "io": { "iow": 0, "time_since_update": 5.1789350509643555, "cumulative_ior": 94208, "ior": 0, "cumulative_iow": 4096 }, ``` This small PR adds support dealing with this differences. * Making line shorter * Fixing indentation * Fix lint error * Fix ident * Fix intend --- homeassistant/components/sensor/glances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/glances.py b/homeassistant/components/sensor/glances.py index 3b6f3ddc99d..0de87bd17ea 100644 --- a/homeassistant/components/sensor/glances.py +++ b/homeassistant/components/sensor/glances.py @@ -161,7 +161,8 @@ class GlancesSensor(Entity): elif self.type == 'docker_active': count = 0 for container in value['docker']['containers']: - if container['Status'] == 'running': + if container['Status'] == 'running' or \ + 'Up' in container['Status']: count += 1 self._state = count elif self.type == 'docker_cpu_use': From d53a8c08238dd03b3574e873c8796eb0bc809011 Mon Sep 17 00:00:00 2001 From: Nik Klever Date: Fri, 25 May 2018 10:29:20 +0200 Subject: [PATCH 805/924] Adding illumination sensor (#14615) * Adding illumination sensor Adding Illumination sensor of 1wire device DS2438 (DEVICE_SENSOR type 26) according to [OWFS API](http://owfs.org/index.php?page=ds2438) * Correcting typo illumination -> illuminance --- homeassistant/components/sensor/onewire.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/onewire.py b/homeassistant/components/sensor/onewire.py index 8a07d3484d5..43105d54e38 100644 --- a/homeassistant/components/sensor/onewire.py +++ b/homeassistant/components/sensor/onewire.py @@ -28,7 +28,8 @@ DEVICE_SENSORS = {'10': {'temperature': 'temperature'}, '22': {'temperature': 'temperature'}, '26': {'temperature': 'temperature', 'humidity': 'humidity', - 'pressure': 'B1-R1-A/pressure'}, + 'pressure': 'B1-R1-A/pressure', + 'illuminance': 'S3-R1-A/illuminance'}, '28': {'temperature': 'temperature'}, '3B': {'temperature': 'temperature'}, '42': {'temperature': 'temperature'}} @@ -37,6 +38,7 @@ SENSOR_TYPES = { 'temperature': ['temperature', TEMP_CELSIUS], 'humidity': ['humidity', '%'], 'pressure': ['pressure', 'mb'], + 'illuminance': ['illuminance', 'lux'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ From a9f19a16ee3e2a625832ffda6a0520ff2035edc5 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 25 May 2018 05:37:20 -0400 Subject: [PATCH 806/924] Add HomeKit support for media players (#14446) --- homeassistant/components/homekit/__init__.py | 14 +- homeassistant/components/homekit/const.py | 6 + .../components/homekit/type_media_players.py | 142 ++++++++++++++++++ homeassistant/components/homekit/util.py | 43 +++++- .../homekit/test_get_accessories.py | 22 ++- .../homekit/test_type_media_players.py | 106 +++++++++++++ tests/components/homekit/test_util.py | 32 +++- 7 files changed, 354 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/homekit/type_media_players.py create mode 100644 tests/components/homekit/test_type_media_players.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 202f9694689..561301cdb6d 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + CONF_IP_ADDRESS, CONF_MODE, CONF_NAME, CONF_PORT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -25,7 +25,8 @@ from .const import ( CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) -from .util import show_setup_message, validate_entity_config +from .util import ( + show_setup_message, validate_entity_config, validate_media_player_modes) TYPES = Registry() _LOGGER = logging.getLogger(__name__) @@ -125,6 +126,11 @@ def get_accessory(hass, state, aid, config): elif state.domain == 'lock': a_type = 'Lock' + elif state.domain == 'media_player': + validate_media_player_modes(state, config) + if config.get(CONF_MODE): + a_type = 'MediaPlayer' + elif state.domain == 'sensor': unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) device_class = state.attributes.get(ATTR_DEVICE_CLASS) @@ -208,8 +214,8 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 type_covers, type_fans, type_lights, type_locks, - type_security_systems, type_sensors, type_switches, - type_thermostats) + type_media_players, type_security_systems, type_sensors, + type_switches, type_thermostats) for state in self.hass.states.all(): self.add_bridge_accessory(state) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 21cad2d9cf7..f59ee5488ec 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -23,6 +23,12 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' +# #### Media Player Modes #### +ON_OFF = 'on_off' +PLAY_PAUSE = 'play_pause' +PLAY_STOP = 'play_stop' +TOGGLE_MUTE = 'toggle_mute' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py new file mode 100644 index 00000000000..563cd0cb25c --- /dev/null +++ b/homeassistant/components/homekit/type_media_players.py @@ -0,0 +1,142 @@ +"""Class to hold all media player accessories.""" +import logging + +from pyhap.const import CATEGORY_SWITCH + +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_MODE, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, + STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + CHAR_NAME, CHAR_ON, ON_OFF, PLAY_PAUSE, PLAY_STOP, SERV_SWITCH, + TOGGLE_MUTE) + +_LOGGER = logging.getLogger(__name__) + +MODE_FRIENDLY_NAME = {ON_OFF: 'Power', + PLAY_PAUSE: 'Play/Pause', + PLAY_STOP: 'Play/Stop', + TOGGLE_MUTE: 'Mute'} + + +@TYPES.register('MediaPlayer') +class MediaPlayer(HomeAccessory): + """Generate a Media Player accessory.""" + + def __init__(self, *args): + """Initialize a Switch accessory object.""" + super().__init__(*args, category=CATEGORY_SWITCH) + self._flag = {ON_OFF: False, PLAY_PAUSE: False, + PLAY_STOP: False, TOGGLE_MUTE: False} + self.chars = {ON_OFF: None, PLAY_PAUSE: None, + PLAY_STOP: None, TOGGLE_MUTE: None} + modes = self.config[CONF_MODE] + + if ON_OFF in modes: + serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_on_off.configure_char( + CHAR_NAME, value=self.generate_service_name(ON_OFF)) + self.chars[ON_OFF] = serv_on_off.configure_char( + CHAR_ON, value=False, setter_callback=self.set_on_off) + + if PLAY_PAUSE in modes: + serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_play_pause.configure_char( + CHAR_NAME, value=self.generate_service_name(PLAY_PAUSE)) + self.chars[PLAY_PAUSE] = serv_play_pause.configure_char( + CHAR_ON, value=False, setter_callback=self.set_play_pause) + + if PLAY_STOP in modes: + serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_play_stop.configure_char( + CHAR_NAME, value=self.generate_service_name(PLAY_STOP)) + self.chars[PLAY_STOP] = serv_play_stop.configure_char( + CHAR_ON, value=False, setter_callback=self.set_play_stop) + + if TOGGLE_MUTE in modes: + serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME) + serv_toggle_mute.configure_char( + CHAR_NAME, value=self.generate_service_name(TOGGLE_MUTE)) + self.chars[TOGGLE_MUTE] = serv_toggle_mute.configure_char( + CHAR_ON, value=False, setter_callback=self.set_toggle_mute) + + def generate_service_name(self, mode): + """Generate name for individual service.""" + return '{} {}'.format(self.display_name, MODE_FRIENDLY_NAME[mode]) + + def set_on_off(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "on_off" to %s', + self.entity_id, value) + self._flag[ON_OFF] = True + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_play_pause(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "play_pause" to %s', + self.entity_id, value) + self._flag[PLAY_PAUSE] = True + service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_play_stop(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "play_stop" to %s', + self.entity_id, value) + self._flag[PLAY_STOP] = True + service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_toggle_mute(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', + self.entity_id, value) + self._flag[TOGGLE_MUTE] = True + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_MEDIA_VOLUME_MUTED: value} + self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = new_state.state + + if self.chars[ON_OFF]: + hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None') + if not self._flag[ON_OFF]: + _LOGGER.debug('%s: Set current state for "on_off" to %s', + self.entity_id, hk_state) + self.chars[ON_OFF].set_value(hk_state) + self._flag[ON_OFF] = False + + if self.chars[PLAY_PAUSE]: + hk_state = current_state == STATE_PLAYING + if not self._flag[PLAY_PAUSE]: + _LOGGER.debug('%s: Set current state for "play_pause" to %s', + self.entity_id, hk_state) + self.chars[PLAY_PAUSE].set_value(hk_state) + self._flag[PLAY_PAUSE] = False + + if self.chars[PLAY_STOP]: + hk_state = current_state == STATE_PLAYING + if not self._flag[PLAY_STOP]: + _LOGGER.debug('%s: Set current state for "play_stop" to %s', + self.entity_id, hk_state) + self.chars[PLAY_STOP].set_value(hk_state) + self._flag[PLAY_STOP] = False + + if self.chars[TOGGLE_MUTE]: + current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) + if not self._flag[TOGGLE_MUTE]: + _LOGGER.debug('%s: Set current state for "toggle_mute" to %s', + self.entity_id, current_state) + self.chars[TOGGLE_MUTE].set_value(current_state) + self._flag[TOGGLE_MUTE] = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 447257f9e8f..57ce562ce21 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -3,15 +3,21 @@ import logging import voluptuous as vol +from homeassistant.components.media_player import ( + SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE) from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util -from .const import HOMEKIT_NOTIFY_ID +from .const import ( + HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) _LOGGER = logging.getLogger(__name__) +MEDIA_PLAYER_MODES = (ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" @@ -34,10 +40,43 @@ def validate_entity_config(values): code = config.get(ATTR_CODE) params[ATTR_CODE] = cv.string(code) if code else None + if domain == 'media_player': + mode = config.get(CONF_MODE) + params[CONF_MODE] = cv.ensure_list(mode) + for key in params[CONF_MODE]: + if key not in MEDIA_PLAYER_MODES: + raise vol.Invalid( + 'Invalid mode: "{}", valid modes are: "{}".' + .format(key, MEDIA_PLAYER_MODES)) + entities[entity] = params return entities +def validate_media_player_modes(state, config): + """Validate modes for media playeres.""" + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + supported_modes = [] + if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF): + supported_modes.append(ON_OFF) + if features & (SUPPORT_PLAY | SUPPORT_PAUSE): + supported_modes.append(PLAY_PAUSE) + if features & (SUPPORT_PLAY | SUPPORT_STOP): + supported_modes.append(PLAY_STOP) + if features & SUPPORT_VOLUME_MUTE: + supported_modes.append(TOGGLE_MUTE) + + if not config.get(CONF_MODE): + config[CONF_MODE] = supported_modes + return + + for mode in config[CONF_MODE]: + if mode not in supported_modes: + raise vol.Invalid('"{}" does not support mode: "{}".' + .format(state.entity_id, mode)) + + def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" pin = pincode.decode() diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 25a0dd3f1cb..6f6d39e477a 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -2,15 +2,20 @@ from unittest.mock import patch, Mock import pytest +import voluptuous as vol from homeassistant.core import State from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) +from homeassistant.components.media_player import ( + SUPPORT_TURN_OFF, SUPPORT_TURN_ON) from homeassistant.components.homekit import get_accessory, TYPES +from homeassistant.components.homekit.const import ON_OFF from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_MODE, CONF_NAME, TEMP_CELSIUS, + TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -24,6 +29,18 @@ def test_not_supported(caplog): assert 'invalid aid' in caplog.records[0].msg +def test_not_supported_media_player(): + """Test if mode isn't supported and if no supported modes.""" + # selected mode for entity not supported + with pytest.raises(vol.Invalid): + entity_state = State('media_player.demo', 'on') + get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]}) + + # no supported modes for entity + entity_state = State('media_player.demo', 'on') + assert get_accessory(None, entity_state, 2, {}) is None + + @pytest.mark.parametrize('config, name', [ ({CONF_NAME: 'Customize Name'}, 'Customize Name'), ]) @@ -40,6 +57,9 @@ def test_customize_options(config, name): ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), + ('MediaPlayer', 'media_player.test', 'on', + {ATTR_SUPPORTED_FEATURES: SUPPORT_TURN_ON | SUPPORT_TURN_OFF}, + {CONF_MODE: [ON_OFF]}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py new file mode 100644 index 00000000000..03135b1418e --- /dev/null +++ b/tests/components/homekit/test_type_media_players.py @@ -0,0 +1,106 @@ +"""Test different accessory types: Media Players.""" + +from homeassistant.components.media_player import ( + ATTR_MEDIA_VOLUME_MUTED, DOMAIN) +from homeassistant.components.homekit.type_media_players import MediaPlayer +from homeassistant.components.homekit.const import ( + ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_MODE, SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_VOLUME_MUTE, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, + STATE_PLAYING) + +from tests.common import async_mock_service + + +async def test_media_player_set_state(hass): + """Test if accessory and HA are updated accordingly.""" + config = {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + entity_id = 'media_player.test' + + hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, + ATTR_MEDIA_VOLUME_MUTED: False}) + await hass.async_block_till_done() + acc = MediaPlayer(hass, 'MediaPlayer', entity_id, 2, config) + await hass.async_add_job(acc.run) + + assert acc.aid == 2 + assert acc.category == 8 # Switch + + assert acc.chars[ON_OFF].value == 0 + assert acc.chars[PLAY_PAUSE].value == 0 + assert acc.chars[PLAY_STOP].value == 0 + assert acc.chars[TOGGLE_MUTE].value == 0 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True}) + await hass.async_block_till_done() + assert acc.chars[ON_OFF].value == 1 + assert acc.chars[TOGGLE_MUTE].value == 1 + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.chars[ON_OFF].value == 0 + + hass.states.async_set(entity_id, STATE_PLAYING) + await hass.async_block_till_done() + assert acc.chars[PLAY_PAUSE].value == 1 + assert acc.chars[PLAY_STOP].value == 1 + + hass.states.async_set(entity_id, STATE_PAUSED) + await hass.async_block_till_done() + assert acc.chars[PLAY_PAUSE].value == 0 + + hass.states.async_set(entity_id, STATE_IDLE) + await hass.async_block_till_done() + assert acc.chars[PLAY_STOP].value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + call_media_play = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PLAY) + call_media_pause = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_PAUSE) + call_media_stop = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_STOP) + call_toggle_mute = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) + + await hass.async_add_job(acc.chars[ON_OFF].client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[ON_OFF].client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, True) + await hass.async_block_till_done() + assert call_media_play + assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, False) + await hass.async_block_till_done() + assert call_media_pause + assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, True) + await hass.async_block_till_done() + assert call_media_play + assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, False) + await hass.async_block_till_done() + assert call_media_stop + assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, True) + await hass.async_block_till_done() + assert call_toggle_mute + assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id + assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True + + await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, False) + await hass.async_block_till_done() + assert call_toggle_mute + assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id + assert call_toggle_mute[1].data[ATTR_MEDIA_VOLUME_MUTED] is False diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0755e8f54d4..56a625e02d7 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,16 +2,20 @@ import pytest import voluptuous as vol -from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID +from homeassistant.core import State +from homeassistant.components.homekit.const import ( + HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, - show_setup_message, temperature_to_homekit, temperature_to_states) + show_setup_message, temperature_to_homekit, temperature_to_states, + validate_media_player_modes) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, STATE_UNKNOWN, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -20,7 +24,8 @@ def test_validate_entity_config(): """Test validate entities.""" configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] + {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, + {'media_player.test': {CONF_MODE: 'invalid_mode'}}] for conf in configs: with pytest.raises(vol.Invalid): @@ -39,6 +44,25 @@ def test_validate_entity_config(): assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ {'lock.demo': {ATTR_CODE: '1234'}} + assert vec({'media_player.demo': {}}) == \ + {'media_player.demo': {CONF_MODE: []}} + assert vec({'media_player.demo': {CONF_MODE: [ON_OFF]}}) == \ + {'media_player.demo': {CONF_MODE: [ON_OFF]}} + + +def test_validate_media_player_modes(): + """Test validate modes for media players.""" + config = {} + attrs = {ATTR_SUPPORTED_FEATURES: 20873} + entity_state = State('media_player.demo', 'on', attrs) + validate_media_player_modes(entity_state, config) + assert config == {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + + entity_state = State('media_player.demo', 'on') + config = {CONF_MODE: [ON_OFF]} + with pytest.raises(vol.Invalid): + validate_media_player_modes(entity_state, config) + def test_convert_to_float(): """Test convert_to_float method.""" From 143be49c668ac6512237f20520c1adae877f3f53 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 25 May 2018 05:38:48 -0400 Subject: [PATCH 807/924] Add HomeKit support for automations (#14595) --- homeassistant/components/homekit/__init__.py | 3 ++- tests/components/homekit/test_get_accessories.py | 6 ++++-- tests/components/homekit/test_type_switches.py | 7 ++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 561301cdb6d..f011a56a77b 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -149,7 +149,8 @@ def get_accessory(hass, state, aid, config): elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' - elif state.domain in ('switch', 'remote', 'input_boolean', 'script'): + elif state.domain in ('automation', 'input_boolean', 'remote', 'script', + 'switch'): a_type = 'Switch' if a_type is None: diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 6f6d39e477a..11b2d737a70 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -129,9 +129,11 @@ def test_type_sensors(type_name, entity_id, state, attrs): @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ - ('Switch', 'switch.test', 'on', {}), - ('Switch', 'remote.test', 'on', {}), + ('Switch', 'automation.test', 'on', {}), ('Switch', 'input_boolean.test', 'on', {}), + ('Switch', 'remote.test', 'on', {}), + ('Switch', 'script.test', 'on', {}), + ('Switch', 'switch.test', 'on', {}), ]) def test_type_switches(type_name, entity_id, state, attrs): """Test if switch types are associated correctly.""" diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 5fc0b6ce1b9..ff94c4b6a0b 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -9,7 +9,12 @@ from tests.common import async_mock_service @pytest.mark.parametrize('entity_id', [ - 'switch.test', 'remote.test', 'input_boolean.test']) + 'automation.test', + 'input_boolean.test', + 'remote.test', + 'script.test', + 'switch.test', +]) async def test_switch_set_state(hass, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] From b4f8d52fb10e64408849efff3f9d54d4e00e5b4c Mon Sep 17 00:00:00 2001 From: Marius Kotlarz Date: Fri, 25 May 2018 15:39:04 +0200 Subject: [PATCH 808/924] Add configurable decimal rounding of display value for CoinMarketCap sensor and upgrade to 5.0.3 (#14437) (#14604) --- .../components/sensor/coinmarketcap.py | 82 ++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_coinmarketcap.py | 5 +- tests/fixtures/coinmarketcap.json | 55 ++++++++----- 5 files changed, 93 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index 849e21a0901..f4b666f1e5c 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -13,64 +13,78 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_CURRENCY) + ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY) from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['coinmarketcap==4.2.1'] +REQUIREMENTS = ['coinmarketcap==5.0.3'] _LOGGER = logging.getLogger(__name__) -ATTR_24H_VOLUME = '24h_volume' +ATTR_VOLUME_24H = 'volume_24h' ATTR_AVAILABLE_SUPPLY = 'available_supply' +ATTR_CIRCULATING_SUPPLY = 'circulating_supply' ATTR_MARKET_CAP = 'market_cap' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' ATTR_PRICE = 'price' +ATTR_RANK = 'rank' ATTR_SYMBOL = 'symbol' ATTR_TOTAL_SUPPLY = 'total_supply' CONF_ATTRIBUTION = "Data provided by CoinMarketCap" +CONF_CURRENCY_ID = 'currency_id' +CONF_DISPLAY_CURRENCY_DECIMALS = 'display_currency_decimals' -DEFAULT_CURRENCY = 'bitcoin' +DEFAULT_CURRENCY_ID = 1 DEFAULT_DISPLAY_CURRENCY = 'USD' +DEFAULT_DISPLAY_CURRENCY_DECIMALS = 2 ICON = 'mdi:currency-usd' SCAN_INTERVAL = timedelta(minutes=15) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_CURRENCY, default=DEFAULT_CURRENCY): cv.string, + vol.Optional(CONF_CURRENCY_ID, default=DEFAULT_CURRENCY_ID): + cv.positive_int, vol.Optional(CONF_DISPLAY_CURRENCY, default=DEFAULT_DISPLAY_CURRENCY): cv.string, + vol.Optional(CONF_DISPLAY_CURRENCY_DECIMALS, + default=DEFAULT_DISPLAY_CURRENCY_DECIMALS): + vol.All(vol.Coerce(int), vol.Range(min=1)), }) def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the CoinMarketCap sensor.""" - currency = config.get(CONF_CURRENCY) - display_currency = config.get(CONF_DISPLAY_CURRENCY).lower() + currency_id = config.get(CONF_CURRENCY_ID) + display_currency = config.get(CONF_DISPLAY_CURRENCY).upper() + display_currency_decimals = config.get(CONF_DISPLAY_CURRENCY_DECIMALS) try: - CoinMarketCapData(currency, display_currency).update() + CoinMarketCapData(currency_id, display_currency).update() except HTTPError: - _LOGGER.warning("Currency %s or display currency %s is not available. " - "Using bitcoin and USD.", currency, display_currency) - currency = DEFAULT_CURRENCY + _LOGGER.warning("Currency ID %s or display currency %s " + "is not available. Using 1 (bitcoin) " + "and USD.", currency_id, display_currency) + currency_id = DEFAULT_CURRENCY_ID display_currency = DEFAULT_DISPLAY_CURRENCY add_devices([CoinMarketCapSensor( - CoinMarketCapData(currency, display_currency))], True) + CoinMarketCapData( + currency_id, display_currency), display_currency_decimals)], True) class CoinMarketCapSensor(Entity): """Representation of a CoinMarketCap sensor.""" - def __init__(self, data): + def __init__(self, data, display_currency_decimals): """Initialize the sensor.""" self.data = data + self.display_currency_decimals = display_currency_decimals self._ticker = None - self._unit_of_measurement = self.data.display_currency.upper() + self._unit_of_measurement = self.data.display_currency @property def name(self): @@ -80,8 +94,9 @@ class CoinMarketCapSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return round(float(self._ticker.get( - 'price_{}'.format(self.data.display_currency))), 2) + return round(float( + self._ticker.get('quotes').get(self.data.display_currency) + .get('price')), self.display_currency_decimals) @property def unit_of_measurement(self): @@ -97,15 +112,24 @@ class CoinMarketCapSensor(Entity): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_24H_VOLUME: self._ticker.get( - '24h_volume_{}'.format(self.data.display_currency)), + ATTR_VOLUME_24H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('volume_24h'), ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_AVAILABLE_SUPPLY: self._ticker.get('available_supply'), - ATTR_MARKET_CAP: self._ticker.get( - 'market_cap_{}'.format(self.data.display_currency)), - ATTR_PERCENT_CHANGE_24H: self._ticker.get('percent_change_24h'), - ATTR_PERCENT_CHANGE_7D: self._ticker.get('percent_change_7d'), - ATTR_PERCENT_CHANGE_1H: self._ticker.get('percent_change_1h'), + ATTR_CIRCULATING_SUPPLY: self._ticker.get('circulating_supply'), + ATTR_MARKET_CAP: + self._ticker.get('quotes').get(self.data.display_currency) + .get('market_cap'), + ATTR_PERCENT_CHANGE_24H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_24h'), + ATTR_PERCENT_CHANGE_7D: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_7d'), + ATTR_PERCENT_CHANGE_1H: + self._ticker.get('quotes').get(self.data.display_currency) + .get('percent_change_1h'), + ATTR_RANK: self._ticker.get('rank'), ATTR_SYMBOL: self._ticker.get('symbol'), ATTR_TOTAL_SUPPLY: self._ticker.get('total_supply'), } @@ -113,20 +137,20 @@ class CoinMarketCapSensor(Entity): def update(self): """Get the latest data and updates the states.""" self.data.update() - self._ticker = self.data.ticker[0] + self._ticker = self.data.ticker.get('data') class CoinMarketCapData(object): """Get the latest data and update the states.""" - def __init__(self, currency, display_currency): + def __init__(self, currency_id, display_currency): """Initialize the data object.""" - self.currency = currency + self.currency_id = currency_id self.display_currency = display_currency self.ticker = None def update(self): - """Get the latest data from blockchain.info.""" + """Get the latest data from coinmarketcap.com.""" from coinmarketcap import Market self.ticker = Market().ticker( - self.currency, limit=1, convert=self.display_currency) + self.currency_id, convert=self.display_currency) diff --git a/requirements_all.txt b/requirements_all.txt index d50fe829712..e9c0aaffdf9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -199,7 +199,7 @@ ciscosparkapi==0.4.2 coinbase==2.1.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.2.1 +coinmarketcap==5.0.3 # homeassistant.scripts.check_config colorlog==3.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 275a1974104..9deebf797eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -44,7 +44,7 @@ apns2==0.3.0 caldav==0.5.0 # homeassistant.components.sensor.coinmarketcap -coinmarketcap==4.2.1 +coinmarketcap==5.0.3 # homeassistant.components.device_tracker.upc_connect defusedxml==0.5.0 diff --git a/tests/components/sensor/test_coinmarketcap.py b/tests/components/sensor/test_coinmarketcap.py index 15c254bfb27..37a63e5cba5 100644 --- a/tests/components/sensor/test_coinmarketcap.py +++ b/tests/components/sensor/test_coinmarketcap.py @@ -11,8 +11,9 @@ from tests.common import ( VALID_CONFIG = { 'platform': 'coinmarketcap', - 'currency': 'ethereum', + 'currency_id': 1027, 'display_currency': 'EUR', + 'display_currency_decimals': 3 } @@ -39,6 +40,6 @@ class TestCoinMarketCapSensor(unittest.TestCase): state = self.hass.states.get('sensor.ethereum') assert state is not None - assert state.state == '240.47' + assert state.state == '493.455' assert state.attributes.get('symbol') == 'ETH' assert state.attributes.get('unit_of_measurement') == 'EUR' diff --git a/tests/fixtures/coinmarketcap.json b/tests/fixtures/coinmarketcap.json index 20f5e4fe91e..5a6b63c5da1 100644 --- a/tests/fixtures/coinmarketcap.json +++ b/tests/fixtures/coinmarketcap.json @@ -1,21 +1,36 @@ -[ - { - "id": "ethereum", - "name": "Ethereum", - "symbol": "ETH", - "rank": "2", - "price_usd": "282.423", - "price_btc": "0.048844", - "24h_volume_usd": "407024000.0", - "market_cap_usd": "26908205315.0", - "available_supply": "95276253.0", - "total_supply": "95276253.0", - "percent_change_1h": "0.06", - "percent_change_24h": "-4.57", - "percent_change_7d": "-16.39", - "last_updated": "1508776751", - "price_eur": "240.473299695", - "24h_volume_eur": "346566690.16", - "market_cap_eur": "22911395039.0" +{ + "cached": false, + "data": { + "id": 1027, + "name": "Ethereum", + "symbol": "ETH", + "website_slug": "ethereum", + "rank": 2, + "circulating_supply": 99619842.0, + "total_supply": 99619842.0, + "max_supply": null, + "quotes": { + "USD": { + "price": 577.019, + "volume_24h": 2839960000.0, + "market_cap": 57482541899.0, + "percent_change_1h": -2.28, + "percent_change_24h": -14.88, + "percent_change_7d": -17.51 + }, + "EUR": { + "price": 493.454724572, + "volume_24h": 2428699712.48, + "market_cap": 49158380042.0, + "percent_change_1h": -2.28, + "percent_change_24h": -14.88, + "percent_change_7d": -17.51 + } + }, + "last_updated": 1527098658 + }, + "metadata": { + "timestamp": 1527098716, + "error": null } -] \ No newline at end of file +} \ No newline at end of file From bf3ead33596e260ec6bc3630df3d1559fa86d840 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 11:32:45 -0400 Subject: [PATCH 809/924] Use libsodium18 (#14624) --- virtualization/Docker/setup_docker_prereqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 3bb4136c991..97c3c6bdd1c 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -23,7 +23,7 @@ PACKAGES=( # homeassistant.components.device_tracker.bluetooth_tracker bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks - libsodium13 + libsodium18 # homeassistant.components.zwave libudev-dev # homeassistant.components.homekit_controller From 48972c75708b2a55194d0e9e25750c99c0c9529a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 13:49:45 -0400 Subject: [PATCH 810/924] No longer use backports for ffmpeg (#14626) --- virtualization/Docker/scripts/ffmpeg | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg index 81b9ce694f9..914c2648e56 100755 --- a/virtualization/Docker/scripts/ffmpeg +++ b/virtualization/Docker/scripts/ffmpeg @@ -8,9 +8,4 @@ PACKAGES=( ffmpeg ) -# Add jessie-backports -echo "Adding jessie-backports" -echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list -apt-get update - -apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES[@]} \ No newline at end of file +apt-get install -y --no-install-recommends ${PACKAGES[@]} From 6ceafabd786c8c688e9fb380ffbbda110c02fa30 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 25 May 2018 22:41:50 +0200 Subject: [PATCH 811/924] Extend package support (#14611) --- homeassistant/config.py | 32 ++++++++++++++--- tests/test_config.py | 80 +++++++++++++++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 14 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 2f916e69b76..44bf542f7cd 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -548,6 +548,31 @@ def _identify_config_schema(module): return '', schema +def _recursive_merge(pack_name, comp_name, config, conf, package): + """Merge package into conf, recursively.""" + for key, pack_conf in package.items(): + if isinstance(pack_conf, dict): + if not pack_conf: + continue + conf[key] = conf.get(key, OrderedDict()) + _recursive_merge(pack_name, comp_name, config, + conf=conf[key], package=pack_conf) + + elif isinstance(pack_conf, list): + if not pack_conf: + continue + conf[key] = cv.ensure_list(conf.get(key)) + conf[key].extend(cv.ensure_list(pack_conf)) + + else: + if conf.get(key) is not None: + _log_pkg_error( + pack_name, comp_name, config, + 'has keys that are defined multiple times') + else: + conf[key] = pack_conf + + def merge_packages_config(hass, config, packages, _log_pkg_error=_log_pkg_error): """Merge packages into the top-level configuration. Mutate config.""" @@ -607,11 +632,10 @@ def merge_packages_config(hass, config, packages, config[comp_name][key] = val continue - # The last merge type are sections that may occur only once + # The last merge type are sections that require recursive merging if comp_name in config: - _log_pkg_error( - pack_name, comp_name, config, "may occur only once" - " and it already exist in your main configuration") + _recursive_merge(pack_name, comp_name, config, + conf=config[comp_name], package=comp_conf) continue config[comp_name] = comp_conf diff --git a/tests/test_config.py b/tests/test_config.py index 4b1115c3814..d22d6b2acfd 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -654,21 +654,81 @@ def test_merge_type_mismatch(merge_log_err, hass): assert len(config['light']) == 2 -def test_merge_once_only(merge_log_err, hass): - """Test if we have a merge for a comp that may occur only once.""" - packages = { - 'pack_2': { - 'mqtt': {}, - 'api': {}, # No config schema - }, - } +def test_merge_once_only_keys(merge_log_err, hass): + """Test if we have a merge for a comp that may occur only once. Keys.""" + packages = {'pack_2': {'api': { + 'key_3': 3, + }}} config = { config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, - 'mqtt': {}, 'api': {} + 'api': { + 'key_1': 1, + 'key_2': 2, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == {'key_1': 1, 'key_2': 2, 'key_3': 3, } + + # Duplicate keys error + packages = {'pack_2': {'api': { + 'key': 2, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': {'key': 1, } } config_util.merge_packages_config(hass, config, packages) assert merge_log_err.call_count == 1 - assert len(config) == 3 + + +def test_merge_once_only_lists(hass): + """Test if we have a merge for a comp that may occur only once. Lists.""" + packages = {'pack_2': {'api': { + 'list_1': ['item_2', 'item_3'], + 'list_2': ['item_1'], + 'list_3': [], + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'list_1': ['item_1'], + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'list_1': ['item_1', 'item_2', 'item_3'], + 'list_2': ['item_1'], + } + + +def test_merge_once_only_dictionaries(hass): + """Test if we have a merge for a comp that may occur only once. Dicts.""" + packages = {'pack_2': {'api': { + 'dict_1': { + 'key_2': 2, + 'dict_1.1': {'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + 'dict_3': {}, + }}} + config = { + config_util.CONF_CORE: {config_util.CONF_PACKAGES: packages}, + 'api': { + 'dict_1': { + 'key_1': 1, + 'dict_1.1': {'key_1.1': 1.1, } + }, + } + } + config_util.merge_packages_config(hass, config, packages) + assert config['api'] == { + 'dict_1': { + 'key_1': 1, + 'key_2': 2, + 'dict_1.1': {'key_1.1': 1.1, 'key_1.2': 1.2, }, + }, + 'dict_2': {'key_1': 1, }, + } def test_merge_id_schema(hass): From edfc54b2ebfc4994007245ca551c14d0d121ea4d Mon Sep 17 00:00:00 2001 From: Lorenz Schmid Date: Sat, 26 May 2018 09:51:21 +0200 Subject: [PATCH 812/924] Added option to connect via SSL for OpenWRT(luci) device tracker (#14627) * Added option to connect via HTTPS for OpenWRT(luci) device tracker * Use string formatting * Update quotes * Remove whitespace --- .../components/device_tracker/luci.py | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/device_tracker/luci.py b/homeassistant/components/device_tracker/luci.py index a4b826a009f..f479dea184b 100644 --- a/homeassistant/components/device_tracker/luci.py +++ b/homeassistant/components/device_tracker/luci.py @@ -15,14 +15,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import HomeAssistantError from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_SSL) _LOGGER = logging.getLogger(__name__) +DEFAULT_SSL = False + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean }) @@ -44,7 +48,9 @@ class LuciDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - self.host = config[CONF_HOST] + host = config[CONF_HOST] + protocol = 'http' if not config[CONF_SSL] else 'https' + self.origin = '{}://{}'.format(protocol, host) self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] @@ -57,7 +63,7 @@ class LuciDeviceScanner(DeviceScanner): def refresh_token(self): """Get a new token.""" - self.token = _get_token(self.host, self.username, self.password) + self.token = _get_token(self.origin, self.username, self.password) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -67,9 +73,9 @@ class LuciDeviceScanner(DeviceScanner): def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" if self.mac2name is None: - url = 'http://{}/cgi-bin/luci/rpc/uci'.format(self.host) - result = _req_json_rpc(url, 'get_all', 'dhcp', - params={'auth': self.token}) + url = '{}/cgi-bin/luci/rpc/uci'.format(self.origin) + result = _req_json_rpc( + url, 'get_all', 'dhcp', params={'auth': self.token}) if result: hosts = [x for x in result.values() if x['.type'] == 'host' and @@ -92,11 +98,11 @@ class LuciDeviceScanner(DeviceScanner): _LOGGER.info("Checking ARP") - url = 'http://{}/cgi-bin/luci/rpc/sys'.format(self.host) + url = '{}/cgi-bin/luci/rpc/sys'.format(self.origin) try: - result = _req_json_rpc(url, 'net.arptable', - params={'auth': self.token}) + result = _req_json_rpc( + url, 'net.arptable', params={'auth': self.token}) except InvalidLuciTokenError: _LOGGER.info("Refreshing token") self.refresh_token() @@ -146,10 +152,10 @@ def _req_json_rpc(url, method, *args, **kwargs): raise InvalidLuciTokenError else: - _LOGGER.error('Invalid response from luci: %s', res) + _LOGGER.error("Invalid response from luci: %s", res) -def _get_token(host, username, password): - """Get authentication token for the given host+username+password.""" - url = 'http://{}/cgi-bin/luci/rpc/auth'.format(host) +def _get_token(origin, username, password): + """Get authentication token for the given configuration.""" + url = '{}/cgi-bin/luci/rpc/auth'.format(origin) return _req_json_rpc(url, 'login', username, password) From 28d6910e56a5f7c186928cc7f0f4e650e6308802 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Sat, 26 May 2018 07:43:31 -0400 Subject: [PATCH 813/924] Added UDP and parallel streams support to Iperf3 (#14629) --- homeassistant/components/sensor/iperf3.py | 29 ++++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/iperf3.py b/homeassistant/components/sensor/iperf3.py index 1a209faf17f..8e030390f50 100644 --- a/homeassistant/components/sensor/iperf3.py +++ b/homeassistant/components/sensor/iperf3.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import DOMAIN, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ENTITY_ID, CONF_MONITORED_CONDITIONS, - CONF_HOST, CONF_PORT) + CONF_HOST, CONF_PORT, CONF_PROTOCOL) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -27,13 +27,16 @@ ATTR_VERSION = 'Version' CONF_ATTRIBUTION = 'Data retrieved using Iperf3' CONF_DURATION = 'duration' +CONF_PARALLEL = 'parallel' DEFAULT_DURATION = 10 DEFAULT_PORT = 5201 +DEFAULT_PARALLEL = 1 +DEFAULT_PROTOCOL = 'tcp' IPERF3_DATA = 'iperf3' -SCAN_INTERVAL = timedelta(minutes=30) +SCAN_INTERVAL = timedelta(minutes=60) SERVICE_NAME = 'iperf3_update' @@ -50,6 +53,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.Range(5, 10), + vol.Optional(CONF_PARALLEL, default=DEFAULT_PARALLEL): vol.Range(1, 20), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): + vol.In(['tcp', 'udp']), }) @@ -70,6 +76,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): Iperf3Sensor(config[CONF_HOST], config[CONF_PORT], config[CONF_DURATION], + config[CONF_PARALLEL], + config[CONF_PROTOCOL], sensor)) hass.data[IPERF3_DATA]['sensors'].extend(dev) @@ -98,10 +106,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class Iperf3Sensor(Entity): """A Iperf3 sensor implementation.""" - def __init__(self, server, port, duration, sensor_type): + def __init__(self, server, port, duration, streams, + protocol, sensor_type): """Initialize the sensor.""" self._attrs = { ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + ATTR_PROTOCOL: protocol, } self._name = \ "{} {}".format(SENSOR_TYPES[sensor_type][0], server) @@ -111,6 +121,8 @@ class Iperf3Sensor(Entity): self._port = port self._server = server self._duration = duration + self._num_streams = streams + self._protocol = protocol self.result = None @property @@ -133,7 +145,6 @@ class Iperf3Sensor(Entity): """Return the state attributes.""" if self.result is not None: self._attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION - self._attrs[ATTR_PROTOCOL] = self.result.protocol self._attrs[ATTR_REMOTE_HOST] = self.result.remote_host self._attrs[ATTR_REMOTE_PORT] = self.result.remote_port self._attrs[ATTR_VERSION] = self.result.version @@ -147,6 +158,8 @@ class Iperf3Sensor(Entity): client.server_hostname = self._server client.port = self._port client.verbose = False + client.num_streams = self._num_streams + client.protocol = self._protocol # when testing download bandwith, reverse must be True if self._sensor_type == 'download': @@ -154,7 +167,7 @@ class Iperf3Sensor(Entity): try: self.result = client.run() - except (OSError, AttributeError) as error: + except (AttributeError, OSError, ValueError) as error: self.result = None _LOGGER.error("Iperf3 sensor error: %s", error) return @@ -166,7 +179,11 @@ class Iperf3Sensor(Entity): self.result = None return - if self._sensor_type == 'download': + # UDP only have 1 way attribute + if self._protocol == 'udp': + self._state = round(self.result.Mbps, 2) + + elif self._sensor_type == 'download': self._state = round(self.result.received_Mbps, 2) elif self._sensor_type == 'upload': From a55fbd2be79fdf08c690252a94512708ae8ee857 Mon Sep 17 00:00:00 2001 From: Max Muth Date: Sat, 26 May 2018 13:53:48 +0200 Subject: [PATCH 814/924] Add services for adding and removing items to shopping list (#14574) --- homeassistant/components/services.yaml | 14 ++++++++ homeassistant/components/shopping_list.py | 41 ++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index 746c3c7f483..c0279ef1d0f 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -556,3 +556,17 @@ xiaomi_aqara: device_id: description: Hardware address of the device to remove. example: 158d0000000000 + +shopping_list: + add_item: + description: Adds an item to the shopping list. + fields: + name: + description: The name of the item to add. + example: Beer + complete_item: + description: Marks an item as completed in the shopping list. It does not remove the item. + fields: + name: + description: The name of the item to mark as completed. + example: Beer diff --git a/homeassistant/components/shopping_list.py b/homeassistant/components/shopping_list.py index 0ca0fef6e06..f113561429a 100644 --- a/homeassistant/components/shopping_list.py +++ b/homeassistant/components/shopping_list.py @@ -14,6 +14,8 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +ATTR_NAME = 'name' + DOMAIN = 'shopping_list' DEPENDENCIES = ['http'] _LOGGER = logging.getLogger(__name__) @@ -23,20 +25,57 @@ INTENT_ADD_ITEM = 'HassShoppingListAddItem' INTENT_LAST_ITEMS = 'HassShoppingListLastItems' ITEM_UPDATE_SCHEMA = vol.Schema({ 'complete': bool, - 'name': str, + ATTR_NAME: str, }) PERSISTENCE = '.shopping_list.json' +SERVICE_ADD_ITEM = 'add_item' +SERVICE_COMPLETE_ITEM = 'complete_item' + +SERVICE_ITEM_SCHEMA = vol.Schema({ + vol.Required(ATTR_NAME): vol.Any(None, cv.string) +}) + @asyncio.coroutine def async_setup(hass, config): """Initialize the shopping list.""" + @asyncio.coroutine + def add_item_service(call): + """Add an item with `name`.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is not None: + data.async_add(name) + + @asyncio.coroutine + def complete_item_service(call): + """Mark the item provided via `name` as completed.""" + data = hass.data[DOMAIN] + name = call.data.get(ATTR_NAME) + if name is None: + return + try: + item = [item for item in data.items if item['name'] == name][0] + except IndexError: + _LOGGER.error("Removing of item failed: %s cannot be found", name) + else: + data.async_update(item['id'], {'name': name, 'complete': True}) + data = hass.data[DOMAIN] = ShoppingData(hass) yield from data.async_load() intent.async_register(hass, AddItemIntent()) intent.async_register(hass, ListTopItemsIntent()) + hass.services.async_register( + DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, + schema=SERVICE_ITEM_SCHEMA + ) + hass.http.register_view(ShoppingListView) hass.http.register_view(CreateShoppingListItemView) hass.http.register_view(UpdateShoppingListItemView) From 41fc44b27c2c35244ed5ebbf5ae61ca9d97908f6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 08:33:22 -0400 Subject: [PATCH 815/924] Bump frontend to 20180526.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ee6ce549a4..f6b8bc9cb7a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180524.0'] +REQUIREMENTS = ['home-assistant-frontend==20180526.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index e9c0aaffdf9..08296b91c7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9deebf797eb..12d2d2154e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From bfc16428dad23ea72c836955a43cf5f289b5b836 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 08:33:22 -0400 Subject: [PATCH 816/924] Bump frontend to 20180526.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ee6ce549a4..f6b8bc9cb7a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180524.0'] +REQUIREMENTS = ['home-assistant-frontend==20180526.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0bd40c03fae..1386c490695 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57224aa4233..9e2ea04fb8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 19351fc429389a948297e4eb31e24c0ceb825058 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 11:32:45 -0400 Subject: [PATCH 817/924] Use libsodium18 (#14624) --- virtualization/Docker/setup_docker_prereqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 302dfba2e1d..23f55eea13f 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -22,7 +22,7 @@ PACKAGES=( # homeassistant.components.device_tracker.bluetooth_tracker bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks - libsodium13 + libsodium18 # homeassistant.components.zwave libudev-dev # homeassistant.components.homekit_controller From 2f0435ebd81bc7b3a9a0b474b831b33720218c9d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 13:49:45 -0400 Subject: [PATCH 818/924] No longer use backports for ffmpeg (#14626) --- virtualization/Docker/scripts/ffmpeg | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg index 81b9ce694f9..914c2648e56 100755 --- a/virtualization/Docker/scripts/ffmpeg +++ b/virtualization/Docker/scripts/ffmpeg @@ -8,9 +8,4 @@ PACKAGES=( ffmpeg ) -# Add jessie-backports -echo "Adding jessie-backports" -echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list -apt-get update - -apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES[@]} \ No newline at end of file +apt-get install -y --no-install-recommends ${PACKAGES[@]} From c9498d9f0941575bbf15e19b602f5dda9352b55c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 08:35:18 -0400 Subject: [PATCH 819/924] Version bump to 0.70.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dafe7e90db5..86653d4c909 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 7ea25cd3600603ac198807c0ef7bf7a689b7154e Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sat, 26 May 2018 16:03:53 +0200 Subject: [PATCH 820/924] Add homematicip cloud climate platform (#14388) * Add support for climatic devices * Update requirements_all * Change icon to mdi:thermostat * Update of homematicip-rest-api lib version * Remove all mode or state relevant things - will come later * Add current_operation again to see proper status * Remove STATE_PERFORMANCE import * Remove trailing whitespace * Update requirements file --- .../components/climate/homematicip_cloud.py | 101 ++++++++++++++++++ homeassistant/components/homematicip_cloud.py | 5 +- requirements_all.txt | 2 +- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/climate/homematicip_cloud.py diff --git a/homeassistant/components/climate/homematicip_cloud.py b/homeassistant/components/climate/homematicip_cloud.py new file mode 100644 index 00000000000..bf96f1f746d --- /dev/null +++ b/homeassistant/components/climate/homematicip_cloud.py @@ -0,0 +1,101 @@ +""" +Support for HomematicIP climate. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/climate.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.climate import ( + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, ATTR_TEMPERATURE, + STATE_AUTO, STATE_MANUAL) +from homeassistant.const import TEMP_CELSIUS +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +_LOGGER = logging.getLogger(__name__) + +STATE_BOOST = 'Boost' + +HA_STATE_TO_HMIP = { + STATE_AUTO: 'AUTOMATIC', + STATE_MANUAL: 'MANUAL', +} + +HMIP_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_HMIP.items()} + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP climate devices.""" + from homematicip.group import HeatingGroup + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + + devices = [] + for device in home.groups: + if isinstance(device, HeatingGroup): + devices.append(HomematicipHeatingGroup(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): + """Representation of a MomematicIP heating group.""" + + def __init__(self, home, device): + """Initialize heating group.""" + device.modelType = 'Group-Heating' + super().__init__(home, device) + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._device.setPointTemperature + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.actualTemperature + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._device.humidity + + @property + def current_operation(self): + """Return current operation ie. automatic or manual.""" + return HMIP_STATE_TO_HA.get(self._device.controlMode) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return self._device.minTemperature + + @property + def max_temp(self): + """Return the maximum temperature.""" + return self._device.maxTemperature + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + await self._device.set_point_temperature(temperature) diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index d85d867d8f8..859841dfca6 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -17,7 +17,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from homeassistant.core import callback -REQUIREMENTS = ['homematicip==0.9.2.4'] +REQUIREMENTS = ['homematicip==0.9.4'] _LOGGER = logging.getLogger(__name__) @@ -27,7 +27,8 @@ COMPONENTS = [ 'sensor', 'binary_sensor', 'switch', - 'light' + 'light', + 'climate', ] CONF_NAME = 'name' diff --git a/requirements_all.txt b/requirements_all.txt index 08296b91c7b..f172dcb13e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -395,7 +395,7 @@ home-assistant-frontend==20180526.1 # homekit==0.6 # homeassistant.components.homematicip_cloud -homematicip==0.9.2.4 +homematicip==0.9.4 # homeassistant.components.camera.onvif http://github.com/tgaugry/suds-passworddigest-py3/archive/86fc50e39b4d2b8997481967d6a7fe1c57118999.zip#suds-passworddigest-py3==0.1.2a From 8de56cfc1057de9de54fd3391d5887944605827d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 26 May 2018 17:35:16 +0200 Subject: [PATCH 821/924] Upgrade speedtest-cli to 2.0.2 (#14633) --- homeassistant/components/sensor/speedtest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/speedtest.py b/homeassistant/components/sensor/speedtest.py index 5b03be036d5..bf2868d3b01 100644 --- a/homeassistant/components/sensor/speedtest.py +++ b/homeassistant/components/sensor/speedtest.py @@ -17,7 +17,7 @@ from homeassistant.helpers.event import track_time_change from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.util.dt as dt_util -REQUIREMENTS = ['speedtest-cli==2.0.0'] +REQUIREMENTS = ['speedtest-cli==2.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index f172dcb13e7..3eb75d92cf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1228,7 +1228,7 @@ socialbladeclient==0.2 somecomfort==0.5.2 # homeassistant.components.sensor.speedtest -speedtest-cli==2.0.0 +speedtest-cli==2.0.2 # homeassistant.components.sensor.spotcrime spotcrime==1.0.3 From fdb250d86c50f486dccbdaa191c7934da791324f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:53:24 -0400 Subject: [PATCH 822/924] Bump frontend to 20180526.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f6b8bc9cb7a..7d888a2b082 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.1'] +REQUIREMENTS = ['home-assistant-frontend==20180526.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3eb75d92cf0..b751f592093 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12d2d2154e5..6d1a58bc5d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 52c21a53b37bdfa37f2466ecfd301a1ccae4511c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:53:24 -0400 Subject: [PATCH 823/924] Bump frontend to 20180526.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f6b8bc9cb7a..7d888a2b082 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.1'] +REQUIREMENTS = ['home-assistant-frontend==20180526.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 1386c490695..0dbd5f74d3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e2ea04fb8c..405511c6d50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6c62f7231b07db9f058908799955418642829fa6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:53:57 -0400 Subject: [PATCH 824/924] Version bump to 0.70.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 86653d4c909..f2d95bc2ac4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6b9addfeeab05a8de0d2a2cb0df99b1c6e73c085 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:54:50 -0400 Subject: [PATCH 825/924] Update release script --- script/release | 1 + 1 file changed, 1 insertion(+) diff --git a/script/release b/script/release index dc3e208bc1a..cf4f808377e 100755 --- a/script/release +++ b/script/release @@ -27,5 +27,6 @@ then exit 1 fi +rm -rf dist python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing From dfd7ef1fcecbe4fe13b3ec17d845f21d9f8de179 Mon Sep 17 00:00:00 2001 From: David Ryan Date: Sat, 26 May 2018 12:42:52 -0400 Subject: [PATCH 826/924] Add Hydrawise component (#14055) * Added the Hydrawise component. * Fixed lint errors. * Multiple changes due to review comments addressed. * Simplified boolean test. Passes pylint. * Need hydrawiser package version 0.1.1. * Added a docstring to the device_class method. * Addressed all review comments from MartinHjelmare. * Changed keys to single quote. Removed unnecessary duplicate method. * Removed unused imports. * Changed state to lowercase snakecase. * Changes & fixes from review comments. --- .coveragerc | 3 + .../components/binary_sensor/hydrawise.py | 81 ++++++++++ homeassistant/components/hydrawise.py | 153 ++++++++++++++++++ homeassistant/components/sensor/hydrawise.py | 72 +++++++++ homeassistant/components/switch/hydrawise.py | 103 ++++++++++++ requirements_all.txt | 3 + 6 files changed, 415 insertions(+) create mode 100644 homeassistant/components/binary_sensor/hydrawise.py create mode 100644 homeassistant/components/hydrawise.py create mode 100644 homeassistant/components/sensor/hydrawise.py create mode 100644 homeassistant/components/switch/hydrawise.py diff --git a/.coveragerc b/.coveragerc index 3ccfdeb3569..d4dc4e4367d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -123,6 +123,9 @@ omit = homeassistant/components/homematicip_cloud.py homeassistant/components/*/homematicip_cloud.py + homeassistant/components/hydrawise.py + homeassistant/components/*/hydrawise.py + homeassistant/components/ihc/* homeassistant/components/*/ihc.py diff --git a/homeassistant/components/binary_sensor/hydrawise.py b/homeassistant/components/binary_sensor/hydrawise.py new file mode 100644 index 00000000000..a3e0ebd782d --- /dev/null +++ b/homeassistant/components/binary_sensor/hydrawise.py @@ -0,0 +1,81 @@ +""" +Support for Hydrawise sprinkler. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + BINARY_SENSORS, DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, + DEVICE_MAP_INDEX) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSORS): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type in ['status', 'rain_sensor']: + sensors.append( + HydrawiseBinarySensor( + hydrawise.controller_status, sensor_type)) + + else: + # create a sensor for each zone + for zone in hydrawise.relays: + zone_data = zone + zone_data['running'] = \ + hydrawise.controller_status.get('running', False) + sensors.append(HydrawiseBinarySensor(zone_data, sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorDevice): + """A sensor implementation for Hydrawise device.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating Hydrawise binary sensor: %s", self._name) + mydata = self.hass.data[DATA_HYDRAWISE].data + if self._sensor_type == 'status': + self._state = mydata.status == 'All good!' + elif self._sensor_type == 'rain_sensor': + for sensor in mydata.sensors: + if sensor['name'] == 'Rain': + self._state = sensor['active'] == 1 + elif self._sensor_type == 'is_watering': + if not mydata.running: + self._state = False + elif int(mydata.running[0]['relay']) == self.data['relay']: + self._state = True + else: + self._state = False + + @property + def device_class(self): + """Return the device class of the sensor type.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('DEVICE_CLASS_INDEX')] diff --git a/homeassistant/components/hydrawise.py b/homeassistant/components/hydrawise.py new file mode 100644 index 00000000000..a60e3d5b8fc --- /dev/null +++ b/homeassistant/components/hydrawise.py @@ -0,0 +1,153 @@ +""" +Support for Hydrawise cloud. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/hydrawise/ +""" +import asyncio +from datetime import timedelta +import logging + +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL) +import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['hydrawiser==0.1.1'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] + +CONF_ATTRIBUTION = "Data provided by hydrawise.com" +CONF_WATERING_TIME = 'watering_minutes' + +NOTIFICATION_ID = 'hydrawise_notification' +NOTIFICATION_TITLE = 'Hydrawise Setup' + +DATA_HYDRAWISE = 'hydrawise' +DOMAIN = 'hydrawise' +DEFAULT_WATERING_TIME = 15 + +DEVICE_MAP_INDEX = ['KEY_INDEX', 'ICON_INDEX', 'DEVICE_CLASS_INDEX', + 'UNIT_OF_MEASURE_INDEX'] +DEVICE_MAP = { + 'auto_watering': ['Automatic Watering', 'mdi:autorenew', '', ''], + 'is_watering': ['Watering', '', 'moisture', ''], + 'manual_watering': ['Manual Watering', 'mdi:water-pump', '', ''], + 'next_cycle': ['Next Cycle', 'mdi:calendar-clock', '', ''], + 'status': ['Status', '', 'connectivity', ''], + 'watering_time': ['Watering Time', 'mdi:water-pump', '', 'min'], + 'rain_sensor': ['Rain Sensor', '', 'moisture', ''] +} + +BINARY_SENSORS = ['is_watering', 'status', 'rain_sensor'] + +SENSORS = ['next_cycle', 'watering_time'] + +SWITCHES = ['auto_watering', 'manual_watering'] + +SCAN_INTERVAL = timedelta(seconds=30) + +SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Hunter Hydrawise component.""" + conf = config[DOMAIN] + access_token = conf[CONF_ACCESS_TOKEN] + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + from hydrawiser.core import Hydrawiser + + hydrawise = Hydrawiser(user_token=access_token) + hass.data[DATA_HYDRAWISE] = HydrawiseHub(hydrawise) + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error( + "Unable to connect to Hydrawise cloud service: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + def hub_refresh(event_time): + """Call Hydrawise hub to refresh information.""" + _LOGGER.debug("Updating Hydrawise Hub component") + hass.data[DATA_HYDRAWISE].data.update_controller_info() + dispatcher_send(hass, SIGNAL_UPDATE_HYDRAWISE) + + # Call the Hydrawise API to refresh updates + track_time_interval(hass, hub_refresh, scan_interval) + + return True + + +class HydrawiseHub(object): + """Representation of a base Hydrawise device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class HydrawiseEntity(Entity): + """Entity class for Hydrawise devices.""" + + def __init__(self, data, sensor_type): + """Initialize the Hydrawise entity.""" + self.data = data + self._sensor_type = sensor_type + self._name = "{0} {1}".format( + self.data['name'], + DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('KEY_INDEX')]) + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_HYDRAWISE, self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('UNIT_OF_MEASURE_INDEX')] + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'identifier': self.data.get('relay'), + } diff --git a/homeassistant/components/sensor/hydrawise.py b/homeassistant/components/sensor/hydrawise.py new file mode 100644 index 00000000000..fea2780da07 --- /dev/null +++ b/homeassistant/components/sensor/hydrawise.py @@ -0,0 +1,72 @@ +""" +Support for Hydrawise sprinkler. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + DATA_HYDRAWISE, HydrawiseEntity, DEVICE_MAP, DEVICE_MAP_INDEX, SENSORS) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + for zone in hydrawise.relays: + sensors.append(HydrawiseSensor(zone, sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseSensor(HydrawiseEntity): + """A sensor implementation for Hydrawise device.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise sensor: %s", self._name) + if self._sensor_type == 'watering_time': + if not mydata.running: + self._state = 0 + else: + if int(mydata.running[0]['relay']) == self.data['relay']: + self._state = int(mydata.running[0]['time_left']/60) + else: + self._state = 0 + else: # _sensor_type == 'next_cycle' + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay['nicetime'] == 'Not scheduled': + self._state = 'not_scheduled' + else: + self._state = relay['nicetime'].split(',')[0] + \ + ' ' + relay['nicetime'].split(' ')[3] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/homeassistant/components/switch/hydrawise.py b/homeassistant/components/switch/hydrawise.py new file mode 100644 index 00000000000..d0abe5febf5 --- /dev/null +++ b/homeassistant/components/switch/hydrawise.py @@ -0,0 +1,103 @@ +""" +Support for Hydrawise cloud. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.hydrawise/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.hydrawise import ( + ALLOWED_WATERING_TIME, CONF_WATERING_TIME, + DATA_HYDRAWISE, DEFAULT_WATERING_TIME, HydrawiseEntity, SWITCHES, + DEVICE_MAP, DEVICE_MAP_INDEX) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['hydrawise'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME): + vol.All(vol.In(ALLOWED_WATERING_TIME)), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a Hydrawise device.""" + hydrawise = hass.data[DATA_HYDRAWISE].data + + default_watering_timer = config.get(CONF_WATERING_TIME) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + # create a switch for each zone + for zone in hydrawise.relays: + sensors.append( + HydrawiseSwitch(default_watering_timer, + zone, + sensor_type)) + + add_devices(sensors, True) + + +class HydrawiseSwitch(HydrawiseEntity, SwitchDevice): + """A switch implementation for Hydrawise device.""" + + def __init__(self, default_watering_timer, *args): + """Initialize a switch for Hydrawise device.""" + super().__init__(*args) + self._default_watering_timer = default_watering_timer + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self, **kwargs): + """Turn the device on.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + self._default_watering_timer, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 0, (self.data['relay']-1)) + + def turn_off(self, **kwargs): + """Turn the device off.""" + if self._sensor_type == 'manual_watering': + self.hass.data[DATA_HYDRAWISE].data.run_zone( + 0, (self.data['relay']-1)) + elif self._sensor_type == 'auto_watering': + self.hass.data[DATA_HYDRAWISE].data.suspend_zone( + 365, (self.data['relay']-1)) + + def update(self): + """Update device state.""" + mydata = self.hass.data[DATA_HYDRAWISE].data + _LOGGER.debug("Updating Hydrawise switch: %s", self._name) + if self._sensor_type == 'manual_watering': + if not mydata.running: + self._state = False + else: + self._state = int( + mydata.running[0]['relay']) == self.data['relay'] + elif self._sensor_type == 'auto_watering': + for relay in mydata.relays: + if relay['relay'] == self.data['relay']: + if relay.get('suspended') is not None: + self._state = False + else: + self._state = True + break + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEVICE_MAP[self._sensor_type][ + DEVICE_MAP_INDEX.index('ICON_INDEX')] diff --git a/requirements_all.txt b/requirements_all.txt index b751f592093..a5f0bcaf78f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -430,6 +430,9 @@ https://github.com/soldag/pyflic/archive/0.4.zip#pyflic==0.4 # homeassistant.components.media_player.lg_netcast https://github.com/wokar/pylgnetcast/archive/v0.2.0.zip#pylgnetcast==0.2.0 +# homeassistant.components.hydrawise +hydrawiser==0.1.1 + # homeassistant.components.sensor.bh1750 # homeassistant.components.sensor.bme280 # homeassistant.components.sensor.htu21d From bcde57bff89835bfe03acf61f62f8f1decdc6c2c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 14:29:26 -0400 Subject: [PATCH 827/924] Bump frontend to 20180526.3 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7d888a2b082..654afd67f42 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.2'] +REQUIREMENTS = ['home-assistant-frontend==20180526.3'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a5f0bcaf78f..934feb4ea42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d1a58bc5d3..7ae86cf65f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From fb447cab82dae8a2332f82d4f9db4fc11d7e804c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 14:29:26 -0400 Subject: [PATCH 828/924] Bump frontend to 20180526.3 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7d888a2b082..654afd67f42 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.2'] +REQUIREMENTS = ['home-assistant-frontend==20180526.3'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0dbd5f74d3d..d58c58e38b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 405511c6d50..9622fe9674a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a5b9e59ceeee8cdf3d4629b44abd9485927b382b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 14:30:03 -0400 Subject: [PATCH 829/924] Version bump to 0.70.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f2d95bc2ac4..37c583e3b7e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From c425afe50eca32a536168da4a952d7b8bbced950 Mon Sep 17 00:00:00 2001 From: guillaume1410 Date: Sat, 26 May 2018 16:46:53 -0400 Subject: [PATCH 830/924] Adding ryobi garage door opener (#14618) * Initial component for Ryobi cover * Initial component for Ryobi cover * Adding Ryobi cover * Adding Ryobi cover * Adding Ryobi cover * Minor changes * Remove import --- .coveragerc | 1 + homeassistant/components/cover/ryobi_gdo.py | 103 ++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 107 insertions(+) create mode 100644 homeassistant/components/cover/ryobi_gdo.py diff --git a/.coveragerc b/.coveragerc index d4dc4e4367d..3d1bbab8456 100644 --- a/.coveragerc +++ b/.coveragerc @@ -385,6 +385,7 @@ omit = homeassistant/components/cover/myq.py homeassistant/components/cover/opengarage.py homeassistant/components/cover/rpi_gpio.py + homeassistant/components/cover/ryobi_gdo.py homeassistant/components/cover/scsgate.py homeassistant/components/device_tracker/actiontec.py homeassistant/components/device_tracker/aruba.py diff --git a/homeassistant/components/cover/ryobi_gdo.py b/homeassistant/components/cover/ryobi_gdo.py new file mode 100644 index 00000000000..a11d70dd3ad --- /dev/null +++ b/homeassistant/components/cover/ryobi_gdo.py @@ -0,0 +1,103 @@ +""" +Ryobi platform for the cover component. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/cover.ryobi_gdo/ +""" +import logging +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.cover import ( + CoverDevice, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.const import ( + CONF_USERNAME, CONF_PASSWORD, STATE_UNKNOWN, STATE_CLOSED) + +REQUIREMENTS = ['py_ryobi_gdo==0.0.10'] + +_LOGGER = logging.getLogger(__name__) + +CONF_DEVICE_ID = 'device_id' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, +}) + +SUPPORTED_FEATURES = (SUPPORT_OPEN | SUPPORT_CLOSE) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ryobi covers.""" + from py_ryobi_gdo import RyobiGDO as ryobi_door + covers = [] + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + devices = config.get(CONF_DEVICE_ID) + + for device_id in devices: + my_door = ryobi_door(username, password, device_id) + _LOGGER.debug("Getting the API key") + if my_door.get_api_key() is False: + _LOGGER.error("Wrong credentials, no API key retrieved") + return + _LOGGER.debug("Checking if the device ID is present") + if my_door.check_device_id() is False: + _LOGGER.error("%s not in your device list", device_id) + return + _LOGGER.debug("Adding device %s to covers", device_id) + covers.append(RyobiCover(hass, my_door)) + if covers: + _LOGGER.debug("Adding covers") + add_devices(covers, True) + + +class RyobiCover(CoverDevice): + """Representation of a ryobi cover.""" + + def __init__(self, hass, ryobi_door): + """Initialize the cover.""" + self.ryobi_door = ryobi_door + self._name = 'ryobi_gdo_{}'.format(ryobi_door.get_device_id()) + self._door_state = None + + @property + def name(self): + """Return the name of the cover.""" + return self._name + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._door_state == STATE_UNKNOWN: + return False + return self._door_state == STATE_CLOSED + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return 'garage' + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES + + def close_cover(self, **kwargs): + """Close the cover.""" + _LOGGER.debug("Closing garage door") + self.ryobi_door.close_device() + + def open_cover(self, **kwargs): + """Open the cover.""" + _LOGGER.debug("Opening garage door") + self.ryobi_door.open_device() + + def update(self): + """Update status from the door.""" + _LOGGER.debug("Updating RyobiGDO status") + self.ryobi_door.update() + self._door_state = self.ryobi_door.get_door_status() diff --git a/requirements_all.txt b/requirements_all.txt index 934feb4ea42..2c3a9b4bf5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -706,6 +706,9 @@ pyTibber==0.4.1 # homeassistant.components.switch.dlink pyW215==0.6.0 +# homeassistant.components.cover.ryobi_gdo +py_ryobi_gdo==0.0.10 + # homeassistant.components.ads pyads==2.2.6 From eae9726beca0b0da84fe06ab94785d4c6a3f4c14 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 26 May 2018 16:50:05 -0400 Subject: [PATCH 831/924] Add electrical measurement sensor to ZHA (#14561) * Add electrical measurement sensor * correct state update * hound fix * zha: Add metering sensor (#14562) * Add IlluminanceMeasurementSensor to ZHA (#14563) * add IlluminanceMeasurementSensor * address review comment * Fix whitespace error during merge * Add electrical measurement sensor * correct state update * hound / flake8 --- homeassistant/components/sensor/zha.py | 38 ++++++++++++++++++++++++++ homeassistant/components/zha/const.py | 1 + 2 files changed, 39 insertions(+) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 72368bdb3ba..984d6efed66 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -36,6 +36,7 @@ def make_sensor(discovery_info): IlluminanceMeasurement ) from zigpy.zcl.clusters.smartenergy import Metering + from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) @@ -47,6 +48,9 @@ def make_sensor(discovery_info): sensor = IlluminanceMeasurementSensor(**discovery_info) elif Metering.cluster_id in in_clusters: sensor = MeteringSensor(**discovery_info) + elif ElectricalMeasurement.cluster_id in in_clusters: + sensor = ElectricalMeasurementSensor(**discovery_info) + return sensor else: sensor = Sensor(**discovery_info) @@ -182,3 +186,37 @@ class MeteringSensor(Sensor): return None return round(float(self._state)) + + +class ElectricalMeasurementSensor(Sensor): + """ZHA Electrical Measurement sensor.""" + + value_attribute = 1291 + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'W' + + @property + def state(self): + """Return the state of the entity.""" + if self._state is None: + return None + + return round(float(self._state) / 10, 1) + + @property + def should_poll(self) -> bool: + """Poll state from device.""" + return True + + async def async_update(self): + """Retrieve latest state.""" + _LOGGER.debug("%s async_update", self.entity_id) + + result = await zha.safe_read( + self._endpoint.electrical_measurement, + ['active_power'], + allow_cache=False) + self._state = result.get('active_power', self._state) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 087b19c6693..37c7f5592a0 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -50,6 +50,7 @@ def populate_data(): zcl.clusters.measurement.PressureMeasurement: 'sensor', zcl.clusters.measurement.IlluminanceMeasurement: 'sensor', zcl.clusters.smartenergy.Metering: 'sensor', + zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) From a5e66ce6bafc3212f78524481dd6aa39471fee3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 20:02:16 -0400 Subject: [PATCH 832/924] Bump frontend to 20180526.4 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 654afd67f42..2bd7283e38e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.3'] +REQUIREMENTS = ['home-assistant-frontend==20180526.4'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 2c3a9b4bf5c..d6e4ac86976 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ae86cf65f1..5f2ed329637 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 07b27283805dea27ae9163c49db1ac4d23147e52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 20:02:16 -0400 Subject: [PATCH 833/924] Bump frontend to 20180526.4 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 654afd67f42..2bd7283e38e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.3'] +REQUIREMENTS = ['home-assistant-frontend==20180526.4'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d58c58e38b2..ae16651d8e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9622fe9674a..ce458995d2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a9b0f92afefb6a5bef1abd865808ac0845e691ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 20:02:54 -0400 Subject: [PATCH 834/924] Version bump to 0.70.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 37c583e3b7e..0ff3f0738a1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5f9e4ae136e55a1da8aa24513f084686ccd0984e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 09:53:53 +0200 Subject: [PATCH 835/924] Upgrade luftdaten to 0.2.0 (#14620) --- homeassistant/components/sensor/luftdaten.py | 9 ++++----- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index c5e0b12b0e0..9952e2a1c24 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -4,7 +4,6 @@ Support for Luftdaten sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.luftdaten/ """ -import asyncio from datetime import timedelta import logging @@ -19,7 +18,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['luftdaten==0.1.3'] +REQUIREMENTS = ['luftdaten==0.2.0'] _LOGGER = logging.getLogger(__name__) @@ -59,8 +58,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Luftdaten sensor.""" from luftdaten import Luftdaten @@ -71,7 +70,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): session = async_get_clientsession(hass) luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session)) - yield from luftdaten.async_update() + await luftdaten.async_update() if luftdaten.data is None: _LOGGER.error("Sensor is not available: %s", sensor_id) diff --git a/requirements_all.txt b/requirements_all.txt index d6e4ac86976..fe9b088e1f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ lmnotify==0.0.4 locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten -luftdaten==0.1.3 +luftdaten==0.2.0 # homeassistant.components.light.lw12wifi lw12==0.9.2 From 5acfe5da68d354782007072341ad7061bf7a3aa6 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sun, 27 May 2018 02:31:05 -0700 Subject: [PATCH 836/924] Upgrade python-nest to 4.0.0 (#14638) * Upgrade python-nest to 4.0.0 Drop in replace to use nest stream API Didn't change any logic from HA side * Update requirements_all.txt --- homeassistant/components/nest.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 2500755d495..f474bfa7a26 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, CONF_MONITORED_CONDITIONS) -REQUIREMENTS = ['python-nest==3.7.0'] +REQUIREMENTS = ['python-nest==4.0.0'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fe9b088e1f9..2264c716c5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1027,7 +1027,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.2 # homeassistant.components.nest -python-nest==3.7.0 +python-nest==4.0.0 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From 2d88f47795cd43655a67d42f1a3b1cafd0b40d9d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 15:45:43 +0200 Subject: [PATCH 837/924] Upgrade gitterpy to 0.1.7 (#14643) --- homeassistant/components/sensor/gitter.py | 17 ++++++++++++----- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/gitter.py b/homeassistant/components/sensor/gitter.py index 58f33635750..907af07a2db 100644 --- a/homeassistant/components/sensor/gitter.py +++ b/homeassistant/components/sensor/gitter.py @@ -8,12 +8,12 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_API_KEY, CONF_ROOM +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_ROOM +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['gitterpy==0.1.6'] +REQUIREMENTS = ['gitterpy==0.1.7'] _LOGGER = logging.getLogger(__name__) @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): username = gitter.auth.get_my_id['name'] except GitterTokenError: _LOGGER.error("Token is not valid") - return False + return add_devices([GitterSensor(gitter, room, name, username)], True) @@ -96,7 +96,14 @@ class GitterSensor(Entity): def update(self): """Get the latest data and updates the state.""" - data = self._data.user.unread_items(self._room) + from gitterpy.errors import GitterRoomError + + try: + data = self._data.user.unread_items(self._room) + except GitterRoomError as error: + _LOGGER.error(error) + return + if 'error' not in data.keys(): self._mention = len(data['mention']) self._state = len(data['chat']) diff --git a/requirements_all.txt b/requirements_all.txt index 2264c716c5b..126d28832b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ gTTS-token==1.1.1 gearbest_parser==1.0.5 # homeassistant.components.sensor.gitter -gitterpy==0.1.6 +gitterpy==0.1.7 # homeassistant.components.notify.gntp gntp==1.0.3 From 36e8157268d32ae65ea0e819cf8ba50510416fae Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 15:46:58 +0200 Subject: [PATCH 838/924] Upgrade TwitterAPI to 2.5.4 (#14639) --- homeassistant/components/notify/twitter.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/twitter.py b/homeassistant/components/notify/twitter.py index f81a83325ce..e38e7fcaa0f 100644 --- a/homeassistant/components/notify/twitter.py +++ b/homeassistant/components/notify/twitter.py @@ -19,7 +19,7 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME from homeassistant.helpers.event import async_track_point_in_time -REQUIREMENTS = ['TwitterAPI==2.5.3'] +REQUIREMENTS = ['TwitterAPI==2.5.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 126d28832b9..d84e0c0d671 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -61,7 +61,7 @@ SoCo==0.14 TravisPy==0.3.5 # homeassistant.components.notify.twitter -TwitterAPI==2.5.3 +TwitterAPI==2.5.4 # homeassistant.components.sensor.waze_travel_time WazeRouteCalculator==0.5 From 2f4c5f949b095172b3bca584dbbe17495a320be5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 20:16:30 +0200 Subject: [PATCH 839/924] Use constants (#14647) --- homeassistant/components/api.py | 140 ++++++++++++++++---------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index dc34006ad03..ae89e2fc3b6 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -2,7 +2,7 @@ Rest API for Home Assistant. For more details about the RESTful API, please refer to the documentation at -https://home-assistant.io/developers/api/ +https://developers.home-assistant.io/docs/en/external_api_rest.html """ import asyncio import json @@ -11,31 +11,34 @@ import logging from aiohttp import web import async_timeout -import homeassistant.core as ha -import homeassistant.remote as rem from homeassistant.bootstrap import DATA_LOGGING -from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, - HTTP_BAD_REQUEST, HTTP_CREATED, HTTP_NOT_FOUND, - MATCH_ALL, URL_API, URL_API_COMPONENTS, - URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, - URL_API_EVENTS, URL_API_SERVICES, - URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, URL_API_TEMPLATE, - __version__) -from homeassistant.exceptions import TemplateError -from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.helpers import template from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, EVENT_TIME_CHANGED, HTTP_BAD_REQUEST, + HTTP_CREATED, HTTP_NOT_FOUND, MATCH_ALL, URL_API, URL_API_COMPONENTS, + URL_API_CONFIG, URL_API_DISCOVERY_INFO, URL_API_ERROR_LOG, URL_API_EVENTS, + URL_API_SERVICES, URL_API_STATES, URL_API_STATES_ENTITY, URL_API_STREAM, + URL_API_TEMPLATE, __version__) +import homeassistant.core as ha +from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template +from homeassistant.helpers.service import async_get_all_descriptions +from homeassistant.helpers.state import AsyncTrackStates +import homeassistant.remote as rem + +_LOGGER = logging.getLogger(__name__) + +ATTR_BASE_URL = 'base_url' +ATTR_LOCATION_NAME = 'location_name' +ATTR_REQUIRES_API_PASSWORD = 'requires_api_password' +ATTR_VERSION = 'version' DOMAIN = 'api' DEPENDENCIES = ['http'] -STREAM_PING_PAYLOAD = "ping" +STREAM_PING_PAYLOAD = 'ping' STREAM_PING_INTERVAL = 50 # seconds -_LOGGER = logging.getLogger(__name__) - def setup(hass, config): """Register the API with the HTTP interface.""" @@ -62,19 +65,19 @@ class APIStatusView(HomeAssistantView): """View to handle Status requests.""" url = URL_API - name = "api:status" + name = 'api:status' @ha.callback def get(self, request): """Retrieve if API is running.""" - return self.json_message('API running.') + return self.json_message("API running.") class APIEventStream(HomeAssistantView): """View to handle EventStream requests.""" url = URL_API_STREAM - name = "api:stream" + name = 'api:stream' async def get(self, request): """Provide a streaming interface for the event bus.""" @@ -95,7 +98,7 @@ class APIEventStream(HomeAssistantView): if restrict and event.event_type not in restrict: return - _LOGGER.debug('STREAM %s FORWARDING %s', id(stop_obj), event) + _LOGGER.debug("STREAM %s FORWARDING %s", id(stop_obj), event) if event.event_type == EVENT_HOMEASSISTANT_STOP: data = stop_obj @@ -111,7 +114,7 @@ class APIEventStream(HomeAssistantView): unsub_stream = hass.bus.async_listen(MATCH_ALL, forward_events) try: - _LOGGER.debug('STREAM %s ATTACHED', id(stop_obj)) + _LOGGER.debug("STREAM %s ATTACHED", id(stop_obj)) # Fire off one message so browsers fire open event right away await to_write.put(STREAM_PING_PAYLOAD) @@ -126,25 +129,25 @@ class APIEventStream(HomeAssistantView): break msg = "data: {}\n\n".format(payload) - _LOGGER.debug('STREAM %s WRITING %s', id(stop_obj), - msg.strip()) - await response.write(msg.encode("UTF-8")) + _LOGGER.debug( + "STREAM %s WRITING %s", id(stop_obj), msg.strip()) + await response.write(msg.encode('UTF-8')) except asyncio.TimeoutError: await to_write.put(STREAM_PING_PAYLOAD) except asyncio.CancelledError: - _LOGGER.debug('STREAM %s ABORT', id(stop_obj)) + _LOGGER.debug("STREAM %s ABORT", id(stop_obj)) finally: - _LOGGER.debug('STREAM %s RESPONSE CLOSED', id(stop_obj)) + _LOGGER.debug("STREAM %s RESPONSE CLOSED", id(stop_obj)) unsub_stream() class APIConfigView(HomeAssistantView): - """View to handle Config requests.""" + """View to handle Configuration requests.""" url = URL_API_CONFIG - name = "api:config" + name = 'api:config' @ha.callback def get(self, request): @@ -153,22 +156,22 @@ class APIConfigView(HomeAssistantView): class APIDiscoveryView(HomeAssistantView): - """View to provide discovery info.""" + """View to provide Discovery information.""" requires_auth = False url = URL_API_DISCOVERY_INFO - name = "api:discovery" + name = 'api:discovery' @ha.callback def get(self, request): - """Get discovery info.""" + """Get discovery information.""" hass = request.app['hass'] needs_auth = hass.config.api.api_password is not None return self.json({ - 'base_url': hass.config.api.base_url, - 'location_name': hass.config.location_name, - 'requires_api_password': needs_auth, - 'version': __version__ + ATTR_BASE_URL: hass.config.api.base_url, + ATTR_LOCATION_NAME: hass.config.location_name, + ATTR_REQUIRES_API_PASSWORD: needs_auth, + ATTR_VERSION: __version__, }) @@ -187,8 +190,8 @@ class APIStatesView(HomeAssistantView): class APIEntityStateView(HomeAssistantView): """View to handle EntityState requests.""" - url = "/api/states/{entity_id}" - name = "api:entity-state" + url = '/api/states/{entity_id}' + name = 'api:entity-state' @ha.callback def get(self, request, entity_id): @@ -196,7 +199,7 @@ class APIEntityStateView(HomeAssistantView): state = request.app['hass'].states.get(entity_id) if state: return self.json(state) - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message("Entity not found.", HTTP_NOT_FOUND) async def post(self, request, entity_id): """Update state of entity.""" @@ -204,13 +207,13 @@ class APIEntityStateView(HomeAssistantView): try: data = await request.json() except ValueError: - return self.json_message('Invalid JSON specified', - HTTP_BAD_REQUEST) + return self.json_message( + "Invalid JSON specified.", HTTP_BAD_REQUEST) new_state = data.get('state') if new_state is None: - return self.json_message('No state specified', HTTP_BAD_REQUEST) + return self.json_message("No state specified.", HTTP_BAD_REQUEST) attributes = data.get('attributes') force_update = data.get('force_update', False) @@ -232,15 +235,15 @@ class APIEntityStateView(HomeAssistantView): def delete(self, request, entity_id): """Remove entity.""" if request.app['hass'].states.async_remove(entity_id): - return self.json_message('Entity removed') - return self.json_message('Entity not found', HTTP_NOT_FOUND) + return self.json_message("Entity removed.") + return self.json_message("Entity not found.", HTTP_NOT_FOUND) class APIEventListenersView(HomeAssistantView): """View to handle EventListeners requests.""" url = URL_API_EVENTS - name = "api:event-listeners" + name = 'api:event-listeners' @ha.callback def get(self, request): @@ -252,7 +255,7 @@ class APIEventView(HomeAssistantView): """View to handle Event requests.""" url = '/api/events/{event_type}' - name = "api:event" + name = 'api:event' async def post(self, request, event_type): """Fire events.""" @@ -260,12 +263,12 @@ class APIEventView(HomeAssistantView): try: event_data = json.loads(body) if body else None except ValueError: - return self.json_message('Event data should be valid JSON', - HTTP_BAD_REQUEST) + return self.json_message( + "Event data should be valid JSON.", HTTP_BAD_REQUEST) if event_data is not None and not isinstance(event_data, dict): - return self.json_message('Event data should be a JSON object', - HTTP_BAD_REQUEST) + return self.json_message( + "Event data should be a JSON object", HTTP_BAD_REQUEST) # Special case handling for event STATE_CHANGED # We will try to convert state dicts back to State objects @@ -276,8 +279,8 @@ class APIEventView(HomeAssistantView): if state: event_data[key] = state - request.app['hass'].bus.async_fire(event_type, event_data, - ha.EventOrigin.remote) + request.app['hass'].bus.async_fire( + event_type, event_data, ha.EventOrigin.remote) return self.json_message("Event {} fired.".format(event_type)) @@ -286,7 +289,7 @@ class APIServicesView(HomeAssistantView): """View to handle Services requests.""" url = URL_API_SERVICES - name = "api:services" + name = 'api:services' async def get(self, request): """Get registered services.""" @@ -297,8 +300,8 @@ class APIServicesView(HomeAssistantView): class APIDomainServicesView(HomeAssistantView): """View to handle DomainServices requests.""" - url = "/api/services/{domain}/{service}" - name = "api:domain-services" + url = '/api/services/{domain}/{service}' + name = 'api:domain-services' async def post(self, request, domain, service): """Call a service. @@ -310,8 +313,8 @@ class APIDomainServicesView(HomeAssistantView): try: data = json.loads(body) if body else None except ValueError: - return self.json_message('Data should be valid JSON', - HTTP_BAD_REQUEST) + return self.json_message( + "Data should be valid JSON.", HTTP_BAD_REQUEST) with AsyncTrackStates(hass) as changed_states: await hass.services.async_call(domain, service, data, True) @@ -323,7 +326,7 @@ class APIComponentsView(HomeAssistantView): """View to handle Components requests.""" url = URL_API_COMPONENTS - name = "api:components" + name = 'api:components' @ha.callback def get(self, request): @@ -332,10 +335,10 @@ class APIComponentsView(HomeAssistantView): class APITemplateView(HomeAssistantView): - """View to handle requests.""" + """View to handle Template requests.""" url = URL_API_TEMPLATE - name = "api:template" + name = 'api:template' async def post(self, request): """Render a template.""" @@ -344,30 +347,29 @@ class APITemplateView(HomeAssistantView): tpl = template.Template(data['template'], request.app['hass']) return tpl.async_render(data.get('variables')) except (ValueError, TemplateError) as ex: - return self.json_message('Error rendering template: {}'.format(ex), - HTTP_BAD_REQUEST) + return self.json_message( + "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST) class APIErrorLog(HomeAssistantView): - """View to fetch the error log.""" + """View to fetch the API error log.""" url = URL_API_ERROR_LOG - name = "api:error_log" + name = 'api:error_log' async def get(self, request): """Retrieve API error log.""" - return web.FileResponse( - request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse(request.app['hass'].data[DATA_LOGGING]) async def async_services_json(hass): """Generate services data to JSONify.""" descriptions = await async_get_all_descriptions(hass) - return [{"domain": key, "services": value} + return [{'domain': key, 'services': value} for key, value in descriptions.items()] def async_events_json(hass): """Generate event data to JSONify.""" - return [{"event": key, "listener_count": value} + return [{'event': key, 'listener_count': value} for key, value in hass.bus.async_listeners().items()] From 13859388c1895189e756be9393d96d5a2dbf99b4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 27 May 2018 20:16:47 +0200 Subject: [PATCH 840/924] Upgrade locationsharinglib to 2.0.7 (#14640) --- .../components/device_tracker/google_maps.py | 11 ++++++----- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 3bf0cb0e126..5f06946fc44 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -4,19 +4,20 @@ Support for Google Maps location sharing. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.google_maps/ """ -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID +from homeassistant.const import ATTR_ID, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify -REQUIREMENTS = ['locationsharinglib==2.0.2'] +REQUIREMENTS = ['locationsharinglib==2.0.7'] _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,7 @@ class GoogleMapsScanner(object): def _update_info(self, now=None): for person in self.service.get_all_people(): try: - dev_id = 'google_maps_{0}'.format(person.id) + dev_id = 'google_maps_{0}'.format(slugify(person.id)) except TypeError: _LOGGER.warning("No location(s) shared with this account") return diff --git a/requirements_all.txt b/requirements_all.txt index d84e0c0d671..2e944f20132 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -515,7 +515,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==2.0.2 +locationsharinglib==2.0.7 # homeassistant.components.sensor.luftdaten luftdaten==0.2.0 From b6e4a7771a2e3a3658cca547eb1997e93fc64c52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 May 2018 17:17:19 -0400 Subject: [PATCH 841/924] Allow Hass.io panel dir (#14655) --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 9dd6427ec38..bb4f8219a33 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), + re.compile(r'^app-(es5|latest)/.+$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ed425ad8cca..ac90deb9f73 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -48,7 +48,7 @@ def test_auth_required_forward_request(hassio_client): @pytest.mark.parametrize( 'build_type', [ 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html' + 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" From 94a82ab7dc1b504ee89bd93f75fc40503b023bf1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 May 2018 17:17:19 -0400 Subject: [PATCH 842/924] Allow Hass.io panel dir (#14655) --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 9dd6427ec38..bb4f8219a33 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), + re.compile(r'^app-(es5|latest)/.+$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ed425ad8cca..ac90deb9f73 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -48,7 +48,7 @@ def test_auth_required_forward_request(hassio_client): @pytest.mark.parametrize( 'build_type', [ 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html' + 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" From cd0e321668706c767d4be49dd66002743dac9d7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 May 2018 17:18:53 -0400 Subject: [PATCH 843/924] Version bump to 0.70.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0ff3f0738a1..84088c4511c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b7' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From cc2437614ba5556b517b3897970027bf62d4ad96 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 May 2018 08:16:55 +0200 Subject: [PATCH 844/924] Upgrade youtube_dl to 2018.05.26 (#14654) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index bef02d7113f..73837ce2ca1 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.05.18'] +REQUIREMENTS = ['youtube_dl==2018.05.26'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2e944f20132..addc954ba06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1388,7 +1388,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.18 +youtube_dl==2018.05.26 # homeassistant.components.light.zengge zengge==0.2 From bff1e1ff6c9bbb223c0df959fd62f78aedb22e2e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 28 May 2018 08:17:10 +0200 Subject: [PATCH 845/924] Upgrade python_opendata_transport to 0.1.0 (#14652) --- .../components/sensor/swiss_public_transport.py | 14 ++++++-------- requirements_all.txt | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index a489adf6776..928d84caa2b 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -4,7 +4,6 @@ Support for transport.opendata.ch. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.swiss_public_transport/ """ -import asyncio from datetime import timedelta import logging @@ -17,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util -REQUIREMENTS = ['python_opendata_transport==0.0.3'] +REQUIREMENTS = ['python_opendata_transport==0.1.0'] _LOGGER = logging.getLogger(__name__) @@ -48,8 +47,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Swiss public transport sensor.""" from opendata_transport import OpendataTransport, exceptions @@ -61,7 +60,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): opendata = OpendataTransport(start, destination, hass.loop, session) try: - yield from opendata.async_get_data() + await opendata.async_get_data() except exceptions.OpendataTransportError: _LOGGER.error( "Check at http://transport.opendata.ch/examples/stationboard.html " @@ -122,12 +121,11 @@ class SwissPublicTransportSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the latest data from opendata.ch and update the states.""" from opendata_transport.exceptions import OpendataTransportError try: - yield from self._opendata.async_get_data() + await self._opendata.async_get_data() except OpendataTransportError: _LOGGER.error("Unable to retrieve data from transport.opendata.ch") diff --git a/requirements_all.txt b/requirements_all.txt index addc954ba06..11533c195ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1069,7 +1069,7 @@ python-vlc==1.1.2 python-wink==1.7.3 # homeassistant.components.sensor.swiss_public_transport -python_opendata_transport==0.0.3 +python_opendata_transport==0.1.0 # homeassistant.components.zwave python_openzwave==0.4.3 From 799ae894a8c9c641d529625cd18ebe98f9e08bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Mon, 28 May 2018 16:17:01 +0200 Subject: [PATCH 846/924] Remove docker prereqs scripts that only install a package. Add informational message for this. (#14661) --- virtualization/Docker/scripts/ffmpeg | 11 ----------- virtualization/Docker/scripts/iperf3 | 11 ----------- virtualization/Docker/setup_docker_prereqs | 18 ++++++++---------- 3 files changed, 8 insertions(+), 32 deletions(-) delete mode 100755 virtualization/Docker/scripts/ffmpeg delete mode 100755 virtualization/Docker/scripts/iperf3 diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg deleted file mode 100755 index 914c2648e56..00000000000 --- a/virtualization/Docker/scripts/ffmpeg +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Sets up ffmpeg. - -# Stop on errors -set -e - -PACKAGES=( - ffmpeg -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/scripts/iperf3 b/virtualization/Docker/scripts/iperf3 deleted file mode 100755 index 2d9d5a33761..00000000000 --- a/virtualization/Docker/scripts/iperf3 +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Sets up iperf3. - -# Stop on errors -set -e - -PACKAGES=( - iperf3 -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 97c3c6bdd1c..0cb49fde54e 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -6,11 +6,9 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" -INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" -INSTALL_IPERF3="${INSTALL_IPERF3:-yes}" # Required debian packages for running hass or components PACKAGES=( @@ -28,6 +26,10 @@ PACKAGES=( libudev-dev # homeassistant.components.homekit_controller libmpc-dev libmpfr-dev libgmp-dev + # homeassistant.components.ffmpeg + ffmpeg + # homeassistant.components.sensor.iperf3 + iperf3 ) # Required debian packages for building dependencies @@ -41,6 +43,10 @@ PACKAGES_DEV=( apt-get update apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} +# This is a list of scripts that install additional dependencies. If you only +# need to install a package from the official debian repository, just add it +# to the list above. Only create a script if you need compiling, manually +# downloading or a 3th party repository. if [ "$INSTALL_TELLSTICK" == "yes" ]; then virtualization/Docker/scripts/tellstick fi @@ -49,10 +55,6 @@ if [ "$INSTALL_OPENALPR" == "yes" ]; then virtualization/Docker/scripts/openalpr fi -if [ "$INSTALL_FFMPEG" == "yes" ]; then - virtualization/Docker/scripts/ffmpeg -fi - if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi @@ -65,10 +67,6 @@ if [ "$INSTALL_SSOCR" == "yes" ]; then virtualization/Docker/scripts/ssocr fi -if [ "$INSTALL_IPERF3" == "yes" ]; then - virtualization/Docker/scripts/iperf3 -fi - # Remove packages apt-get remove -y --purge ${PACKAGES_DEV[@]} apt-get -y --purge autoremove From 9044a9157ffb12f39e37e4f4f4679b00046b136d Mon Sep 17 00:00:00 2001 From: koreth Date: Mon, 28 May 2018 07:19:03 -0700 Subject: [PATCH 847/924] Reduce log churn from Envisalink binary sensors (#14659) The Envisalink binary sensor was logging events with a relative timestamp that updated every time it polled, so even when nothing new was happening, the event log would be full of meaningless state changes. Modify the sensor code to use an absolute time which stays stable when there isn't new activity. --- .../components/binary_sensor/envisalink.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/envisalink.py b/homeassistant/components/binary_sensor/envisalink.py index 0aadcc247ea..f358f814dc5 100644 --- a/homeassistant/components/binary_sensor/envisalink.py +++ b/homeassistant/components/binary_sensor/envisalink.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/binary_sensor.envisalink/ """ import asyncio import logging +import datetime from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -14,6 +15,7 @@ from homeassistant.components.envisalink import ( DATA_EVL, ZONE_SCHEMA, CONF_ZONENAME, CONF_ZONETYPE, EnvisalinkDevice, SIGNAL_ZONE_UPDATE) from homeassistant.const import ATTR_LAST_TRIP_TIME +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -63,7 +65,25 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorDevice): def device_state_attributes(self): """Return the state attributes.""" attr = {} - attr[ATTR_LAST_TRIP_TIME] = self._info['last_fault'] + + # The Envisalink library returns a "last_fault" value that's the + # number of seconds since the last fault, up to a maximum of 327680 + # seconds (65536 5-second ticks). + # + # We don't want the HA event log to fill up with a bunch of no-op + # "state changes" that are just that number ticking up once per poll + # interval, so we subtract it from the current second-accurate time + # unless it is already at the maximum value, in which case we set it + # to None since we can't determine the actual value. + seconds_ago = self._info['last_fault'] + if seconds_ago < 65536 * 5: + now = dt_util.now().replace(microsecond=0) + delta = datetime.timedelta(seconds=seconds_ago) + last_trip_time = (now - delta).isoformat() + else: + last_trip_time = None + + attr[ATTR_LAST_TRIP_TIME] = last_trip_time return attr @property From 9a87e62e0ee50a3d464b27a9873298e0856dacaa Mon Sep 17 00:00:00 2001 From: MizterB <5458030+MizterB@users.noreply.github.com> Date: Mon, 28 May 2018 10:21:00 -0400 Subject: [PATCH 848/924] Update Hue platform to aiohue 1.5.0, and re-implement logic for duplicate scene names. (#14653) --- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 18 +++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 0aed854d4e4..251d8cba095 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -17,7 +17,7 @@ from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.3.0'] +REQUIREMENTS = ['aiohue==1.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5ff5e2dbf6f..d7a8dc7f730 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -124,9 +124,21 @@ class HueBridge(object): (group for group in self.api.groups.values() if group.name == group_name), None) - scene_id = next( - (scene.id for scene in self.api.scenes.values() - if scene.name == scene_name), None) + # The same scene name can exist in multiple groups. + # In this case, activate first scene that contains the + # the exact same light IDs as the group + scenes = [] + for scene in self.api.scenes.values(): + if scene.name == scene_name: + scenes.append(scene) + if len(scenes) == 1: + scene_id = scenes[0].id + else: + group_lights = sorted(group.lights) + for scene in scenes: + if group_lights == scene.lights: + scene_id = scene.id + break # If we can't find it, fetch latest info. if not updated and (group is None or scene_id is None): diff --git a/requirements_all.txt b/requirements_all.txt index 11533c195ce..78239492ce9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.3.0 +aiohue==1.5.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5f2ed329637..adcba607db0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.3.0 +aiohue==1.5.0 # homeassistant.components.notify.apns apns2==0.3.0 From 27f3285d17a6303443ac46fd0463745f6c9af64b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 28 May 2018 10:22:29 -0400 Subject: [PATCH 849/924] Force update ZHA electrical sensor (#14649) * force state update because we have a real reading * hound * docstring --- homeassistant/components/sensor/zha.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 984d6efed66..3051d8f2afa 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -198,6 +198,11 @@ class ElectricalMeasurementSensor(Sensor): """Return the unit of measurement of this entity.""" return 'W' + @property + def force_update(self) -> bool: + """Force update this entity.""" + return True + @property def state(self): """Return the state of the entity.""" From 6f4dd7b057b492379404c92b8f96fee20fdc1c74 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 28 May 2018 16:26:33 +0200 Subject: [PATCH 850/924] Improve Homekit media_player options (#14637) * Optimize imports * Optimize name * Optimize config schema * Rename mode to feature * Replace mode with feature_list --- homeassistant/components/homekit/__init__.py | 26 ++--- homeassistant/components/homekit/const.py | 14 ++- .../components/homekit/type_media_players.py | 96 ++++++++-------- homeassistant/components/homekit/util.py | 107 +++++++++++------- .../homekit/test_get_accessories.py | 32 +++--- .../homekit/test_type_media_players.py | 55 +++++---- tests/components/homekit/test_util.py | 34 +++--- 7 files changed, 201 insertions(+), 163 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index f011a56a77b..a79fbf85400 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -9,12 +9,11 @@ from zlib import adler32 import voluptuous as vol -from homeassistant.components.cover import ( - SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) +import homeassistant.components.cover as cover from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_MODE, CONF_NAME, CONF_PORT, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv @@ -22,11 +21,11 @@ from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, - DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, + DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from .util import ( - show_setup_message, validate_entity_config, validate_media_player_modes) + show_setup_message, validate_entity_config, validate_media_player_features) TYPES = Registry() _LOGGER = logging.getLogger(__name__) @@ -110,11 +109,11 @@ def get_accessory(hass, state, aid, config): device_class = state.attributes.get(ATTR_DEVICE_CLASS) if device_class == 'garage' and \ - features & (SUPPORT_OPEN | SUPPORT_CLOSE): + features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = 'GarageDoorOpener' - elif features & SUPPORT_SET_POSITION: + elif features & cover.SUPPORT_SET_POSITION: a_type = 'WindowCovering' - elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): + elif features & (cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE): a_type = 'WindowCoveringBasic' elif state.domain == 'fan': @@ -127,8 +126,9 @@ def get_accessory(hass, state, aid, config): a_type = 'Lock' elif state.domain == 'media_player': - validate_media_player_modes(state, config) - if config.get(CONF_MODE): + feature_list = config.get(CONF_FEATURE_LIST) + if feature_list and \ + validate_media_player_features(state, feature_list): a_type = 'MediaPlayer' elif state.domain == 'sensor': diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index f59ee5488ec..6d49c806e0f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -8,12 +8,20 @@ HOMEKIT_NOTIFY_ID = 4663548 # #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' +CONF_FEATURE = 'feature' +CONF_FEATURE_LIST = 'feature_list' CONF_FILTER = 'filter' # #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 +# #### Features #### +FEATURE_ON_OFF = 'on_off' +FEATURE_PLAY_PAUSE = 'play_pause' +FEATURE_PLAY_STOP = 'play_stop' +FEATURE_TOGGLE_MUTE = 'toggle_mute' + # #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' @@ -23,12 +31,6 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' -# #### Media Player Modes #### -ON_OFF = 'on_off' -PLAY_PAUSE = 'play_pause' -PLAY_STOP = 'play_stop' -TOGGLE_MUTE = 'toggle_mute' - # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 563cd0cb25c..ec41b9fd618 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -4,7 +4,7 @@ import logging from pyhap.const import CATEGORY_SWITCH from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_MODE, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + ATTR_ENTITY_ID, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, STATE_OFF, STATE_PLAYING, STATE_UNKNOWN) from homeassistant.components.media_player import ( @@ -13,15 +13,15 @@ from homeassistant.components.media_player import ( from . import TYPES from .accessories import HomeAccessory from .const import ( - CHAR_NAME, CHAR_ON, ON_OFF, PLAY_PAUSE, PLAY_STOP, SERV_SWITCH, - TOGGLE_MUTE) + CHAR_NAME, CHAR_ON, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, SERV_SWITCH) _LOGGER = logging.getLogger(__name__) -MODE_FRIENDLY_NAME = {ON_OFF: 'Power', - PLAY_PAUSE: 'Play/Pause', - PLAY_STOP: 'Play/Stop', - TOGGLE_MUTE: 'Mute'} +MODE_FRIENDLY_NAME = {FEATURE_ON_OFF: 'Power', + FEATURE_PLAY_PAUSE: 'Play/Pause', + FEATURE_PLAY_STOP: 'Play/Stop', + FEATURE_TOGGLE_MUTE: 'Mute'} @TYPES.register('MediaPlayer') @@ -31,38 +31,38 @@ class MediaPlayer(HomeAccessory): def __init__(self, *args): """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) - self._flag = {ON_OFF: False, PLAY_PAUSE: False, - PLAY_STOP: False, TOGGLE_MUTE: False} - self.chars = {ON_OFF: None, PLAY_PAUSE: None, - PLAY_STOP: None, TOGGLE_MUTE: None} - modes = self.config[CONF_MODE] + self._flag = {FEATURE_ON_OFF: False, FEATURE_PLAY_PAUSE: False, + FEATURE_PLAY_STOP: False, FEATURE_TOGGLE_MUTE: False} + self.chars = {FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None} + feature_list = self.config[CONF_FEATURE_LIST] - if ON_OFF in modes: + if FEATURE_ON_OFF in feature_list: + name = self.generate_service_name(FEATURE_ON_OFF) serv_on_off = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_on_off.configure_char( - CHAR_NAME, value=self.generate_service_name(ON_OFF)) - self.chars[ON_OFF] = serv_on_off.configure_char( + serv_on_off.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_ON_OFF] = serv_on_off.configure_char( CHAR_ON, value=False, setter_callback=self.set_on_off) - if PLAY_PAUSE in modes: + if FEATURE_PLAY_PAUSE in feature_list: + name = self.generate_service_name(FEATURE_PLAY_PAUSE) serv_play_pause = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_play_pause.configure_char( - CHAR_NAME, value=self.generate_service_name(PLAY_PAUSE)) - self.chars[PLAY_PAUSE] = serv_play_pause.configure_char( + serv_play_pause.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_PLAY_PAUSE] = serv_play_pause.configure_char( CHAR_ON, value=False, setter_callback=self.set_play_pause) - if PLAY_STOP in modes: + if FEATURE_PLAY_STOP in feature_list: + name = self.generate_service_name(FEATURE_PLAY_STOP) serv_play_stop = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_play_stop.configure_char( - CHAR_NAME, value=self.generate_service_name(PLAY_STOP)) - self.chars[PLAY_STOP] = serv_play_stop.configure_char( + serv_play_stop.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_PLAY_STOP] = serv_play_stop.configure_char( CHAR_ON, value=False, setter_callback=self.set_play_stop) - if TOGGLE_MUTE in modes: + if FEATURE_TOGGLE_MUTE in feature_list: + name = self.generate_service_name(FEATURE_TOGGLE_MUTE) serv_toggle_mute = self.add_preload_service(SERV_SWITCH, CHAR_NAME) - serv_toggle_mute.configure_char( - CHAR_NAME, value=self.generate_service_name(TOGGLE_MUTE)) - self.chars[TOGGLE_MUTE] = serv_toggle_mute.configure_char( + serv_toggle_mute.configure_char(CHAR_NAME, value=name) + self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char( CHAR_ON, value=False, setter_callback=self.set_toggle_mute) def generate_service_name(self, mode): @@ -73,7 +73,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "on_off" to %s', self.entity_id, value) - self._flag[ON_OFF] = True + self._flag[FEATURE_ON_OFF] = True service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call(DOMAIN, service, params) @@ -82,7 +82,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "play_pause" to %s', self.entity_id, value) - self._flag[PLAY_PAUSE] = True + self._flag[FEATURE_PLAY_PAUSE] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_PAUSE params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call(DOMAIN, service, params) @@ -91,7 +91,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "play_stop" to %s', self.entity_id, value) - self._flag[PLAY_STOP] = True + self._flag[FEATURE_PLAY_STOP] = True service = SERVICE_MEDIA_PLAY if value else SERVICE_MEDIA_STOP params = {ATTR_ENTITY_ID: self.entity_id} self.hass.services.call(DOMAIN, service, params) @@ -100,7 +100,7 @@ class MediaPlayer(HomeAccessory): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug('%s: Set switch state for "toggle_mute" to %s', self.entity_id, value) - self._flag[TOGGLE_MUTE] = True + self._flag[FEATURE_TOGGLE_MUTE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} self.hass.services.call(DOMAIN, SERVICE_VOLUME_MUTE, params) @@ -109,34 +109,34 @@ class MediaPlayer(HomeAccessory): """Update switch state after state changed.""" current_state = new_state.state - if self.chars[ON_OFF]: + if self.chars[FEATURE_ON_OFF]: hk_state = current_state not in (STATE_OFF, STATE_UNKNOWN, 'None') - if not self._flag[ON_OFF]: + if not self._flag[FEATURE_ON_OFF]: _LOGGER.debug('%s: Set current state for "on_off" to %s', self.entity_id, hk_state) - self.chars[ON_OFF].set_value(hk_state) - self._flag[ON_OFF] = False + self.chars[FEATURE_ON_OFF].set_value(hk_state) + self._flag[FEATURE_ON_OFF] = False - if self.chars[PLAY_PAUSE]: + if self.chars[FEATURE_PLAY_PAUSE]: hk_state = current_state == STATE_PLAYING - if not self._flag[PLAY_PAUSE]: + if not self._flag[FEATURE_PLAY_PAUSE]: _LOGGER.debug('%s: Set current state for "play_pause" to %s', self.entity_id, hk_state) - self.chars[PLAY_PAUSE].set_value(hk_state) - self._flag[PLAY_PAUSE] = False + self.chars[FEATURE_PLAY_PAUSE].set_value(hk_state) + self._flag[FEATURE_PLAY_PAUSE] = False - if self.chars[PLAY_STOP]: + if self.chars[FEATURE_PLAY_STOP]: hk_state = current_state == STATE_PLAYING - if not self._flag[PLAY_STOP]: + if not self._flag[FEATURE_PLAY_STOP]: _LOGGER.debug('%s: Set current state for "play_stop" to %s', self.entity_id, hk_state) - self.chars[PLAY_STOP].set_value(hk_state) - self._flag[PLAY_STOP] = False + self.chars[FEATURE_PLAY_STOP].set_value(hk_state) + self._flag[FEATURE_PLAY_STOP] = False - if self.chars[TOGGLE_MUTE]: + if self.chars[FEATURE_TOGGLE_MUTE]: current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) - if not self._flag[TOGGLE_MUTE]: + if not self._flag[FEATURE_TOGGLE_MUTE]: _LOGGER.debug('%s: Set current state for "toggle_mute" to %s', self.entity_id, current_state) - self.chars[TOGGLE_MUTE].set_value(current_state) - self._flag[TOGGLE_MUTE] = False + self.chars[FEATURE_TOGGLE_MUTE].set_value(current_state) + self._flag[FEATURE_TOGGLE_MUTE] = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 57ce562ce21..50095844757 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -3,20 +3,37 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import ( - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE) +import homeassistant.components.media_player as media_player from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( - HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE) _LOGGER = logging.getLogger(__name__) -MEDIA_PLAYER_MODES = (ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + +BASIC_INFO_SCHEMA = vol.Schema({ + vol.Optional(CONF_NAME): cv.string, +}) + +FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list, +}) + + +CODE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string), +}) + +MEDIA_PLAYER_SCHEMA = vol.Schema({ + vol.Required(CONF_FEATURE): vol.All( + cv.string, vol.In((FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))), +}) def validate_entity_config(values): @@ -24,57 +41,59 @@ def validate_entity_config(values): entities = {} for entity_id, config in values.items(): entity = cv.entity_id(entity_id) - params = {} - if not isinstance(config, dict): - raise vol.Invalid('The configuration for "{}" must be ' - ' a dictionary.'.format(entity)) - - for key in (CONF_NAME, ): - value = config.get(key, -1) - if value != -1: - params[key] = cv.string(value) - domain, _ = split_entity_id(entity) + if not isinstance(config, dict): + raise vol.Invalid('The configuration for {} must be ' + ' a dictionary.'.format(entity)) + if domain in ('alarm_control_panel', 'lock'): - code = config.get(ATTR_CODE) - params[ATTR_CODE] = cv.string(code) if code else None + config = CODE_SCHEMA(config) - if domain == 'media_player': - mode = config.get(CONF_MODE) - params[CONF_MODE] = cv.ensure_list(mode) - for key in params[CONF_MODE]: - if key not in MEDIA_PLAYER_MODES: - raise vol.Invalid( - 'Invalid mode: "{}", valid modes are: "{}".' - .format(key, MEDIA_PLAYER_MODES)) + elif domain == media_player.DOMAIN: + config = FEATURE_SCHEMA(config) + feature_list = {} + for feature in config[CONF_FEATURE_LIST]: + params = MEDIA_PLAYER_SCHEMA(feature) + key = params.pop(CONF_FEATURE) + if key in feature_list: + raise vol.Invalid('A feature can be added only once for {}' + .format(entity)) + feature_list[key] = params + config[CONF_FEATURE_LIST] = feature_list - entities[entity] = params + else: + config = BASIC_INFO_SCHEMA(config) + + entities[entity] = config return entities -def validate_media_player_modes(state, config): - """Validate modes for media playeres.""" +def validate_media_player_features(state, feature_list): + """Validate features for media players.""" features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_modes = [] - if features & (SUPPORT_TURN_ON | SUPPORT_TURN_OFF): - supported_modes.append(ON_OFF) - if features & (SUPPORT_PLAY | SUPPORT_PAUSE): - supported_modes.append(PLAY_PAUSE) - if features & (SUPPORT_PLAY | SUPPORT_STOP): - supported_modes.append(PLAY_STOP) - if features & SUPPORT_VOLUME_MUTE: - supported_modes.append(TOGGLE_MUTE) + if features & (media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF): + supported_modes.append(FEATURE_ON_OFF) + if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_PAUSE): + supported_modes.append(FEATURE_PLAY_PAUSE) + if features & (media_player.SUPPORT_PLAY | media_player.SUPPORT_STOP): + supported_modes.append(FEATURE_PLAY_STOP) + if features & media_player.SUPPORT_VOLUME_MUTE: + supported_modes.append(FEATURE_TOGGLE_MUTE) - if not config.get(CONF_MODE): - config[CONF_MODE] = supported_modes - return + error_list = [] + for feature in feature_list: + if feature not in supported_modes: + error_list.append(feature) - for mode in config[CONF_MODE]: - if mode not in supported_modes: - raise vol.Invalid('"{}" does not support mode: "{}".' - .format(state.entity_id, mode)) + if error_list: + _LOGGER.error("%s does not support features: %s", + state.entity_id, error_list) + return False + return True def show_setup_message(hass, pincode): diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 11b2d737a70..46e5f8b1174 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -2,20 +2,17 @@ from unittest.mock import patch, Mock import pytest -import voluptuous as vol from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN -from homeassistant.components.climate import ( - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) -from homeassistant.components.media_player import ( - SUPPORT_TURN_OFF, SUPPORT_TURN_ON) +import homeassistant.components.cover as cover +import homeassistant.components.climate as climate +import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES -from homeassistant.components.homekit.const import ON_OFF +from homeassistant.components.homekit.const import ( + CONF_FEATURE_LIST, FEATURE_ON_OFF) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_MODE, CONF_NAME, TEMP_CELSIUS, - TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -32,9 +29,9 @@ def test_not_supported(caplog): def test_not_supported_media_player(): """Test if mode isn't supported and if no supported modes.""" # selected mode for entity not supported - with pytest.raises(vol.Invalid): - entity_state = State('media_player.demo', 'on') - get_accessory(None, entity_state, 2, {CONF_MODE: [ON_OFF]}) + config = {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}} + entity_state = State('media_player.demo', 'on') + get_accessory(None, entity_state, 2, config) is None # no supported modes for entity entity_state = State('media_player.demo', 'on') @@ -58,14 +55,15 @@ def test_customize_options(config, name): ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), ('MediaPlayer', 'media_player.test', 'on', - {ATTR_SUPPORTED_FEATURES: SUPPORT_TURN_ON | SUPPORT_TURN_OFF}, - {CONF_MODE: [ON_OFF]}), + {ATTR_SUPPORTED_FEATURES: media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF}, {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: None}}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', - {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), + {ATTR_SUPPORTED_FEATURES: climate.SUPPORT_TARGET_TEMPERATURE_LOW | + climate.SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" @@ -82,7 +80,7 @@ def test_types(type_name, entity_id, state, attrs, config): @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ ('GarageDoorOpener', 'cover.garage_door', 'open', {ATTR_DEVICE_CLASS: 'garage', - ATTR_SUPPORTED_FEATURES: SUPPORT_OPEN | SUPPORT_CLOSE}), + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_OPEN | cover.SUPPORT_CLOSE}), ('WindowCovering', 'cover.set_position', 'open', {ATTR_SUPPORTED_FEATURES: 4}), ('WindowCoveringBasic', 'cover.open_window', 'open', diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index 03135b1418e..d89f9740ea6 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -4,9 +4,10 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_MUTED, DOMAIN) from homeassistant.components.homekit.type_media_players import MediaPlayer from homeassistant.components.homekit.const import ( - ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, + FEATURE_TOGGLE_MUTE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_MODE, SERVICE_MEDIA_PAUSE, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_MUTE, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING) @@ -16,7 +17,9 @@ from tests.common import async_mock_service async def test_media_player_set_state(hass): """Test if accessory and HA are updated accordingly.""" - config = {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + config = {CONF_FEATURE_LIST: { + FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, + FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None}} entity_id = 'media_player.test' hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, @@ -28,32 +31,32 @@ async def test_media_player_set_state(hass): assert acc.aid == 2 assert acc.category == 8 # Switch - assert acc.chars[ON_OFF].value == 0 - assert acc.chars[PLAY_PAUSE].value == 0 - assert acc.chars[PLAY_STOP].value == 0 - assert acc.chars[TOGGLE_MUTE].value == 0 + assert acc.chars[FEATURE_ON_OFF].value == 0 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 + assert acc.chars[FEATURE_PLAY_STOP].value == 0 + assert acc.chars[FEATURE_TOGGLE_MUTE].value == 0 hass.states.async_set(entity_id, STATE_ON, {ATTR_MEDIA_VOLUME_MUTED: True}) await hass.async_block_till_done() - assert acc.chars[ON_OFF].value == 1 - assert acc.chars[TOGGLE_MUTE].value == 1 + assert acc.chars[FEATURE_ON_OFF].value == 1 + assert acc.chars[FEATURE_TOGGLE_MUTE].value == 1 hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() - assert acc.chars[ON_OFF].value == 0 + assert acc.chars[FEATURE_ON_OFF].value == 0 hass.states.async_set(entity_id, STATE_PLAYING) await hass.async_block_till_done() - assert acc.chars[PLAY_PAUSE].value == 1 - assert acc.chars[PLAY_STOP].value == 1 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 1 + assert acc.chars[FEATURE_PLAY_STOP].value == 1 hass.states.async_set(entity_id, STATE_PAUSED) await hass.async_block_till_done() - assert acc.chars[PLAY_PAUSE].value == 0 + assert acc.chars[FEATURE_PLAY_PAUSE].value == 0 hass.states.async_set(entity_id, STATE_IDLE) await hass.async_block_till_done() - assert acc.chars[PLAY_STOP].value == 0 + assert acc.chars[FEATURE_PLAY_STOP].value == 0 # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) @@ -63,43 +66,51 @@ async def test_media_player_set_state(hass): call_media_stop = async_mock_service(hass, DOMAIN, SERVICE_MEDIA_STOP) call_toggle_mute = async_mock_service(hass, DOMAIN, SERVICE_VOLUME_MUTE) - await hass.async_add_job(acc.chars[ON_OFF].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_ON_OFF] + .client_update_value, True) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[ON_OFF].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_ON_OFF] + .client_update_value, False) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] + .client_update_value, True) await hass.async_block_till_done() assert call_media_play assert call_media_play[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_PAUSE].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_PLAY_PAUSE] + .client_update_value, False) await hass.async_block_till_done() assert call_media_pause assert call_media_pause[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] + .client_update_value, True) await hass.async_block_till_done() assert call_media_play assert call_media_play[1].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[PLAY_STOP].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_PLAY_STOP] + .client_update_value, False) await hass.async_block_till_done() assert call_media_stop assert call_media_stop[0].data[ATTR_ENTITY_ID] == entity_id - await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, True) + await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] + .client_update_value, True) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[0].data[ATTR_ENTITY_ID] == entity_id assert call_toggle_mute[0].data[ATTR_MEDIA_VOLUME_MUTED] is True - await hass.async_add_job(acc.chars[TOGGLE_MUTE].client_update_value, False) + await hass.async_add_job(acc.chars[FEATURE_TOGGLE_MUTE] + .client_update_value, False) await hass.async_block_till_done() assert call_toggle_mute assert call_toggle_mute[1].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 56a625e02d7..0bc1eb96841 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,17 +4,18 @@ import voluptuous as vol from homeassistant.core import State from homeassistant.components.homekit.const import ( - HOMEKIT_NOTIFY_ID, ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE) + CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, - validate_media_player_modes) + validate_media_player_features) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_MODE, CONF_NAME, STATE_UNKNOWN, + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -25,7 +26,11 @@ def test_validate_entity_config(): configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, - {'media_player.test': {CONF_MODE: 'invalid_mode'}}] + {'media_player.test': {CONF_FEATURE_LIST: [ + {CONF_FEATURE: 'invalid_feature'}]}}, + {'media_player.test': {CONF_FEATURE_LIST: [ + {CONF_FEATURE: FEATURE_ON_OFF}, + {CONF_FEATURE: FEATURE_ON_OFF}]}}, ] for conf in configs: with pytest.raises(vol.Invalid): @@ -45,23 +50,26 @@ def test_validate_entity_config(): {'lock.demo': {ATTR_CODE: '1234'}} assert vec({'media_player.demo': {}}) == \ - {'media_player.demo': {CONF_MODE: []}} - assert vec({'media_player.demo': {CONF_MODE: [ON_OFF]}}) == \ - {'media_player.demo': {CONF_MODE: [ON_OFF]}} + {'media_player.demo': {CONF_FEATURE_LIST: {}}} + config = {CONF_FEATURE_LIST: [{CONF_FEATURE: FEATURE_ON_OFF}, + {CONF_FEATURE: FEATURE_PLAY_PAUSE}]} + assert vec({'media_player.demo': config}) == \ + {'media_player.demo': {CONF_FEATURE_LIST: + {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} -def test_validate_media_player_modes(): +def test_validate_media_player_features(): """Test validate modes for media players.""" config = {} attrs = {ATTR_SUPPORTED_FEATURES: 20873} entity_state = State('media_player.demo', 'on', attrs) - validate_media_player_modes(entity_state, config) - assert config == {CONF_MODE: [ON_OFF, PLAY_PAUSE, PLAY_STOP, TOGGLE_MUTE]} + assert validate_media_player_features(entity_state, config) is True + + config = {FEATURE_ON_OFF: None} + assert validate_media_player_features(entity_state, config) is True entity_state = State('media_player.demo', 'on') - config = {CONF_MODE: [ON_OFF]} - with pytest.raises(vol.Invalid): - validate_media_player_modes(entity_state, config) + assert validate_media_player_features(entity_state, config) is False def test_convert_to_float(): From 144bb3492ad260f77ef1374d2de15a07c0b04f57 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 28 May 2018 10:32:47 -0400 Subject: [PATCH 851/924] zha/light: Properly parse currentX and currentY on async_update() (#14605) --- homeassistant/components/light/zha.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index b44bf820b23..bd01a513e0b 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -172,7 +172,8 @@ class Light(zha.Entity, light.Light): result = await zha.safe_read(self._endpoint.light_color, ['current_x', 'current_y']) if 'current_x' in result and 'current_y' in result: - xy_color = (result['current_x'], result['current_y']) + xy_color = (round(result['current_x']/65535, 3), + round(result['current_y']/65535, 3)) self._hs_color = color_util.color_xy_to_hs(*xy_color) @property From 07255a29b4c44a6ece10a20c91b062dd906f0cbe Mon Sep 17 00:00:00 2001 From: Enrico Berndt Date: Mon, 28 May 2018 16:41:51 +0200 Subject: [PATCH 852/924] Add tv channel and volume level for philips js API 5 (#14276) * PhilipsTV API 5: Added tv channel change and setting of volume level. * set_volume only sets volume via api now and nothing else. --- .../components/media_player/philips_js.py | 20 +++++++++++-------- requirements_all.txt | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/philips_js.py b/homeassistant/components/media_player/philips_js.py index d526fbb0387..01d63e0b6c8 100644 --- a/homeassistant/components/media_player/philips_js.py +++ b/homeassistant/components/media_player/philips_js.py @@ -13,20 +13,22 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.media_player import ( PLATFORM_SCHEMA, SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, SUPPORT_PLAY, MediaPlayerDevice) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + SUPPORT_PLAY, MediaPlayerDevice) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_API_VERSION, STATE_OFF, STATE_ON, STATE_UNKNOWN) from homeassistant.helpers.script import Script from homeassistant.util import Throttle -REQUIREMENTS = ['ha-philipsjs==0.0.3'] +REQUIREMENTS = ['ha-philipsjs==0.0.4'] _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ - SUPPORT_VOLUME_MUTE | SUPPORT_SELECT_SOURCE + SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ + SUPPORT_SELECT_SOURCE SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY @@ -165,6 +167,10 @@ class PhilipsTV(MediaPlayerDevice): if not self._tv.on: self._state = STATE_OFF + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._tv.setVolume(volume) + def media_previous_track(self): """Send rewind command.""" self._tv.sendKey('Previous') @@ -189,12 +195,10 @@ class PhilipsTV(MediaPlayerDevice): self._volume = self._tv.volume self._muted = self._tv.muted if self._tv.source_id: - src = self._tv.sources.get(self._tv.source_id, None) - if src: - self._source = src.get('name', None) + self._source = self._tv.getSourceName(self._tv.source_id) if self._tv.sources and not self._source_list: - for srcid in sorted(self._tv.sources): - srcname = self._tv.sources.get(srcid, dict()).get('name', None) + for srcid in self._tv.sources: + srcname = self._tv.getSourceName(srcid) self._source_list.append(srcname) self._source_mapping[srcname] = srcid if self._tv.on: diff --git a/requirements_all.txt b/requirements_all.txt index 78239492ce9..ef4914a5ec0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -368,7 +368,7 @@ gstreamer-player==1.1.0 ha-ffmpeg==1.9 # homeassistant.components.media_player.philips_js -ha-philipsjs==0.0.3 +ha-philipsjs==0.0.4 # homeassistant.components.sensor.geo_rss_events haversine==0.4.5 From 6c3e2021df523be5f47f23dfead4f0d0cb565a92 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 29 May 2018 04:49:38 +0300 Subject: [PATCH 853/924] Give unknown zwave nodes a better name (#14353) * Give unknown zwave nodes a better name * Update util.py --- homeassistant/components/zwave/util.py | 8 +++++--- tests/components/zwave/test_init.py | 5 +++-- tests/components/zwave/test_node_entity.py | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 1c0bb14f7e5..b62eeb67d32 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -68,8 +68,10 @@ def check_value_schema(value, schema): def node_name(node): """Return the name of the node.""" - return node.name or '{} {}'.format( - node.manufacturer_name, node.product_name) + if is_node_parsed(node): + return node.name or '{} {}'.format( + node.manufacturer_name, node.product_name) + return 'Unknown Node {}'.format(node.node_id) async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): @@ -89,4 +91,4 @@ async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): def is_node_parsed(node): """Check whether the node has been parsed or still waiting to be parsed.""" - return node.manufacturer_name and node.product_name + return bool((node.manufacturer_name and node.product_name) or node.name) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index a25b725e500..e608dcccaba 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -238,7 +238,8 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) + node = MockNode( + node_id=14, manufacturer_name=None, name=None, is_ready=False) sleeps = [] @@ -263,7 +264,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_logger.warning.mock_calls) == 1 assert mock_logger.warning.mock_calls[0][1][1:] == \ (14, const.NODE_READY_WAIT_SECS) - assert hass.states.get('zwave.mock_node').state is 'unknown' + assert hass.states.get('zwave.unknown_node_14').state is 'unknown' @asyncio.coroutine diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index f4d9b3ef0e8..b91245d5a12 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -363,6 +363,7 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_unique_id_missing_data(self): """Test unique_id.""" self.node.manufacturer_name = None + self.node.name = None entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) self.assertIsNone(entity.unique_id) From 9bc26e93a4340b4a4e2eb35f4732a6b60d435f43 Mon Sep 17 00:00:00 2001 From: Robert Accettura Date: Tue, 29 May 2018 01:50:27 -0400 Subject: [PATCH 854/924] Add pin pad to alarm panel (#14178) * Add pin pad to alarm panel * Add pin pad to alarm panel * Update regex * Update regex * Update regex * Add pin pad to alarm panel * Add pin pad to alarm panel * Add pin pad to alarm panel * Add pin pad to alarm panel * Fix typos * Fix typos * Fix typos * Add pin pad to alarm panel * Fix errors --- .../components/alarm_control_panel/alarmdecoder.py | 4 ++-- .../components/alarm_control_panel/alarmdotcom.py | 9 +++++++-- .../components/alarm_control_panel/concord232.py | 2 +- .../components/alarm_control_panel/envisalink.py | 2 +- homeassistant/components/alarm_control_panel/ifttt.py | 9 +++++++-- homeassistant/components/alarm_control_panel/manual.py | 9 +++++++-- .../components/alarm_control_panel/manual_mqtt.py | 9 +++++++-- homeassistant/components/alarm_control_panel/mqtt.py | 9 +++++++-- homeassistant/components/alarm_control_panel/nx584.py | 4 ++-- .../components/alarm_control_panel/simplisafe.py | 9 +++++++-- homeassistant/components/alarm_control_panel/verisure.py | 4 ++-- 11 files changed, 50 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 49df9f2cefa..13b51aea701 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -100,8 +100,8 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): - """Return the regex for code format or None if no code is required.""" - return '^\\d{4,6}$' + """Return one or more digits/characters.""" + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 31d93373286..6b523e8b606 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.alarmdotcom/ """ import asyncio import logging +import re import voluptuous as vol @@ -79,8 +80,12 @@ class AlarmDotCom(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index d48a107f33d..bd3ee762ccb 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return the characters if code is defined.""" - return '[0-9]{4}([0-9]{2})?' + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index e5003f1ba1d..19bbfa611f2 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return '^\\d{4,6}$' + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 7bdc1ccd9d9..203044f3915 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.ifttt/ """ import logging +import re import voluptuous as vol @@ -124,8 +125,12 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index 5beb5261607..e66251143da 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/alarm_control_panel.manual/ import copy import datetime import logging +import re import voluptuous as vol @@ -201,8 +202,12 @@ class ManualAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index 4b08ad67292..c09105c91e0 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -8,6 +8,7 @@ import asyncio import copy import datetime import logging +import re import voluptuous as vol @@ -237,8 +238,12 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 1422136c405..0298c7384a2 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -6,6 +6,7 @@ https://home-assistant.io/components/alarm_control_panel.mqtt/ """ import asyncio import logging +import re import voluptuous as vol @@ -117,8 +118,12 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): @property def code_format(self): - """One or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' @asyncio.coroutine def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index ceb79c1dc7b..67d3725fc38 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -69,8 +69,8 @@ class NX584Alarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return che characters if code is defined.""" - return '[0-9]{4}([0-9]{2})?' + """Return one or more digits/characters.""" + return '^\\d+$' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index 3b991c5b236..c08ac3c0ea0 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/alarm_control_panel.simplisafe/ """ import logging +import re import voluptuous as vol @@ -83,8 +84,12 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return one or more characters if code is defined.""" - return None if self._code is None else '.+' + """Return one or more digits/characters.""" + if self._code is None: + return None + elif isinstance(self._code, str) and re.search('^\\d+$', self._code): + return '^\\d+$' + return '.+' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 74d63b1fb9c..6651334400f 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -60,8 +60,8 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): - """Return the code format as regex.""" - return '^\\d{%s}$' % self._digits + """Return one or more digits/characters.""" + return '^\\d+$' @property def changed_by(self): From 79efb0e607282118cd3318cf536b64c81eb98769 Mon Sep 17 00:00:00 2001 From: Bakkoda Date: Tue, 29 May 2018 01:51:14 -0400 Subject: [PATCH 855/924] Update mfi.py (#14667) Add ability to read door sensor states from the mPort. --- homeassistant/components/sensor/mfi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/mfi.py b/homeassistant/components/sensor/mfi.py index ecea0815e79..f6bec3284c3 100644 --- a/homeassistant/components/sensor/mfi.py +++ b/homeassistant/components/sensor/mfi.py @@ -33,6 +33,7 @@ DIGITS = { SENSOR_MODELS = [ 'Ubiquiti mFi-THS', 'Ubiquiti mFi-CS', + 'Ubiquiti mFi-DS', 'Outlet', 'Input Analog', 'Input Digital', From d36c7c3de792cb5c74717af53c3b8eed71098957 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Mon, 28 May 2018 23:42:27 -0700 Subject: [PATCH 856/924] Increase Eufy's requirement on lakeside (#14671) python-lakeside was broken with at least some versions of the Python protobuf code, so bump the requirement to a fixed version. --- homeassistant/components/eufy.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 892c0b9972a..e86e7348d58 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.6'] +REQUIREMENTS = ['lakeside==0.7'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ef4914a5ec0..3e79b2981f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ keyrings.alt==3.1 konnected==0.1.2 # homeassistant.components.eufy -lakeside==0.6 +lakeside==0.7 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From 8c7f0669c699bb927eaa95c53fb9756ba72c33ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 May 2018 02:51:08 -0400 Subject: [PATCH 857/924] Allow hassio frontend development (#14675) * Allow hassio frontend development * Fix tests --- homeassistant/components/hassio/__init__.py | 24 ++++++++++++++++++++- tests/components/hassio/test_init.py | 20 +++++++++++------ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index aa24cc61af3..45c35dcdd2a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -28,6 +28,15 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'hassio' DEPENDENCIES = ['http'] +CONF_FRONTEND_REPO = 'development_repo' + +CONFIG_SCHEMA = vol.Schema({ + vol.Optional(DOMAIN): vol.Schema({ + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + }), +}, extra=vol.ALLOW_EXTRA) + + DATA_HOMEASSISTANT_VERSION = 'hassio_hass_version' HASSIO_UPDATE_INTERVAL = timedelta(minutes=55) @@ -142,7 +151,13 @@ def async_setup(hass, config): try: host = os.environ['HASSIO'] except KeyError: - _LOGGER.error("No Hass.io supervisor detect") + _LOGGER.error("Missing HASSIO environment variable.") + return False + + try: + os.environ['HASSIO_TOKEN'] + except KeyError: + _LOGGER.error("Missing HASSIO_TOKEN environment variable.") return False websession = hass.helpers.aiohttp_client.async_get_clientsession() @@ -152,6 +167,13 @@ def async_setup(hass, config): _LOGGER.error("Not connected with Hass.io") return False + # This overrides the normal API call that would be forwarded + development_repo = config.get(DOMAIN, {}).get(CONF_FRONTEND_REPO) + if development_repo is not None: + hass.http.register_static_path( + '/api/hassio/app-es5', + os.path.join(development_repo, 'hassio/build-es5'), False) + hass.http.register_view(HassIOView(host, websession)) if 'frontend' in hass.config.components: diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index e17419e7fd5..f67a6cbccec 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -9,6 +9,12 @@ from homeassistant.components.hassio import async_check_config from tests.common import mock_coro +MOCK_ENVIRON = { + 'HASSIO': '127.0.0.1', + 'HASSIO_TOKEN': 'abcdefgh', +} + + @asyncio.coroutine def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" @@ -18,7 +24,7 @@ def test_setup_api_ping(hass, aioclient_mock): "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', {}) assert result @@ -38,7 +44,7 @@ def test_setup_api_push_api_data(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { 'api_password': "123456", @@ -66,7 +72,7 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': { 'api_password': "123456", @@ -95,7 +101,7 @@ def test_setup_api_push_api_data_default(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'http': {}, 'hassio': {} @@ -119,7 +125,7 @@ def test_setup_core_push_timezone(hass, aioclient_mock): aioclient_mock.post( "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}): + with patch.dict(os.environ, MOCK_ENVIRON): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, 'homeassistant': { @@ -143,7 +149,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={'result': 'ok'}) - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + with patch.dict(os.environ, MOCK_ENVIRON), \ patch.dict(os.environ, {'HASSIO_TOKEN': "123456"}): result = yield from async_setup_component(hass, 'hassio', { 'hassio': {}, @@ -165,7 +171,7 @@ def test_fail_setup_without_environ_var(hass): @asyncio.coroutine def test_fail_setup_cannot_connect(hass): """Fail setup if cannot connect.""" - with patch.dict(os.environ, {'HASSIO': "127.0.0.1"}), \ + with patch.dict(os.environ, MOCK_ENVIRON), \ patch('homeassistant.components.hassio.HassIO.is_connected', Mock(return_value=mock_coro(None))): result = yield from async_setup_component(hass, 'hassio', {}) From f2a2f2cca5f1a677a57a4ae0bfb87e396e143237 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 29 May 2018 10:15:30 +0200 Subject: [PATCH 858/924] Ignore unsupported Sonos favorite lists (#14665) --- .../components/media_player/sonos.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 06e5f3befe4..0f536e1edfb 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -427,15 +427,18 @@ class SonosDevice(MediaPlayerDevice): self.update_volume() self._favorites = [] - for fav in self.soco.music_library.get_sonos_favorites(): - # SoCo 0.14 raises a generic Exception on invalid xml in favorites. - # Filter those out now so our list is safe to use. - try: - if fav.reference.get_uri(): - self._favorites.append(fav) - # pylint: disable=broad-except - except Exception: - _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + # pylint: disable=broad-except + try: + for fav in self.soco.music_library.get_sonos_favorites(): + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) + except Exception: + _LOGGER.debug("Ignoring invalid favorite list") def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" From fcb60d472ea0fcc0121c8163be15d1987be775e9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 29 May 2018 15:03:45 +0200 Subject: [PATCH 859/924] MQTT Cover Fix Assumed State (#14672) --- homeassistant/components/cover/mqtt.py | 5 +++++ tests/components/cover/test_mqtt.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 0f31d3a9fe0..235ff5799cc 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -235,6 +235,11 @@ class MqttCover(MqttAvailability, CoverDevice): """No polling needed.""" return False + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + @property def name(self): """Return the name of the cover.""" diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index 23a7b32fc28..aea6398e3ae 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -2,8 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN,\ - STATE_UNAVAILABLE +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, \ + STATE_UNAVAILABLE, ATTR_ASSUMED_STATE import homeassistant.components.cover as cover from homeassistant.components.cover.mqtt import MqttCover @@ -40,6 +40,7 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'state-topic', '0') self.hass.block_till_done() @@ -112,6 +113,7 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) cover.open_cover(self.hass, 'cover.test') self.hass.block_till_done() From eff1d1f14e6335eb3194a970279085bc38cc8695 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 29 May 2018 09:05:07 -0400 Subject: [PATCH 860/924] zha: fix temperature rounding for ZHA temperature sensors. (#14669) --- homeassistant/components/sensor/zha.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 3051d8f2afa..53e0e8d0329 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -114,9 +114,11 @@ class TemperatureSensor(Sensor): """Return the state of the entity.""" if self._state is None: return None - celsius = round(float(self._state) / 100, 1) - return convert_temperature( - celsius, TEMP_CELSIUS, self.unit_of_measurement) + celsius = self._state / 100 + return round(convert_temperature(celsius, + TEMP_CELSIUS, + self.unit_of_measurement), + 1) class RelativeHumiditySensor(Sensor): From 3b38de63ea92296b49eae196c8802837dae6f03d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 29 May 2018 16:03:00 +0200 Subject: [PATCH 861/924] Allow user-defined sensors (#14613) * Allow user-defined sensors * Require element for resources * Don't use .get() --- homeassistant/components/sensor/netdata.py | 164 ++++++++++----------- requirements_all.txt | 3 + 2 files changed, 84 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/sensor/netdata.py b/homeassistant/components/sensor/netdata.py index 0d2a542c7bb..2d08159967c 100644 --- a/homeassistant/components/sensor/netdata.py +++ b/homeassistant/components/sensor/netdata.py @@ -4,154 +4,152 @@ Support gathering system information of hosts which are running netdata. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.netdata/ """ -import logging from datetime import timedelta -from urllib.parse import urlsplit +import logging -import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_HOST, CONF_PORT, CONF_NAME, CONF_RESOURCES) + CONF_HOST, CONF_ICON, CONF_NAME, CONF_PORT, CONF_RESOURCES) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['netdata==0.1.2'] _LOGGER = logging.getLogger(__name__) -_RESOURCE = 'api/v1' -_REALTIME = 'before=0&after=-1&options=seconds' + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) + +CONF_ELEMENT = 'element' DEFAULT_HOST = 'localhost' DEFAULT_NAME = 'Netdata' -DEFAULT_PORT = '19999' +DEFAULT_PORT = 19999 -SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_ICON = 'mdi:desktop-classic' -SENSOR_TYPES = { - 'memory_free': ['RAM Free', 'MiB', 'system.ram', 'free', 1], - 'memory_used': ['RAM Used', 'MiB', 'system.ram', 'used', 1], - 'memory_cached': ['RAM Cached', 'MiB', 'system.ram', 'cached', 1], - 'memory_buffers': ['RAM Buffers', 'MiB', 'system.ram', 'buffers', 1], - 'swap_free': ['Swap Free', 'MiB', 'system.swap', 'free', 1], - 'swap_used': ['Swap Used', 'MiB', 'system.swap', 'used', 1], - 'processes_running': ['Processes Running', 'Count', 'system.processes', - 'running', 0], - 'processes_blocked': ['Processes Blocked', 'Count', 'system.processes', - 'blocked', 0], - 'system_load': ['System Load', '15 min', 'system.load', 'load15', 2], - 'system_io_in': ['System IO In', 'Count', 'system.io', 'in', 0], - 'system_io_out': ['System IO Out', 'Count', 'system.io', 'out', 0], - 'ipv4_in': ['IPv4 In', 'kb/s', 'system.ipv4', 'received', 0], - 'ipv4_out': ['IPv4 Out', 'kb/s', 'system.ipv4', 'sent', 0], - 'disk_free': ['Disk Free', 'GiB', 'disk_space._', 'avail', 2], - 'cpu_iowait': ['CPU IOWait', '%', 'system.cpu', 'iowait', 1], - 'cpu_user': ['CPU User', '%', 'system.cpu', 'user', 1], - 'cpu_system': ['CPU System', '%', 'system.cpu', 'system', 1], - 'cpu_softirq': ['CPU SoftIRQ', '%', 'system.cpu', 'softirq', 1], - 'cpu_guest': ['CPU Guest', '%', 'system.cpu', 'guest', 1], - 'uptime': ['Uptime', 's', 'system.uptime', 'uptime', 0], - 'packets_received': ['Packets Received', 'packets/s', 'ipv4.packets', - 'received', 0], - 'packets_sent': ['Packets Sent', 'packets/s', 'ipv4.packets', - 'sent', 0], - 'connections': ['Active Connections', 'Count', - 'netfilter.conntrack_sockets', 'connections', 0] -} +RESOURCE_SCHEMA = vol.Any({ + vol.Required(CONF_ELEMENT): cv.string, + vol.Optional(CONF_ICON, default=DEFAULT_ICON): cv.icon, + vol.Optional(CONF_NAME): cv.string, +}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_RESOURCES, default=['memory_free']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Required(CONF_RESOURCES): vol.Schema({cv.string: RESOURCE_SCHEMA}), }) -# pylint: disable=unused-variable -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Netdata sensor.""" + from netdata import Netdata + name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = 'http://{}:{}'.format(host, port) - data_url = '{}/{}/data?chart='.format(url, _RESOURCE) resources = config.get(CONF_RESOURCES) - values = {} - for key, value in sorted(SENSOR_TYPES.items()): - if key in resources: - values.setdefault(value[2], []).append(key) + session = async_get_clientsession(hass) + netdata = NetdataData(Netdata(host, hass.loop, session, port=port)) + await netdata.async_update() + + if netdata.api.metrics is None: + raise PlatformNotReady dev = [] - for chart in values: - rest_url = '{}{}&{}'.format(data_url, chart, _REALTIME) - rest = NetdataData(rest_url) - rest.update() - for sensor_type in values[chart]: - dev.append(NetdataSensor(rest, name, sensor_type)) + for entry, data in resources.items(): + sensor = entry + element = data[CONF_ELEMENT] + sensor_name = icon = None + try: + resource_data = netdata.api.metrics[sensor] + unit = '%' if resource_data['units'] == 'percentage' else \ + resource_data['units'] + if data is not None: + sensor_name = data.get(CONF_NAME) + icon = data.get(CONF_ICON) + except KeyError: + _LOGGER.error("Sensor is not available: %s", sensor) + continue - add_devices(dev, True) + dev.append(NetdataSensor( + netdata, name, sensor, sensor_name, element, icon, unit)) + + async_add_devices(dev, True) class NetdataSensor(Entity): """Implementation of a Netdata sensor.""" - def __init__(self, rest, name, sensor_type): + def __init__( + self, netdata, name, sensor, sensor_name, element, icon, unit): """Initialize the Netdata sensor.""" - self.rest = rest - self.type = sensor_type - self._name = '{} {}'.format(name, SENSOR_TYPES[self.type][0]) - self._precision = SENSOR_TYPES[self.type][4] - self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self.netdata = netdata + self._state = None + self._sensor = sensor + self._element = element + self._sensor_name = self._sensor if sensor_name is None else \ + sensor_name + self._name = name + self._icon = icon + self._unit_of_measurement = unit @property def name(self): """Return the name of the sensor.""" - return self._name + return '{} {}'.format(self._name, self._sensor_name) @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" return self._unit_of_measurement + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return self._icon + @property def state(self): """Return the state of the resources.""" - value = self.rest.data - - if value is not None: - netdata_id = SENSOR_TYPES[self.type][3] - if netdata_id in value: - return "{0:.{1}f}".format(value[netdata_id], self._precision) - return None + return self._state @property def available(self): """Could the resource be accessed during the last update call.""" - return self.rest.available + return self.netdata.available - def update(self): + async def async_update(self): """Get the latest data from Netdata REST API.""" - self.rest.update() + await self.netdata.async_update() + resource_data = self.netdata.api.metrics.get(self._sensor) + self._state = round( + resource_data['dimensions'][self._element]['value'], 2) class NetdataData(object): """The class for handling the data retrieval.""" - def __init__(self, resource): + def __init__(self, api): """Initialize the data object.""" - self._resource = resource - self.data = None + self.api = api self.available = True - def update(self): + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): """Get the latest data from the Netdata REST API.""" + from netdata.exceptions import NetdataError + try: - response = requests.get(self._resource, timeout=5) - det = response.json() - self.data = {k: v for k, v in zip(det['labels'], det['data'][0])} + await self.api.get_allmetrics() self.available = True - except requests.exceptions.ConnectionError: - _LOGGER.error("Connection error: %s", urlsplit(self._resource)[1]) - self.data = None + except NetdataError: + _LOGGER.error("Unable to retrieve data from Netdata") self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index 3e79b2981f5..3e7b966af4f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -567,6 +567,9 @@ nad_receiver==0.0.9 # homeassistant.components.light.nanoleaf_aurora nanoleaf==0.4.1 +# homeassistant.components.sensor.netdata +netdata==0.1.2 + # homeassistant.components.discovery netdisco==1.4.1 From 8c93b484c449f653191b616beb52ec77dd878251 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 29 May 2018 16:09:53 +0200 Subject: [PATCH 862/924] deCONZ - Option to load or not to load clip sensors on start (#14480) * Option to load or not to load clip sensors on start * Full flow * Fix config flow and add tests * Fix attribute dark reporting properly * Imported and properly configured deCONZ shouldn't need extra input to create config entry --- .../components/binary_sensor/deconz.py | 10 ++- .../components/deconz/.translations/en.json | 8 ++- homeassistant/components/deconz/__init__.py | 8 ++- .../components/deconz/config_flow.py | 69 +++++++++++++------ homeassistant/components/deconz/const.py | 2 + homeassistant/components/deconz/strings.json | 8 ++- homeassistant/components/sensor/deconz.py | 11 ++- tests/components/binary_sensor/test_deconz.py | 18 ++++- tests/components/deconz/test_config_flow.py | 37 ++++++++-- tests/components/deconz/test_init.py | 18 +++++ tests/components/sensor/test_deconz.py | 18 ++++- 11 files changed, 165 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index 9faa703d13c..6f59da0755a 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,7 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,10 +28,13 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add binary sensor from deCONZ.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_BINARY_SENSOR: + if sensor.type in DECONZ_BINARY_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): entities.append(DeconzBinarySensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -103,6 +107,6 @@ class DeconzBinarySensor(BinarySensorDevice): attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery - if self._sensor.type in PRESENCE and self._sensor.dark: + if self._sensor.type in PRESENCE and self._sensor.dark is not None: attr['dark'] = self._sensor.dark return attr diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 0009986d45f..a2f90e49e3a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -19,8 +19,14 @@ "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button", "title": "Link with deCONZ" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data": { + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, - "title": "deCONZ" + "title": "deCONZ Zigbee gateway" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index bbab4029d7e..850645225d0 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -19,8 +19,8 @@ from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts from .const import ( - CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, - DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) + CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT, + DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) REQUIREMENTS = ['pydeconz==38'] @@ -104,8 +104,10 @@ async def async_setup_entry(hass, config_entry): def async_add_remote(sensors): """Setup remote from deCONZ.""" from pydeconz.sensor import SWITCH as DECONZ_REMOTE + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_REMOTE: + if sensor.type in DECONZ_REMOTE and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index e900782ea65..cb7c3aad7fd 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,13 +8,15 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client from homeassistant.util.json import load_json -from .const import CONFIG_FILE, DOMAIN +from .const import CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN + +CONF_BRIDGEID = 'bridgeid' @callback def configured_hosts(hass): """Return a set of the configured hosts.""" - return set(entry.data['host'] for entry + return set(entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN)) @@ -30,7 +32,12 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): self.deconz_config = {} async def async_step_init(self, user_input=None): - """Handle a deCONZ config flow start.""" + """Handle a deCONZ config flow start. + + Only allows one instance to be set up. + If only one bridge is found go to link step. + If more than one bridge is found let user choose bridge to link. + """ from pydeconz.utils import async_discovery if configured_hosts(self.hass): @@ -65,7 +72,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): async def async_step_link(self, user_input=None): """Attempt to link with the deCONZ bridge.""" - from pydeconz.utils import async_get_api_key, async_get_bridgeid + from pydeconz.utils import async_get_api_key errors = {} if user_input is not None: @@ -75,13 +82,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): api_key = await async_get_api_key(session, **self.deconz_config) if api_key: self.deconz_config[CONF_API_KEY] = api_key - if 'bridgeid' not in self.deconz_config: - self.deconz_config['bridgeid'] = await async_get_bridgeid( - session, **self.deconz_config) - return self.async_create_entry( - title='deCONZ-' + self.deconz_config['bridgeid'], - data=self.deconz_config - ) + return await self.async_step_options() errors['base'] = 'no_key' return self.async_show_form( @@ -89,6 +90,34 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): errors=errors, ) + async def async_step_options(self, user_input=None): + """Extra options for deCONZ. + + CONF_CLIP_SENSOR -- Allow user to choose if they want clip sensors. + """ + from pydeconz.utils import async_get_bridgeid + + if user_input is not None: + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = \ + user_input[CONF_ALLOW_CLIP_SENSOR] + + if CONF_BRIDGEID not in self.deconz_config: + session = aiohttp_client.async_get_clientsession(self.hass) + self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid( + session, **self.deconz_config) + + return self.async_create_entry( + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config + ) + + return self.async_show_form( + step_id='options', + data_schema=vol.Schema({ + vol.Optional(CONF_ALLOW_CLIP_SENSOR): bool, + }), + ) + async def async_step_discovery(self, discovery_info): """Prepare configuration for a discovered deCONZ bridge. @@ -97,7 +126,7 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): deconz_config = {} deconz_config[CONF_HOST] = discovery_info.get(CONF_HOST) deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) - deconz_config['bridgeid'] = discovery_info.get('serial') + deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') config_file = await self.hass.async_add_job( load_json, self.hass.config.path(CONFIG_FILE)) @@ -121,19 +150,15 @@ class DeconzFlowHandler(data_entry_flow.FlowHandler): Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - from pydeconz.utils import async_get_bridgeid - if configured_hosts(self.hass): return self.async_abort(reason='one_instance_only') - elif CONF_API_KEY not in import_config: - self.deconz_config = import_config + + self.deconz_config = import_config + if CONF_API_KEY not in import_config: return await self.async_step_link() - if 'bridgeid' not in import_config: - session = aiohttp_client.async_get_clientsession(self.hass) - import_config['bridgeid'] = await async_get_bridgeid( - session, **import_config) + self.deconz_config[CONF_ALLOW_CLIP_SENSOR] = True return self.async_create_entry( - title='deCONZ-' + import_config['bridgeid'], - data=import_config + title='deCONZ-' + self.deconz_config[CONF_BRIDGEID], + data=self.deconz_config ) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 48e5ea75d68..43f3c6441da 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -8,3 +8,5 @@ CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' DATA_DECONZ_UNSUB = 'deconz_dispatchers' + +CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 7ea68af01c1..cabe58694d2 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,6 @@ { "config": { - "title": "deCONZ", + "title": "deCONZ Zigbee gateway", "step": { "init": { "title": "Define deCONZ gateway", @@ -12,6 +12,12 @@ "link": { "title": "Link with deCONZ", "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + }, + "options": { + "title": "Extra configuration options for deCONZ", + "data":{ + "allow_clip_sensor": "Allow importing virtual sensors" + } } }, "error": { diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index 221cdf2129e..0db06622ad8 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,7 +5,8 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) + CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback @@ -33,14 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_devices): """Add sensors from deCONZ.""" from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE entities = [] + allow_clip_sensor = config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, True) for sensor in sensors: - if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_SENSOR and \ + not (not allow_clip_sensor and sensor.type.startswith('CLIP')): if sensor.type in DECONZ_REMOTE: if sensor.battery: entities.append(DeconzBattery(sensor)) else: entities.append(DeconzSensor(sensor)) async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) @@ -114,9 +118,12 @@ class DeconzSensor(Entity): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + from pydeconz.sensor import LIGHTLEVEL attr = {} if self._sensor.battery: attr[ATTR_BATTERY_LEVEL] = self._sensor.battery + if self._sensor.type in LIGHTLEVEL and self._sensor.dark is not None: + attr['dark'] = self._sensor.dark if self.unit_of_measurement == 'Watts': attr[ATTR_CURRENT] = self._sensor.current attr[ATTR_VOLTAGE] = self._sensor.voltage diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 88dd0dae737..2e33e28fa57 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -26,7 +26,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ binary sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -41,7 +41,8 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup( config_entry, 'binary_sensor') # To flush out the service call to update the group @@ -77,3 +78,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_allow_clip_sensor(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d86475b35ef..df3310f3d6f 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -21,7 +21,9 @@ async def test_flow_works(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass await flow.async_step_init() - result = await flow.async_step_link(user_input={}) + await flow.async_step_link(user_input={}) + result = await flow.async_step_options( + user_input={'allow_clip_sensor': True}) assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' @@ -29,7 +31,8 @@ async def test_flow_works(hass, aioclient_mock): 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -146,14 +149,14 @@ async def test_bridge_discovery_config_file(hass): 'port': 80, 'serial': 'id' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True } @@ -214,12 +217,34 @@ async def test_import_with_api_key(hass): 'port': 80, 'api_key': '1234567890ABCDEF' }) - assert result['type'] == 'create_entry' assert result['title'] == 'deCONZ-id' assert result['data'] == { 'bridgeid': 'id', 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF' + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': True + } + + +async def test_options(hass, aioclient_mock): + """Test that options work and that bridgeid can be requested.""" + aioclient_mock.get('http://1.2.3.4:80/api/1234567890ABCDEF/config', + json={"bridgeid": "id"}) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF'} + result = await flow.async_step_options( + user_input={'allow_clip_sensor': False}) + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ-id' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': 80, + 'api_key': '1234567890ABCDEF', + 'allow_clip_sensor': False } diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 888094deea6..1cee08feb0a 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -172,3 +172,21 @@ async def test_add_new_remote(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) await hass.async_block_till_done() assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 + + +async def test_do_not_allow_clip_sensor(hass): + """Test that clip sensors can be ignored.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, + 'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False} + remote = Mock() + remote.name = 'name' + remote.type = 'CLIPSwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index 8f6a53e6e65..d7cdb458646 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -41,7 +41,7 @@ SENSOR = { } -async def setup_bridge(hass, data): +async def setup_bridge(hass, data, allow_clip_sensor=True): """Load the deCONZ sensor platform.""" from pydeconz import DeconzSession loop = Mock() @@ -57,7 +57,8 @@ async def setup_bridge(hass, data): hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( - 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + 1, deconz.DOMAIN, 'Mock Title', + {'host': 'mock-host', 'allow_clip_sensor': allow_clip_sensor}, 'test') await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor') # To flush out the service call to update the group await hass.async_block_till_done() @@ -97,3 +98,16 @@ async def test_add_new_sensor(hass): async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) await hass.async_block_till_done() assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_do_not_allow_clipsensor(hass): + """Test that clip sensors can be ignored.""" + data = {} + await setup_bridge(hass, data, allow_clip_sensor=False) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'CLIPTemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 From 41054296396def7d004011491e4671350289c079 Mon Sep 17 00:00:00 2001 From: Thibault Cohen Date: Tue, 29 May 2018 10:23:12 -0400 Subject: [PATCH 863/924] Add asyncio support for Ebox (#14183) * Fix Ebox sensor * Fix #14183 comments * Update ebox.py * Update ebox.py * Continue fixing comments --- homeassistant/components/sensor/ebox.py | 45 +++++++++++++++---------- requirements_all.txt | 3 ++ 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/sensor/ebox.py b/homeassistant/components/sensor/ebox.py index aca2d7bdb9a..d7b867081a3 100644 --- a/homeassistant/components/sensor/ebox.py +++ b/homeassistant/components/sensor/ebox.py @@ -9,7 +9,6 @@ https://home-assistant.io/components/sensor.ebox/ import logging from datetime import timedelta -import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -18,9 +17,11 @@ from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_MONITORED_VARIABLES) from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +from homeassistant.exceptions import PlatformNotReady -# pylint: disable=import-error -REQUIREMENTS = [] # ['pyebox==0.1.0'] - disabled because it breaks pip10 + +REQUIREMENTS = ['pyebox==1.1.4'] _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,8 @@ PERCENT = '%' # type: str DEFAULT_NAME = 'EBox' REQUESTS_TIMEOUT = 15 -SCAN_INTERVAL = timedelta(minutes=5) +SCAN_INTERVAL = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { 'usage': ['Usage', PERCENT, 'mdi:percent'], @@ -62,25 +64,29 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up the EBox sensor.""" username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - try: - ebox_data = EBoxData(username, password) - ebox_data.update() - except requests.exceptions.HTTPError as error: - _LOGGER.error("Failed login: %s", error) - return False + httpsession = hass.helpers.aiohttp_client.async_get_clientsession() + ebox_data = EBoxData(username, password, httpsession) name = config.get(CONF_NAME) + from pyebox.client import PyEboxError + try: + await ebox_data.async_update() + except PyEboxError as exp: + _LOGGER.error("Failed login: %s", exp) + raise PlatformNotReady + sensors = [] for variable in config[CONF_MONITORED_VARIABLES]: sensors.append(EBoxSensor(ebox_data, variable, name)) - add_devices(sensors, True) + async_add_devices(sensors, True) class EBoxSensor(Entity): @@ -116,9 +122,9 @@ class EBoxSensor(Entity): """Icon to use in the frontend, if any.""" return self._icon - def update(self): + async def async_update(self): """Get the latest data from EBox and update the state.""" - self.ebox_data.update() + await self.ebox_data.async_update() if self.type in self.ebox_data.data: self._state = round(self.ebox_data.data[self.type], 2) @@ -126,18 +132,21 @@ class EBoxSensor(Entity): class EBoxData(object): """Get data from Ebox.""" - def __init__(self, username, password): + def __init__(self, username, password, httpsession): """Initialize the data object.""" from pyebox import EboxClient - self.client = EboxClient(username, password, REQUESTS_TIMEOUT) + self.client = EboxClient(username, password, + REQUESTS_TIMEOUT, httpsession) self.data = {} - def update(self): + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): """Get the latest data from Ebox.""" from pyebox.client import PyEboxError try: - self.client.fetch_data() + await self.client.fetch_data() except PyEboxError as exp: _LOGGER.error("Error on receive last EBox data: %s", exp) return + # Update data self.data = self.client.get_data() diff --git a/requirements_all.txt b/requirements_all.txt index 3e7b966af4f..958b0f1027e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -774,6 +774,9 @@ pydispatcher==2.0.5 # homeassistant.components.android_ip_webcam pydroid-ipcam==0.8 +# homeassistant.components.sensor.ebox +pyebox==1.1.4 + # homeassistant.components.climate.econet pyeconet==0.0.5 From 084b3287ab6755b70cd812a3c8fd83a8098159b5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 May 2018 13:02:16 -0600 Subject: [PATCH 864/924] Add sensors and services to RainMachine (#14326) * Starting to add attributes * All attributes added to programs * Basic zone attributes in place * Added advanced properties for zones * We shouldn't calculate the MAC with every entity * Small fixes * Basic framework for push in play * I THINK IT'S WORKING * Some state cleanup * Restart * Restart part 2 * Added stub for service schema * Update * Added services * Small service description update * Lint * Updated CODEOWNERS * Moving to async methods * Fixed coverage test * Lint * Removed unnecessary hass reference * Lint * Lint * Round 1 of Owner-requested changes * Round 2 of Owner-requested changes * Round 3 of Owner-requested changes * Round 4 (final for now) of Owner-requested changes * Hound * Updated package requirements * Lint * Collaborator-requested changes * Collaborator-requested changes * More small tweaks * One more small tweak * Bumping Travis and Coveralls --- .coveragerc | 2 +- CODEOWNERS | 3 +- .../components/binary_sensor/rainmachine.py | 102 ++++++++ homeassistant/components/rainmachine.py | 132 ---------- .../components/rainmachine/__init__.py | 226 ++++++++++++++++++ .../components/rainmachine/services.yaml | 32 +++ .../components/sensor/rainmachine.py | 88 +++++++ .../components/switch/rainmachine.py | 79 +++--- requirements_all.txt | 2 +- 9 files changed, 490 insertions(+), 176 deletions(-) create mode 100644 homeassistant/components/binary_sensor/rainmachine.py delete mode 100644 homeassistant/components/rainmachine.py create mode 100644 homeassistant/components/rainmachine/__init__.py create mode 100644 homeassistant/components/rainmachine/services.yaml create mode 100644 homeassistant/components/sensor/rainmachine.py diff --git a/.coveragerc b/.coveragerc index 3d1bbab8456..8d884dc53e6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -219,7 +219,7 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py - homeassistant/components/rainmachine.py + homeassistant/components/rainmachine/* homeassistant/components/*/rainmachine.py homeassistant/components/raspihats.py diff --git a/CODEOWNERS b/CODEOWNERS index 32639fed43c..0da8353e5aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,7 +78,6 @@ homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git -homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi @@ -100,6 +99,8 @@ homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza +homeassistant/components/rainmachine/* @bachya +homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py new file mode 100644 index 00000000000..601a73298af --- /dev/null +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -0,0 +1,102 @@ +""" +This platform provides binary sensors for key RainMachine data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rainmachine/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rainmachine import ( + BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE, + TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, + TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + binary_sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon = BINARY_SENSORS[sensor_type] + binary_sensors.append( + RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) + + add_devices(binary_sensors, True) + + +class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon): + """Initialize the sensor.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the state.""" + if self._sensor_type == TYPE_FREEZE: + self._state = self.rainmachine.restrictions['current']['freeze'] + elif self._sensor_type == TYPE_FREEZE_PROTECTION: + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectEnabled'] + elif self._sensor_type == TYPE_HOT_DAYS: + self._state = self.rainmachine.restrictions['global'][ + 'hotDaysExtraWatering'] + elif self._sensor_type == TYPE_HOURLY: + self._state = self.rainmachine.restrictions['current']['hourly'] + elif self._sensor_type == TYPE_MONTH: + self._state = self.rainmachine.restrictions['current']['month'] + elif self._sensor_type == TYPE_RAINDELAY: + self._state = self.rainmachine.restrictions['current']['rainDelay'] + elif self._sensor_type == TYPE_RAINSENSOR: + self._state = self.rainmachine.restrictions['current'][ + 'rainSensor'] + elif self._sensor_type == TYPE_WEEKDAY: + self._state = self.rainmachine.restrictions['current']['weekDay'] diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py deleted file mode 100644 index f2d5893d60b..00000000000 --- a/homeassistant/components/rainmachine.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -This component provides support for RainMachine sprinkler controllers. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/rainmachine/ -""" -import logging - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_SWITCHES) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['regenmaschine==0.4.1'] - -_LOGGER = logging.getLogger(__name__) - -DATA_RAINMACHINE = 'data_rainmachine' -DOMAIN = 'rainmachine' - -NOTIFICATION_ID = 'rainmachine_notification' -NOTIFICATION_TITLE = 'RainMachine Component Setup' - -CONF_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' -DEFAULT_ICON = 'mdi:water' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True - -PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) - -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_ZONE_RUN_TIME): - cv.positive_int -}) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema({ - vol.Required(CONF_IP_ADDRESS): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, - }) - }, - extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the RainMachine component.""" - from regenmaschine import Authenticator, Client - from regenmaschine.exceptions import HTTPError - from requests.exceptions import ConnectTimeout - - conf = config[DOMAIN] - ip_address = conf[CONF_IP_ADDRESS] - password = conf[CONF_PASSWORD] - port = conf[CONF_PORT] - ssl = conf[CONF_SSL] - - _LOGGER.debug('Setting up RainMachine client') - - try: - auth = Authenticator.create_local( - ip_address, password, port=port, https=ssl) - client = Client(auth) - hass.data[DATA_RAINMACHINE] = RainMachine(client) - except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: - _LOGGER.error('An error occurred: %s', str(exc_info)) - hass.components.persistent_notification.create( - 'Error: {0}
' - 'You will need to restart hass after fixing.' - ''.format(exc_info), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False - - _LOGGER.debug('Setting up switch platform') - switch_config = conf.get(CONF_SWITCHES, {}) - discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) - - _LOGGER.debug('Setup complete') - - return True - - -class RainMachine(object): - """Define a generic RainMachine object.""" - - def __init__(self, client): - """Initialize.""" - self.client = client - self.device_mac = self.client.provision.wifi()['macAddress'] - - -class RainMachineEntity(Entity): - """Define a generic RainMachine entity.""" - - def __init__(self, - rainmachine, - rainmachine_type, - rainmachine_entity_id, - icon=DEFAULT_ICON): - """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._icon = icon - self._rainmachine_type = rainmachine_type - self._rainmachine_entity_id = rainmachine_entity_id - self.rainmachine = rainmachine - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}_{2}'.format( - self.rainmachine.device_mac.replace( - ':', ''), self._rainmachine_type, - self._rainmachine_entity_id) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py new file mode 100644 index 00000000000..7ee6b063720 --- /dev/null +++ b/homeassistant/components/rainmachine/__init__.py @@ -0,0 +1,226 @@ +""" +Support for RainMachine devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/rainmachine/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, + CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['regenmaschine==0.4.2'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) + +CONF_PROGRAM_ID = 'program_id' +CONF_ZONE_ID = 'zone_id' +CONF_ZONE_RUN_TIME = 'zone_run_time' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' +DEFAULT_PORT = 8080 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) +DEFAULT_SSL = True +DEFAULT_ZONE_RUN = 60 * 10 + +TYPE_FREEZE = 'freeze' +TYPE_FREEZE_PROTECTION = 'freeze_protection' +TYPE_FREEZE_TEMP = 'freeze_protect_temp' +TYPE_HOT_DAYS = 'extra_water_on_hot_days' +TYPE_HOURLY = 'hourly' +TYPE_MONTH = 'month' +TYPE_RAINDELAY = 'raindelay' +TYPE_RAINSENSOR = 'rainsensor' +TYPE_WEEKDAY = 'weekday' + +BINARY_SENSORS = { + TYPE_FREEZE: ('Freeze Restrictions', 'mdi:cancel'), + TYPE_FREEZE_PROTECTION: ('Freeze Protection', 'mdi:weather-snowy'), + TYPE_HOT_DAYS: ('Extra Water on Hot Days', 'mdi:thermometer-lines'), + TYPE_HOURLY: ('Hourly Restrictions', 'mdi:cancel'), + TYPE_MONTH: ('Month Restrictions', 'mdi:cancel'), + TYPE_RAINDELAY: ('Rain Delay Restrictions', 'mdi:cancel'), + TYPE_RAINSENSOR: ('Rain Sensor Restrictions', 'mdi:cancel'), + TYPE_WEEKDAY: ('Weekday Restrictions', 'mdi:cancel'), +} + +SENSORS = { + TYPE_FREEZE_TEMP: ('Freeze Protect Temperature', 'mdi:thermometer', '°C'), +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +SERVICE_START_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_START_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN): + cv.positive_int, +}) + +SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_STOP_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, +}) + +SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int}) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: + vol.Schema({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import RainMachineError + + conf = config[DOMAIN] + ip_address = conf[CONF_IP_ADDRESS] + password = conf[CONF_PASSWORD] + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + + try: + auth = Authenticator.create_local( + ip_address, password, port=port, https=ssl) + rainmachine = RainMachine(hass, Client(auth)) + rainmachine.update() + hass.data[DATA_RAINMACHINE] = rainmachine + except RainMachineError as exc: + _LOGGER.error('An error occurred: %s', str(exc)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for component, schema in [ + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('sensor', conf[CONF_SENSORS]), + ('switch', conf[CONF_SWITCHES]), + ]: + discovery.load_platform(hass, component, DOMAIN, schema, config) + + def refresh(event_time): + """Refresh RainMachine data.""" + _LOGGER.debug('Updating RainMachine data') + hass.data[DATA_RAINMACHINE].update() + dispatcher_send(hass, DATA_UPDATE_TOPIC) + + track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL) + + def start_program(service): + """Start a particular program.""" + rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + + def start_zone(service): + """Start a particular zone for a certain amount of time.""" + rainmachine.client.zones.start(service.data[CONF_ZONE_ID], + service.data[CONF_ZONE_RUN_TIME]) + + def stop_all(service): + """Stop all watering.""" + rainmachine.client.watering.stop_all() + + def stop_program(service): + """Stop a program.""" + rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + + def stop_zone(service): + """Stop a zone.""" + rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + + for service, method, schema in [ + ('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA), + ('start_zone', start_zone, SERVICE_START_ZONE_SCHEMA), + ('stop_all', stop_all, {}), + ('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA), + ('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA) + ]: + hass.services.register(DOMAIN, service, method, schema=schema) + + return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, hass, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + self.restrictions = {} + + def update(self): + """Update sensor/binary sensor data.""" + self.restrictions.update({ + 'current': self.client.restrictions.current(), + 'global': self.client.restrictions.universal() + }) + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, rainmachine): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._name = None + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml new file mode 100644 index 00000000000..a8c77628c8f --- /dev/null +++ b/homeassistant/components/rainmachine/services.yaml @@ -0,0 +1,32 @@ +# Describes the format for available RainMachine services + +--- +start_program: + description: Start a program. + fields: + program_id: + description: The program to start. + example: 3 +start_zone: + description: Start a zone for a set number of seconds. + fields: + zone_id: + description: The zone to start. + example: 3 + zone_run_time: + description: The number of seconds to run the zone. + example: 120 +stop_all: + description: Stop all watering activities. +stop_program: + description: Stop a program. + fields: + program_id: + description: The program to stop. + example: 3 +stop_zone: + description: Stop a zone. + fields: + zone_id: + description: The zone to stop. + example: 3 diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py new file mode 100644 index 00000000000..8faf30acc38 --- /dev/null +++ b/homeassistant/components/sensor/rainmachine.py @@ -0,0 +1,88 @@ +""" +This platform provides support for sensor data from RainMachine. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rainmachine/ +""" +import logging + +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon, unit = SENSORS[sensor_type] + sensors.append( + RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) + + add_devices(sensors, True) + + +class RainMachineSensor(RainMachineEntity): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon, unit): + """Initialize.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + self._unit = unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def state(self) -> str: + """Return the name of the entity.""" + return self._state + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the sensor's state.""" + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectTemp'] diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index beb00eeca44..f4b2d780a9a 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -4,12 +4,11 @@ This component provides support for RainMachine programs and zones. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.rainmachine/ """ - -from logging import getLogger +import logging from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, - RainMachineEntity) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, + PROGRAM_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback @@ -18,7 +17,7 @@ from homeassistant.helpers.dispatcher import ( DEPENDENCIES = ['rainmachine'] -_LOGGER = getLogger(__name__) +_LOGGER = logging.getLogger(__name__) ATTR_AREA = 'area' ATTR_CS_ON = 'cs_on' @@ -39,8 +38,6 @@ ATTR_SUN_EXPOSURE = 'sun_exposure' ATTR_VEGETATION_TYPE = 'vegetation_type' ATTR_ZONES = 'zones' -DEFAULT_ZONE_RUN = 60 * 10 - DAYS = [ 'Monday', 'Tuesday', @@ -141,26 +138,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RainMachineSwitch(RainMachineEntity, SwitchDevice): - """A class to represent a generic RainMachine entity.""" + """A class to represent a generic RainMachine switch.""" - def __init__(self, rainmachine, rainmachine_type, obj): - """Initialize a generic RainMachine entity.""" + def __init__(self, rainmachine, switch_type, obj): + """Initialize a generic RainMachine switch.""" + super().__init__(rainmachine) + + self._name = obj['name'] self._obj = obj - self._type = rainmachine_type + self._rainmachine_entity_id = obj['uid'] + self._switch_type = switch_type - super().__init__(rainmachine, rainmachine_type, obj.get('uid')) + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" return self._obj.get('active') + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace(':', ''), + self._switch_type, + self._rainmachine_entity_id) + class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" def __init__(self, rainmachine, obj): - """Initialize.""" + """Initialize a generic RainMachine switch.""" super().__init__(rainmachine, 'program', obj) @property @@ -168,11 +180,6 @@ class RainMachineProgram(RainMachineSwitch): """Return whether the program is running.""" return bool(self._obj.get('status')) - @property - def name(self) -> str: - """Return the name of the program.""" - return 'Program: {0}'.format(self._obj.get('name')) - @property def zones(self) -> list: """Return a list of active zones associated with this program.""" @@ -180,29 +187,29 @@ class RainMachineProgram(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.stop(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn off program "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.start(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) def update(self) -> None: """Update info for the program.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.programs.get( @@ -210,16 +217,11 @@ class RainMachineProgram(RainMachineSwitch): self._attrs.update({ ATTR_ID: self._obj['uid'], - ATTR_CS_ON: self._obj.get('cs_on'), - ATTR_CYCLES: self._obj.get('cycles'), - ATTR_DELAY: self._obj.get('delay'), - ATTR_DELAY_ON: self._obj.get('delay_on'), ATTR_SOAK: self._obj.get('soak'), - ATTR_STATUS: - PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')], ATTR_ZONES: ', '.join(z['name'] for z in self.zones) }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) @@ -240,11 +242,6 @@ class RainMachineZone(RainMachineSwitch): """Return whether the zone is running.""" return bool(self._obj.get('state')) - @property - def name(self) -> str: - """Return the name of the zone.""" - return 'Zone: {0}'.format(self._obj.get('name')) - @callback def _program_updated(self): """Update state, trigger updates.""" @@ -257,28 +254,28 @@ class RainMachineZone(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.start(self._rainmachine_entity_id, self._run_time) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def update(self) -> None: """Update info for the zone.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.zones.get( @@ -309,7 +306,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_VEGETATION_TYPE: VEGETATION_MAP[self._obj.get('type')], }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) diff --git a/requirements_all.txt b/requirements_all.txt index 958b0f1027e..1b1db52daef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1147,7 +1147,7 @@ raincloudy==0.0.4 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==0.4.1 +regenmaschine==0.4.2 # homeassistant.components.python_script restrictedpython==4.0b4 From 7d2563eb1f899a908c88811abe9d84c6730db1b8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Tue, 29 May 2018 22:43:26 +0200 Subject: [PATCH 865/924] Update HAP-python to 2.2.2 (#14674) * Pass driver to accessory * Added 'hk_driver' fixture for tests --- homeassistant/components/homekit/__init__.py | 16 ++++---- .../components/homekit/accessories.py | 12 +++--- .../components/homekit/type_switches.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/conftest.py | 16 ++++++++ tests/components/homekit/test_accessories.py | 21 ++++++---- .../homekit/test_get_accessories.py | 23 ++++++----- tests/components/homekit/test_homekit.py | 41 +++++++++++-------- tests/components/homekit/test_type_covers.py | 16 ++++---- tests/components/homekit/test_type_fans.py | 12 +++--- tests/components/homekit/test_type_lights.py | 16 ++++---- tests/components/homekit/test_type_locks.py | 8 ++-- .../homekit/test_type_media_players.py | 4 +- .../homekit/test_type_security_systems.py | 10 +++-- tests/components/homekit/test_type_sensors.py | 31 +++++++------- .../components/homekit/test_type_switches.py | 4 +- .../homekit/test_type_thermostats.py | 16 ++++---- 18 files changed, 143 insertions(+), 109 deletions(-) create mode 100644 tests/components/homekit/conftest.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index a79fbf85400..ce3b79e6c72 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -30,7 +30,7 @@ from .util import ( TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.1.0'] +REQUIREMENTS = ['HAP-python==2.2.2'] # #### Driver Status #### STATUS_READY = 0 @@ -84,7 +84,7 @@ async def async_setup(hass, config): return True -def get_accessory(hass, state, aid, config): +def get_accessory(hass, driver, state, aid, config): """Take state and return an accessory object if supported.""" if not aid: _LOGGER.warning('The entitiy "%s" is not supported, since it ' @@ -157,7 +157,7 @@ def get_accessory(hass, state, aid, config): return None _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, name, state.entity_id, aid, config) + return TYPES[a_type](hass, driver, name, state.entity_id, aid, config) def generate_aid(entity_id): @@ -192,9 +192,9 @@ class HomeKit(): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) - self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.hass, self.bridge, port=self._port, - address=ip_addr, persist_file=path) + self.driver = HomeDriver(self.hass, address=ip_addr, + port=self._port, persist_file=path) + self.bridge = HomeBridge(self.hass, self.driver) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -202,7 +202,7 @@ class HomeKit(): return aid = generate_aid(state.entity_id) conf = self._config.pop(state.entity_id, {}) - acc = get_accessory(self.hass, state, aid, conf) + acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: self.bridge.add_accessory(acc) @@ -220,7 +220,7 @@ class HomeKit(): for state in self.hass.states.all(): self.add_bridge_accessory(state) - self.bridge.set_driver(self.driver) + self.driver.add_accessory(self.bridge) if not self.driver.state.paired: show_setup_message(self.hass, self.driver.state.pincode) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ded4526b008..711bf0030f0 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -64,10 +64,10 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, config, + def __init__(self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER): """Initialize a Accessory object.""" - super().__init__(name, aid=aid) + super().__init__(driver, name, aid=aid) model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, @@ -104,9 +104,9 @@ class HomeAccessory(Accessory): class HomeBridge(Bridge): """Adapter class for Bridge.""" - def __init__(self, hass, name=BRIDGE_NAME): + def __init__(self, hass, driver, name=BRIDGE_NAME): """Initialize a Bridge object.""" - super().__init__(name) + super().__init__(driver, name) self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, model=BRIDGE_MODEL, serial_number=BRIDGE_SERIAL_NUMBER) @@ -120,9 +120,9 @@ class HomeBridge(Bridge): class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, hass, *args, **kwargs): + def __init__(self, hass, **kwargs): """Initialize a AccessoryDriver object.""" - super().__init__(*args, **kwargs) + super().__init__(**kwargs) self.hass = hass def pair(self, client_uuid, client_public): diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 5754266587c..69f14821bd6 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -19,7 +19,7 @@ class Switch(HomeAccessory): """Generate a Switch accessory.""" def __init__(self, *args): - """Initialize a Switch accessory object to represent a remote.""" + """Initialize a Switch accessory object.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] self.flag_target_state = False diff --git a/requirements_all.txt b/requirements_all.txt index 1b1db52daef..3a190a29ed8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.1.0 +HAP-python==2.2.2 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adcba607db0..56b7eec6c5e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5 # homeassistant.components.homekit -HAP-python==2.1.0 +HAP-python==2.2.2 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py new file mode 100644 index 00000000000..f7839265939 --- /dev/null +++ b/tests/components/homekit/conftest.py @@ -0,0 +1,16 @@ +"""HomeKit session fixtures.""" +from unittest.mock import patch + +import pytest + +from pyhap.accessory_driver import AccessoryDriver + + +@pytest.fixture(scope='session') +def hk_driver(): + """Return a custom AccessoryDriver instance for HomeKit accessory init.""" + with patch('pyhap.accessory_driver.Zeroconf'), \ + patch('pyhap.accessory_driver.AccessoryEncoder'), \ + patch('pyhap.accessory_driver.HAPServer'), \ + patch('pyhap.accessory_driver.AccessoryDriver.publish'): + return AccessoryDriver(pincode=b'123-45-678') diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 3d1c335f8ae..a0764d58000 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -50,13 +50,14 @@ async def test_debounce(hass): assert counter == 2 -async def test_home_accessory(hass): +async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = 'homekit.accessory' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HomeAccessory(hass, 'Home Accessory', entity_id, 2, None) + acc = HomeAccessory(hass, hk_driver, 'Home Accessory', + entity_id, 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' assert acc.aid == 2 @@ -86,14 +87,15 @@ async def test_home_accessory(hass): acc.update_state('new_state') # Test model name from domain - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) + entity_id = 'test_model.demo' + acc = HomeAccessory('hass', hk_driver, 'test_name', entity_id, 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' -def test_home_bridge(): +def test_home_bridge(hk_driver): """Test HomeBridge class.""" - bridge = HomeBridge('hass') + bridge = HomeBridge('hass', hk_driver) assert bridge.hass == 'hass' assert bridge.display_name == BRIDGE_NAME assert bridge.category == 2 # Category.BRIDGE @@ -107,7 +109,7 @@ def test_home_bridge(): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ BRIDGE_SERIAL_NUMBER - bridge = HomeBridge('hass', 'test_name') + bridge = HomeBridge('hass', hk_driver, 'test_name') assert bridge.display_name == 'test_name' assert len(bridge.services) == 1 serv = bridge.services[0] # SERV_ACCESSORY_INFO @@ -118,7 +120,6 @@ def test_home_bridge(): def test_home_driver(): """Test HomeDriver class.""" - bridge = HomeBridge('hass') ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' @@ -126,9 +127,11 @@ def test_home_driver(): with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - driver = HomeDriver('hass', bridge, ip_address, port, path) + driver = HomeDriver('hass', address=ip_address, port=port, + persist_file=path) - mock_driver.assert_called_with(bridge, ip_address, port, path) + mock_driver.assert_called_with(address=ip_address, port=port, + persist_file=path) driver.state = Mock(pincode=pin) # pair diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 46e5f8b1174..3b7f307fce7 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -18,10 +18,12 @@ from homeassistant.const import ( def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" # not supported entity - assert get_accessory(None, State('demo.demo', 'on'), 2, {}) is None + assert get_accessory(None, None, State('demo.demo', 'on'), 2, {}) \ + is None # invalid aid - assert get_accessory(None, State('light.demo', 'on'), None, None) is None + assert get_accessory(None, None, State('light.demo', 'on'), None, None) \ + is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg @@ -31,11 +33,11 @@ def test_not_supported_media_player(): # selected mode for entity not supported config = {CONF_FEATURE_LIST: {FEATURE_ON_OFF: None}} entity_state = State('media_player.demo', 'on') - get_accessory(None, entity_state, 2, config) is None + get_accessory(None, None, entity_state, 2, config) is None # no supported modes for entity entity_state = State('media_player.demo', 'on') - assert get_accessory(None, entity_state, 2, {}) is None + assert get_accessory(None, None, entity_state, 2, {}) is None @pytest.mark.parametrize('config, name', [ @@ -46,8 +48,9 @@ def test_customize_options(config, name): mock_type = Mock() with patch.dict(TYPES, {'Light': mock_type}): entity_state = State('light.demo', 'on') - get_accessory(None, entity_state, 2, config) - mock_type.assert_called_with(None, name, 'light.demo', 2, config) + get_accessory(None, None, entity_state, 2, config) + mock_type.assert_called_with(None, None, name, + 'light.demo', 2, config) @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ @@ -70,7 +73,7 @@ def test_types(type_name, entity_id, state, attrs, config): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, config) + get_accessory(None, None, entity_state, 2, config) assert mock_type.called if config: @@ -91,7 +94,7 @@ def test_type_covers(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called @@ -122,7 +125,7 @@ def test_type_sensors(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called @@ -138,5 +141,5 @@ def test_type_switches(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, {}) assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 31337088b33..08e8da7857e 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -94,11 +94,12 @@ async def test_setup_auto_start_disabled(hass): assert homekit.start.called is False -async def test_homekit_setup(hass): +async def test_homekit_setup(hass, hk_driver): """Test setup of bridge and driver.""" homekit = HomeKit(hass, DEFAULT_PORT, None, {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ + with patch(PATH_HOMEKIT + '.accessories.HomeDriver', + return_value=hk_driver) as mock_driver, \ patch('homeassistant.util.get_local_ip') as mock_ip: mock_ip.return_value = IP_ADDRESS await hass.async_add_job(homekit.setup) @@ -106,41 +107,42 @@ async def test_homekit_setup(hass): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - hass, homekit.bridge, port=DEFAULT_PORT, - address=IP_ADDRESS, persist_file=path) + hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path) # Test if stop listener is setup assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 -async def test_homekit_setup_ip_address(hass): +async def test_homekit_setup_ip_address(hass, hk_driver): """Test setup with given IP address.""" homekit = HomeKit(hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + with patch(PATH_HOMEKIT + '.accessories.HomeDriver', + return_value=hk_driver) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( - hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) + hass, address='172.0.0.0', port=DEFAULT_PORT, persist_file=ANY) async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.driver = 'driver' homekit.bridge = mock_bridge = Mock() with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 363398124, {}) assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 294192020, {}) assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_get_acc.assert_called_with('hass', 'driver', ANY, 429982757, {}) mock_bridge.add_accessory.assert_called_with('acc') @@ -164,30 +166,35 @@ async def test_homekit_entity_filter(hass): assert mock_get_acc.called is False -async def test_homekit_start(hass, debounce_patcher): +async def test_homekit_start(hass, hk_driver, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = Mock() - homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = 'bridge' + homekit.driver = hk_driver hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] with patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') as \ mock_add_acc, \ - patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg: + patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg, \ + patch('pyhap.accessory_driver.AccessoryDriver.add_accessory') as \ + hk_driver_add_acc, \ + patch('pyhap.accessory_driver.AccessoryDriver.start') as \ + hk_driver_start: await hass.async_add_job(homekit.start) mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert mock_driver.start.called is True + hk_driver_add_acc.assert_called_with('bridge') + assert hk_driver_start.called assert homekit.status == STATUS_RUNNING # Test start() if already started - mock_driver.reset_mock() + hk_driver_start.reset_mock() await hass.async_add_job(homekit.start) - assert mock_driver.start.called is False + assert not hk_driver_start.called async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 8138d1c506b..c69ddacd328 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -28,13 +28,13 @@ def cls(): patcher.stop() -async def test_garage_door_open_close(hass, cls): +async def test_garage_door_open_close(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) + acc = cls.garage(hass, hk_driver, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -85,13 +85,13 @@ async def test_garage_door_open_close(hass, cls): assert acc.char_target_state.value == 0 -async def test_window_set_cover_position(hass, cls): +async def test_window_set_cover_position(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = cls.window(hass, 'Cover', entity_id, 2, None) + acc = cls.window(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -133,13 +133,13 @@ async def test_window_set_cover_position(hass, cls): assert acc.char_target_position.value == 75 -async def test_window_open_close(hass, cls): +async def test_window_open_close(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + acc = cls.window_basic(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -196,13 +196,13 @@ async def test_window_open_close(hass, cls): assert acc.char_position_state.value == 2 -async def test_window_open_close_stop(hass, cls): +async def test_window_open_close_stop(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) + acc = cls.window_basic(hass, hk_driver, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) # Set from HomeKit diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index f96fe19d603..ba7d4ccdcf0 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -27,14 +27,14 @@ def cls(): patcher.stop() -async def test_fan_basic(hass, cls): +async def test_fan_basic(hass, hk_driver, cls): """Test fan with char state.""" entity_id = 'fan.demo' hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 3 # Fan @@ -75,7 +75,7 @@ async def test_fan_basic(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_fan_direction(hass, cls): +async def test_fan_direction(hass, hk_driver, cls): """Test fan with direction.""" entity_id = 'fan.demo' @@ -83,7 +83,7 @@ async def test_fan_direction(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, ATTR_DIRECTION: DIRECTION_FORWARD}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.char_direction.value == 0 @@ -113,14 +113,14 @@ async def test_fan_direction(hass, cls): assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE -async def test_fan_oscillate(hass, cls): +async def test_fan_oscillate(hass, hk_driver, cls): """Test fan with oscillate.""" entity_id = 'fan.demo' hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) await hass.async_block_till_done() - acc = cls.fan(hass, 'Fan', entity_id, 2, None) + acc = cls.fan(hass, hk_driver, 'Fan', entity_id, 2, None) assert acc.char_swing.value == 0 diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 7a1db7b3f71..a9a5f1c3ece 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -26,14 +26,14 @@ def cls(): patcher.stop() -async def test_light_basic(hass, cls): +async def test_light_basic(hass, hk_driver, cls): """Test light with char state.""" entity_id = 'light.demo' hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 5 # Lightbulb @@ -74,14 +74,14 @@ async def test_light_basic(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_light_brightness(hass, cls): +async def test_light_brightness(hass, hk_driver, cls): """Test light with brightness.""" entity_id = 'light.demo' hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_brightness.value == 0 @@ -118,7 +118,7 @@ async def test_light_brightness(hass, cls): assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id -async def test_light_color_temperature(hass, cls): +async def test_light_color_temperature(hass, hk_driver, cls): """Test light with color temperature.""" entity_id = 'light.demo' @@ -126,7 +126,7 @@ async def test_light_color_temperature(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_color_temperature.value == 153 @@ -145,7 +145,7 @@ async def test_light_color_temperature(hass, cls): assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 -async def test_light_rgb_color(hass, cls): +async def test_light_rgb_color(hass, hk_driver, cls): """Test light with rgb_color.""" entity_id = 'light.demo' @@ -153,7 +153,7 @@ async def test_light_rgb_color(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, None) + acc = cls.light(hass, hk_driver, 'Light', entity_id, 2, None) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index f4698b1380b..8f18a591019 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -9,7 +9,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_lock_unlock(hass): +async def test_lock_unlock(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -17,7 +17,7 @@ async def test_lock_unlock(hass): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, config) + acc = Lock(hass, hk_driver, 'Lock', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -66,13 +66,13 @@ async def test_lock_unlock(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_code(hass, config): +async def test_no_code(hass, hk_driver, config): """Test accessory if lock doesn't require a code.""" entity_id = 'lock.kitchen_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, config) + acc = Lock(hass, hk_driver, 'Lock', entity_id, 2, config) # Set from HomeKit call_lock = async_mock_service(hass, DOMAIN, 'lock') diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index d89f9740ea6..4076b1f8a89 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -15,7 +15,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_media_player_set_state(hass): +async def test_media_player_set_state(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" config = {CONF_FEATURE_LIST: { FEATURE_ON_OFF: None, FEATURE_PLAY_PAUSE: None, @@ -25,7 +25,7 @@ async def test_media_player_set_state(hass): hass.states.async_set(entity_id, None, {ATTR_SUPPORTED_FEATURES: 20873, ATTR_MEDIA_VOLUME_MUTED: False}) await hass.async_block_till_done() - acc = MediaPlayer(hass, 'MediaPlayer', entity_id, 2, config) + acc = MediaPlayer(hass, hk_driver, 'MediaPlayer', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 7b72404cdaa..3ddce0f36eb 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from tests.common import async_mock_service -async def test_switch_set_state(hass): +async def test_switch_set_state(hass, hk_driver): """Test if accessory and HA are updated accordingly.""" code = '1234' config = {ATTR_CODE: code} @@ -20,7 +20,8 @@ async def test_switch_set_state(hass): hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + acc = SecuritySystem(hass, hk_driver, 'SecuritySystem', + entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -95,13 +96,14 @@ async def test_switch_set_state(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) -async def test_no_alarm_code(hass, config): +async def test_no_alarm_code(hass, hk_driver, config): """Test accessory if security_system doesn't require an alarm_code.""" entity_id = 'alarm_control_panel.test' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) + acc = SecuritySystem(hass, hk_driver, 'SecuritySystem', + entity_id, 2, config) # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index e36ae67da12..54ecbcb196f 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -8,13 +8,14 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -async def test_temperature(hass): +async def test_temperature(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) + acc = TemperatureSensor(hass, hk_driver, 'Temperature', + entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -40,13 +41,13 @@ async def test_temperature(hass): assert acc.char_temp.value == 24 -async def test_humidity(hass): +async def test_humidity(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) + acc = HumiditySensor(hass, hk_driver, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -63,13 +64,14 @@ async def test_humidity(hass): assert acc.char_humidity.value == 20 -async def test_air_quality(hass): +async def test_air_quality(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) + acc = AirQualitySensor(hass, hk_driver, 'Air Quality', + entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -94,13 +96,13 @@ async def test_air_quality(hass): assert acc.char_quality.value == 5 -async def test_co2(hass): +async def test_co2(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) + acc = CarbonDioxideSensor(hass, hk_driver, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -129,13 +131,13 @@ async def test_co2(hass): assert acc.char_detected.value == 0 -async def test_light(hass): +async def test_light(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = LightSensor(hass, 'Light', entity_id, 2, None) + acc = LightSensor(hass, hk_driver, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -152,7 +154,7 @@ async def test_light(hass): assert acc.char_light.value == 300 -async def test_binary(hass): +async def test_binary(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = 'binary_sensor.opening' @@ -160,7 +162,7 @@ async def test_binary(hass): {ATTR_DEVICE_CLASS: 'opening'}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Window Opening', entity_id, 2, None) + acc = BinarySensor(hass, hk_driver, 'Window Opening', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -193,7 +195,7 @@ async def test_binary(hass): assert acc.char_detected.value == 0 -async def test_binary_device_classes(hass): +async def test_binary_device_classes(hass, hk_driver): """Test if services and characteristics are assigned correctly.""" entity_id = 'binary_sensor.demo' @@ -202,6 +204,7 @@ async def test_binary_device_classes(hass): {ATTR_DEVICE_CLASS: device_class}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, None) + acc = BinarySensor(hass, hk_driver, 'Binary Sensor', + entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index ff94c4b6a0b..b1830d1926f 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -15,13 +15,13 @@ from tests.common import async_mock_service 'script.test', 'switch.test', ]) -async def test_switch_set_state(hass, entity_id): +async def test_switch_set_state(hass, hk_driver, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Switch(hass, 'Switch', entity_id, 2, None) + acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 337ad23ad05..6d6a48c7971 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -27,13 +27,13 @@ def cls(): patcher.stop() -async def test_default_thermostat(hass, cls): +async def test_default_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -166,14 +166,14 @@ async def test_default_thermostat(hass, cls): assert acc.char_target_heat_cool.value == 1 -async def test_auto_thermostat(hass, cls): +async def test_auto_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.char_cooling_thresh_temp.value == 23.0 @@ -241,7 +241,7 @@ async def test_auto_thermostat(hass, cls): assert acc.char_cooling_thresh_temp.value == 25.0 -async def test_power_state(hass, cls): +async def test_power_state(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' @@ -252,7 +252,7 @@ async def test_power_state(hass, cls): ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.support_power_state is True @@ -297,14 +297,14 @@ async def test_power_state(hass, cls): assert acc.char_target_heat_cool.value == 0 -async def test_thermostat_fahrenheit(hass, cls): +async def test_thermostat_fahrenheit(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) hass.states.async_set(entity_id, STATE_AUTO, From e746b92e0e7111043746bbd45ac4312cc457361b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 May 2018 17:14:58 -0400 Subject: [PATCH 866/924] Fix deprecated code (#14681) --- homeassistant/components/cloud/iot.py | 2 +- tests/components/cloud/test_iot.py | 2 +- tests/components/test_conversation.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cloud/iot.py b/homeassistant/components/cloud/iot.py index 7cf8e50e866..12b81c9003b 100644 --- a/homeassistant/components/cloud/iot.py +++ b/homeassistant/components/cloud/iot.py @@ -185,7 +185,7 @@ class CloudIoT: yield from client.send_json(response) except client_exceptions.WSServerHandshakeError as err: - if err.code == 401: + if err.status == 401: disconnect_warn = 'Invalid auth.' self.close_requested = True # Should we notify user? diff --git a/tests/components/cloud/test_iot.py b/tests/components/cloud/test_iot.py index 81b1e315085..1b580d0eb9b 100644 --- a/tests/components/cloud/test_iot.py +++ b/tests/components/cloud/test_iot.py @@ -210,7 +210,7 @@ def test_cloud_connect_invalid_auth(mock_client, caplog, mock_cloud): """Test invalid auth detected by server.""" conn = iot.CloudIoT(mock_cloud) mock_client.receive.side_effect = \ - client_exceptions.WSServerHandshakeError(None, None, code=401) + client_exceptions.WSServerHandshakeError(None, None, status=401) yield from conn.connect() diff --git a/tests/components/test_conversation.py b/tests/components/test_conversation.py index d9c29cdae83..6a1d5a55c47 100644 --- a/tests/components/test_conversation.py +++ b/tests/components/test_conversation.py @@ -89,7 +89,7 @@ async def test_register_before_setup(hass): assert intent.text_input == 'I would like the Grolsch beer' -async def test_http_processing_intent(hass, test_client): +async def test_http_processing_intent(hass, aiohttp_client): """Test processing intent via HTTP API.""" class TestIntentHandler(intent.IntentHandler): """Test Intent Handler.""" @@ -119,7 +119,7 @@ async def test_http_processing_intent(hass, test_client): }) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.post('/api/conversation/process', json={ 'text': 'I would like the Grolsch beer' }) @@ -243,7 +243,7 @@ async def test_toggle_intent(hass, sentence): assert call.data == {'entity_id': 'light.kitchen'} -async def test_http_api(hass, test_client): +async def test_http_api(hass, aiohttp_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -251,7 +251,7 @@ async def test_http_api(hass, test_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) hass.states.async_set('light.kitchen', 'off') calls = async_mock_service(hass, 'homeassistant', 'turn_on') @@ -267,7 +267,7 @@ async def test_http_api(hass, test_client): assert call.data == {'entity_id': 'light.kitchen'} -async def test_http_api_wrong_data(hass, test_client): +async def test_http_api_wrong_data(hass, aiohttp_client): """Test the HTTP conversation API.""" result = await component.async_setup(hass, {}) assert result @@ -275,7 +275,7 @@ async def test_http_api_wrong_data(hass, test_client): result = await async_setup_component(hass, 'conversation', {}) assert result - client = await test_client(hass.http.app) + client = await aiohttp_client(hass.http.app) resp = await client.post('/api/conversation/process', json={ 'text': 123 From f1f4d80f24c9b3d4d8dbb775dc7b5427b404916f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Wed, 30 May 2018 12:39:27 +0200 Subject: [PATCH 867/924] Homekit Bugfixes (#14689) * Fix async bug * Fix debounce bug --- .../components/homekit/accessories.py | 52 ++++++++----------- tests/components/homekit/test_accessories.py | 3 +- .../homekit/test_type_thermostats.py | 4 ++ 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 711bf0030f0..1b0d5ce1be4 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -1,6 +1,6 @@ """Extend the basic Accessory and Bridge functions.""" from datetime import timedelta -from functools import wraps +from functools import partial, wraps from inspect import getmodule import logging @@ -27,35 +27,25 @@ _LOGGER = logging.getLogger(__name__) def debounce(func): """Decorator function. Debounce callbacks form HomeKit.""" @ha_callback - def call_later_listener(*args): + def call_later_listener(self, *args): """Callback listener called from call_later.""" - # pylint: disable=unsubscriptable-object - nonlocal lastargs, remove_listener - hass = lastargs['hass'] - hass.async_add_job(func, *lastargs['args']) - lastargs = remove_listener = None + debounce_params = self.debounce.pop(func.__name__, None) + if debounce_params: + self.hass.async_add_job(func, self, *debounce_params[1:]) @wraps(func) - def wrapper(*args): - """Wrapper starts async timer. - - The accessory must have 'self.hass' and 'self.entity_id' as attributes. - """ - # pylint: disable=not-callable - hass = args[0].hass - nonlocal lastargs, remove_listener - if remove_listener: - remove_listener() - lastargs = remove_listener = None - lastargs = {'hass': hass, 'args': [*args]} + def wrapper(self, *args): + """Wrapper starts async timer.""" + debounce_params = self.debounce.pop(func.__name__, None) + if debounce_params: + debounce_params[0]() # remove listener remove_listener = track_point_in_utc_time( - hass, call_later_listener, + self.hass, partial(call_later_listener, self), dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT)) - logger.debug('%s: Start %s timeout', args[0].entity_id, + self.debounce[func.__name__] = (remove_listener, *args) + logger.debug('%s: Start %s timeout', self.entity_id, func.__name__.replace('set_', '')) - remove_listener = None - lastargs = None name = getmodule(func).__name__ logger = logging.getLogger(name) return wrapper @@ -76,11 +66,15 @@ class HomeAccessory(Accessory): self.config = config self.entity_id = entity_id self.hass = hass + self.debounce = {} - def run(self): - """Method called by accessory after driver is started.""" + async def run(self): + """Method called by accessory after driver is started. + + Run inside the HAP-python event loop. + """ state = self.hass.states.get(self.entity_id) - self.update_state_callback(new_state=state) + self.hass.add_job(self.update_state_callback, None, None, state) async_track_state_change( self.hass, self.entity_id, self.update_state_callback) @@ -127,10 +121,10 @@ class HomeDriver(AccessoryDriver): def pair(self, client_uuid, client_public): """Override super function to dismiss setup message if paired.""" - value = super().pair(client_uuid, client_public) - if value: + success = super().pair(client_uuid, client_public) + if success: dismiss_setup_message(self.hass) - return value + return success def unpair(self, client_uuid): """Override super function to show setup message if unpaired.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index a0764d58000..711c38443f2 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -26,7 +26,7 @@ async def test_debounce(hass): arguments = None counter = 0 - mock = Mock(hass=hass) + mock = Mock(hass=hass, debounce={}) debounce_demo = debounce(demo_func) assert debounce_demo.__name__ == 'demo_func' @@ -76,6 +76,7 @@ async def test_home_accessory(hass, hk_driver): with patch('homeassistant.components.homekit.accessories.' 'HomeAccessory.update_state') as mock_update_state: await hass.async_add_job(acc.run) + await hass.async_block_till_done() state = hass.states.get(entity_id) mock_update_state.assert_called_with(state) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 6d6a48c7971..1f6554496a9 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -35,6 +35,7 @@ async def test_default_thermostat(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 9 # Thermostat @@ -175,6 +176,7 @@ async def test_auto_thermostat(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_heating_thresh_temp.value == 19.0 @@ -254,6 +256,7 @@ async def test_power_state(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.support_power_state is True assert acc.char_current_heat_cool.value == 1 @@ -306,6 +309,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, From c14e41f4310a010633bc0eaad22a2a07e05adbdb Mon Sep 17 00:00:00 2001 From: Michael Nosthoff Date: Wed, 30 May 2018 16:53:35 +0200 Subject: [PATCH 868/924] Netatmo Sensor: Implement device_class (#14634) added device_class and removed icon for temperature and humidity. --- homeassistant/components/sensor/netatmo.py | 60 +++++++++++++--------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 4aeba082e55..f09e1d4f395 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -10,7 +10,9 @@ from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, STATE_UNKNOWN +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + STATE_UNKNOWN) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -26,28 +28,29 @@ DEPENDENCIES = ['netatmo'] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=600) SENSOR_TYPES = { - 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], - 'co2': ['CO2', 'ppm', 'mdi:cloud'], - 'pressure': ['Pressure', 'mbar', 'mdi:gauge'], - 'noise': ['Noise', 'dB', 'mdi:volume-high'], - 'humidity': ['Humidity', '%', 'mdi:water-percent'], - 'rain': ['Rain', 'mm', 'mdi:weather-rainy'], - 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy'], - 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy'], - 'battery_vp': ['Battery', '', 'mdi:battery'], - 'battery_lvl': ['Battery_lvl', '', 'mdi:battery'], - 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer'], - 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer'], - 'windangle': ['Angle', '', 'mdi:compass'], - 'windangle_value': ['Angle Value', 'º', 'mdi:compass'], - 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy'], - 'gustangle': ['Gust Angle', '', 'mdi:compass'], - 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass'], - 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy'], - 'rf_status': ['Radio', '', 'mdi:signal'], - 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal'], - 'wifi_status': ['Wifi', '', 'mdi:wifi'], - 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi'] + 'temperature': ['Temperature', TEMP_CELSIUS, None, + DEVICE_CLASS_TEMPERATURE], + 'co2': ['CO2', 'ppm', 'mdi:cloud', None], + 'pressure': ['Pressure', 'mbar', 'mdi:gauge', None], + 'noise': ['Noise', 'dB', 'mdi:volume-high', None], + 'humidity': ['Humidity', '%', None, DEVICE_CLASS_HUMIDITY], + 'rain': ['Rain', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_1': ['sum_rain_1', 'mm', 'mdi:weather-rainy', None], + 'sum_rain_24': ['sum_rain_24', 'mm', 'mdi:weather-rainy', None], + 'battery_vp': ['Battery', '', 'mdi:battery', None], + 'battery_lvl': ['Battery_lvl', '', 'mdi:battery', None], + 'min_temp': ['Min Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'max_temp': ['Max Temp.', TEMP_CELSIUS, 'mdi:thermometer', None], + 'windangle': ['Angle', '', 'mdi:compass', None], + 'windangle_value': ['Angle Value', 'º', 'mdi:compass', None], + 'windstrength': ['Strength', 'km/h', 'mdi:weather-windy', None], + 'gustangle': ['Gust Angle', '', 'mdi:compass', None], + 'gustangle_value': ['Gust Angle Value', 'º', 'mdi:compass', None], + 'guststrength': ['Gust Strength', 'km/h', 'mdi:weather-windy', None], + 'rf_status': ['Radio', '', 'mdi:signal', None], + 'rf_status_lvl': ['Radio_lvl', '', 'mdi:signal', None], + 'wifi_status': ['Wifi', '', 'mdi:wifi', None], + 'wifi_status_lvl': ['Wifi_lvl', 'dBm', 'mdi:wifi', None] } MODULE_SCHEMA = vol.Schema({ @@ -106,7 +109,9 @@ class NetAtmoSensor(Entity): self.module_name = module_name self.type = sensor_type self._state = None - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._device_class = SENSOR_TYPES[self.type][3] + self._icon = SENSOR_TYPES[self.type][2] + self._unit_of_measurement = SENSOR_TYPES[self.type][1] module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] self.module_id = module_id[1] @@ -119,7 +124,12 @@ class NetAtmoSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class @property def state(self): From 08fc73aa202f99c55025205b827efe765d0c9306 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 30 May 2018 11:19:27 -0400 Subject: [PATCH 869/924] Bump to 0.71.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acc30bcd57c..4c9757b3260 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 70 +MINOR_VERSION = 71 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 7094d6d61e5d04b3b716ff7538561b2bc1bb9a06 Mon Sep 17 00:00:00 2001 From: c727 Date: Thu, 31 May 2018 14:31:40 +0200 Subject: [PATCH 870/924] Change ACP code_format to None|"Number"|"Any" (#14686) --- homeassistant/components/alarm_control_panel/alarmdecoder.py | 2 +- homeassistant/components/alarm_control_panel/alarmdotcom.py | 4 ++-- homeassistant/components/alarm_control_panel/concord232.py | 2 +- homeassistant/components/alarm_control_panel/envisalink.py | 2 +- homeassistant/components/alarm_control_panel/ifttt.py | 4 ++-- homeassistant/components/alarm_control_panel/manual.py | 4 ++-- homeassistant/components/alarm_control_panel/manual_mqtt.py | 4 ++-- homeassistant/components/alarm_control_panel/mqtt.py | 4 ++-- homeassistant/components/alarm_control_panel/nx584.py | 2 +- homeassistant/components/alarm_control_panel/satel_integra.py | 2 +- homeassistant/components/alarm_control_panel/simplisafe.py | 4 ++-- homeassistant/components/alarm_control_panel/verisure.py | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/alarmdecoder.py b/homeassistant/components/alarm_control_panel/alarmdecoder.py index 13b51aea701..626022e362a 100644 --- a/homeassistant/components/alarm_control_panel/alarmdecoder.py +++ b/homeassistant/components/alarm_control_panel/alarmdecoder.py @@ -101,7 +101,7 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/alarmdotcom.py b/homeassistant/components/alarm_control_panel/alarmdotcom.py index 6b523e8b606..87e85f09da0 100644 --- a/homeassistant/components/alarm_control_panel/alarmdotcom.py +++ b/homeassistant/components/alarm_control_panel/alarmdotcom.py @@ -84,8 +84,8 @@ class AlarmDotCom(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/concord232.py b/homeassistant/components/alarm_control_panel/concord232.py index bd3ee762ccb..9a65fdaff06 100644 --- a/homeassistant/components/alarm_control_panel/concord232.py +++ b/homeassistant/components/alarm_control_panel/concord232.py @@ -80,7 +80,7 @@ class Concord232Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return the characters if code is defined.""" - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/envisalink.py b/homeassistant/components/alarm_control_panel/envisalink.py index 19bbfa611f2..25224484c79 100644 --- a/homeassistant/components/alarm_control_panel/envisalink.py +++ b/homeassistant/components/alarm_control_panel/envisalink.py @@ -106,7 +106,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Regex for code format or None if no code is required.""" if self._code: return None - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/ifttt.py b/homeassistant/components/alarm_control_panel/ifttt.py index 203044f3915..209c5367c92 100644 --- a/homeassistant/components/alarm_control_panel/ifttt.py +++ b/homeassistant/components/alarm_control_panel/ifttt.py @@ -129,8 +129,8 @@ class IFTTTAlarmPanel(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index e66251143da..2f2f89b9dfc 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -206,8 +206,8 @@ class ManualAlarm(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/manual_mqtt.py b/homeassistant/components/alarm_control_panel/manual_mqtt.py index c09105c91e0..895f5edd5da 100644 --- a/homeassistant/components/alarm_control_panel/manual_mqtt.py +++ b/homeassistant/components/alarm_control_panel/manual_mqtt.py @@ -242,8 +242,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' def alarm_disarm(self, code=None): """Send disarm command.""" diff --git a/homeassistant/components/alarm_control_panel/mqtt.py b/homeassistant/components/alarm_control_panel/mqtt.py index 0298c7384a2..8a0dfefdc70 100644 --- a/homeassistant/components/alarm_control_panel/mqtt.py +++ b/homeassistant/components/alarm_control_panel/mqtt.py @@ -122,8 +122,8 @@ class MqttAlarm(MqttAvailability, alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' @asyncio.coroutine def async_alarm_disarm(self, code=None): diff --git a/homeassistant/components/alarm_control_panel/nx584.py b/homeassistant/components/alarm_control_panel/nx584.py index 67d3725fc38..ca6f1a44a6f 100644 --- a/homeassistant/components/alarm_control_panel/nx584.py +++ b/homeassistant/components/alarm_control_panel/nx584.py @@ -70,7 +70,7 @@ class NX584Alarm(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return '^\\d+$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/satel_integra.py b/homeassistant/components/alarm_control_panel/satel_integra.py index 964047f91e9..4ac3a93fff4 100644 --- a/homeassistant/components/alarm_control_panel/satel_integra.py +++ b/homeassistant/components/alarm_control_panel/satel_integra.py @@ -66,7 +66,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanel): @property def code_format(self): """Return the regex for code format or None if no code is required.""" - return '^\\d{4,6}$' + return 'Number' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/simplisafe.py b/homeassistant/components/alarm_control_panel/simplisafe.py index c08ac3c0ea0..b4906acba3c 100644 --- a/homeassistant/components/alarm_control_panel/simplisafe.py +++ b/homeassistant/components/alarm_control_panel/simplisafe.py @@ -88,8 +88,8 @@ class SimpliSafeAlarm(alarm.AlarmControlPanel): if self._code is None: return None elif isinstance(self._code, str) and re.search('^\\d+$', self._code): - return '^\\d+$' - return '.+' + return 'Number' + return 'Any' @property def state(self): diff --git a/homeassistant/components/alarm_control_panel/verisure.py b/homeassistant/components/alarm_control_panel/verisure.py index 6651334400f..59bfe15fa9b 100644 --- a/homeassistant/components/alarm_control_panel/verisure.py +++ b/homeassistant/components/alarm_control_panel/verisure.py @@ -61,7 +61,7 @@ class VerisureAlarm(alarm.AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - return '^\\d+$' + return 'Number' @property def changed_by(self): From 60f692c7bbd7282678a10ee6e9c4f02e2e174002 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 31 May 2018 10:55:50 -0600 Subject: [PATCH 871/924] Fixes (and stabilizes) some incorrect zone codes in RainMachine (#14719) * Fixes (and stabilizes) some incorrect zone codes * Fixed a misspelling --- .../components/switch/rainmachine.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index f4b2d780a9a..bdee64a3d54 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -83,7 +83,7 @@ SPRINKLER_TYPE_MAP = { 1: 'Popup Spray', 2: 'Rotors', 3: 'Surface Drip', - 4: 'Bubblers', + 4: 'Bubblers Drip', 99: 'Other' } @@ -96,14 +96,14 @@ SUN_EXPOSURE_MAP = { VEGETATION_MAP = { 0: 'Not Set', - 1: 'Not Set', - 2: 'Grass', + 2: 'Cool Season Grass', 3: 'Fruit Trees', 4: 'Flowers', 5: 'Vegetables', 6: 'Citrus', - 7: 'Bushes', - 8: 'Xeriscape', + 7: 'Trees and Bushes', + 9: 'Drought Tolerant Plants', + 10: 'Warm Season Grass', 99: 'Other' } @@ -296,15 +296,17 @@ class RainMachineZone(RainMachineSwitch): self._properties_json.get( 'waterSense').get('precipitationRate'), ATTR_RESTRICTIONS: self._obj.get('restriction'), - ATTR_SLOPE: SLOPE_TYPE_MAP[self._properties_json.get('slope')], + ATTR_SLOPE: SLOPE_TYPE_MAP.get( + self._properties_json.get('slope')), ATTR_SOIL_TYPE: - SOIL_TYPE_MAP[self._properties_json.get('sun')], + SOIL_TYPE_MAP.get(self._properties_json.get('sun')), ATTR_SPRINKLER_TYPE: - SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')], + SPRINKLER_TYPE_MAP.get( + self._properties_json.get('group_id')), ATTR_SUN_EXPOSURE: - SUN_EXPOSURE_MAP[self._properties_json.get('sun')], + SUN_EXPOSURE_MAP.get(self._properties_json.get('sun')), ATTR_VEGETATION_TYPE: - VEGETATION_MAP[self._obj.get('type')], + VEGETATION_MAP.get(self._obj.get('type')), }) except RainMachineError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', From dae90abb34586c989ca5ce0610226ec04e84965c Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 31 May 2018 10:34:07 -0600 Subject: [PATCH 872/924] Change climate default limits to constants Min and max temp and humidity are now defined in climate __init__.py and are available for import in subclasses. --- homeassistant/components/climate/__init__.py | 14 ++++++++++---- .../components/climate/generic_thermostat.py | 7 ++++--- homeassistant/components/climate/sensibo.py | 6 +++--- homeassistant/components/climate/tado.py | 7 ++++--- tests/components/climate/test_mqtt.py | 6 +++--- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 550d4035ddd..7f5ef4c4e80 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -22,6 +22,12 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN, TEMP_CELSIUS, PRECISION_WHOLE, PRECISION_TENTHS, ) + +DEFAULT_MIN_TEMP = 7 +DEFAULT_MAX_TEMP = 35 +DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MAX_HUMIDITY = 99 + DOMAIN = 'climate' ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -778,19 +784,19 @@ class ClimateDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(7, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(35, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit) @property def min_humidity(self): """Return the minimum humidity.""" - return 30 + return DEFAULT_MIN_HUMITIDY @property def max_humidity(self): """Return the maximum humidity.""" - return 99 + return DEFAULT_MAX_HUMIDITY diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index b5d3c3f7c25..ce8217aa92c 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -14,7 +14,8 @@ from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.components.climate import ( STATE_HEAT, STATE_COOL, STATE_IDLE, STATE_AUTO, ClimateDevice, ATTR_OPERATION_MODE, ATTR_AWAY_MODE, SUPPORT_OPERATION_MODE, - SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA) + SUPPORT_AWAY_MODE, SUPPORT_TARGET_TEMPERATURE, PLATFORM_SCHEMA, + DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_OFF, ATTR_TEMPERATURE, CONF_NAME, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, @@ -268,7 +269,7 @@ class GenericThermostat(ClimateDevice): return self._min_temp # get default temp from super class - return ClimateDevice.min_temp.fget(self) + return DEFAULT_MIN_TEMP @property def max_temp(self): @@ -278,7 +279,7 @@ class GenericThermostat(ClimateDevice): return self._max_temp # Get default temp from super class - return ClimateDevice.max_temp.fget(self) + return DEFAULT_MAX_TEMP @asyncio.coroutine def _async_sensor_changed(self, entity_id, old_state, new_state): diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 2b92d050d3b..94d9612755c 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -19,7 +19,7 @@ from homeassistant.components.climate import ( ATTR_CURRENT_HUMIDITY, ClimateDevice, DOMAIN, PLATFORM_SCHEMA, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, - SUPPORT_ON_OFF) + SUPPORT_ON_OFF, DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -240,13 +240,13 @@ class SensiboClimate(ClimateDevice): def min_temp(self): """Return the minimum temperature.""" return self._temperatures_list[0] \ - if self._temperatures_list else super().min_temp + if self._temperatures_list else DEFAULT_MIN_TEMP @property def max_temp(self): """Return the maximum temperature.""" return self._temperatures_list[-1] \ - if self._temperatures_list else super().max_temp + if self._temperatures_list else DEFAULT_MAX_TEMP @property def unique_id(self): diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index 437c8ec3371..c3004a0407f 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -8,7 +8,8 @@ import logging from homeassistant.const import (PRECISION_TENTHS, TEMP_CELSIUS) from homeassistant.components.climate import ( - ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE) + ClimateDevice, SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + DEFAULT_MIN_TEMP, DEFAULT_MAX_TEMP) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.components.tado import DATA_TADO @@ -233,7 +234,7 @@ class TadoClimate(ClimateDevice): if self._min_temp: return self._min_temp # get default temp from super class - return super().min_temp + return DEFAULT_MIN_TEMP @property def max_temp(self): @@ -241,7 +242,7 @@ class TadoClimate(ClimateDevice): if self._max_temp: return self._max_temp # Get default temp from super class - return super().max_temp + return DEFAULT_MAX_TEMP def update(self): """Update the state of this climate device.""" diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 663393503ac..677d1b944d0 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -9,9 +9,9 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, + SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) from tests.common import (get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, mock_component) From cc264f415e746173aff8cbb182b8ddc81ddebb8d Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 31 May 2018 11:32:31 -0600 Subject: [PATCH 873/924] Fix PEP-8 issues --- homeassistant/components/climate/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 7f5ef4c4e80..ebe7cbbf2c1 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -784,12 +784,14 @@ class ClimateDevice(Entity): @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MIN_TEMP, TEMP_CELSIUS, + self.temperature_unit) @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, self.temperature_unit) + return convert_temperature(DEFAULT_MAX_TEMP, TEMP_CELSIUS, + self.temperature_unit) @property def min_humidity(self): From 753fe8279ba019d376eeb1caafcd8640b4d29f98 Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Thu, 31 May 2018 13:59:26 -0600 Subject: [PATCH 874/924] Remove deprecated comments --- homeassistant/components/climate/generic_thermostat.py | 2 -- homeassistant/components/climate/tado.py | 4 ++-- tests/components/climate/test_mqtt.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climate/generic_thermostat.py b/homeassistant/components/climate/generic_thermostat.py index ce8217aa92c..6b7f6cb2afc 100644 --- a/homeassistant/components/climate/generic_thermostat.py +++ b/homeassistant/components/climate/generic_thermostat.py @@ -268,7 +268,6 @@ class GenericThermostat(ClimateDevice): if self._min_temp: return self._min_temp - # get default temp from super class return DEFAULT_MIN_TEMP @property @@ -278,7 +277,6 @@ class GenericThermostat(ClimateDevice): if self._max_temp: return self._max_temp - # Get default temp from super class return DEFAULT_MAX_TEMP @asyncio.coroutine diff --git a/homeassistant/components/climate/tado.py b/homeassistant/components/climate/tado.py index c3004a0407f..59da425553a 100644 --- a/homeassistant/components/climate/tado.py +++ b/homeassistant/components/climate/tado.py @@ -233,7 +233,7 @@ class TadoClimate(ClimateDevice): """Return the minimum temperature.""" if self._min_temp: return self._min_temp - # get default temp from super class + return DEFAULT_MIN_TEMP @property @@ -241,7 +241,7 @@ class TadoClimate(ClimateDevice): """Return the maximum temperature.""" if self._max_temp: return self._max_temp - # Get default temp from super class + return DEFAULT_MAX_TEMP def update(self): diff --git a/tests/components/climate/test_mqtt.py b/tests/components/climate/test_mqtt.py index 677d1b944d0..663393503ac 100644 --- a/tests/components/climate/test_mqtt.py +++ b/tests/components/climate/test_mqtt.py @@ -9,9 +9,9 @@ from homeassistant.setup import setup_component from homeassistant.components import climate from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.climate import ( - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, - SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_HOLD_MODE, + SUPPORT_AWAY_MODE, SUPPORT_AUX_HEAT) from tests.common import (get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, mock_component) From 14ee6178f9e3c3d40f243edc27c2c0dc4376f4a3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 31 May 2018 23:07:50 +0200 Subject: [PATCH 875/924] Add Flock notification platform (#14533) * Add Flock notification platform * Use async syntax and move session and loop --- .coveragerc | 3 +- homeassistant/components/notify/flock.py | 61 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/notify/flock.py diff --git a/.coveragerc b/.coveragerc index 8d884dc53e6..26744ad6952 100644 --- a/.coveragerc +++ b/.coveragerc @@ -523,9 +523,10 @@ omit = homeassistant/components/notify/aws_sqs.py homeassistant/components/notify/ciscospark.py homeassistant/components/notify/clickatell.py - homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/clicksend.py + homeassistant/components/notify/clicksend_tts.py homeassistant/components/notify/discord.py + homeassistant/components/notify/flock.py homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py homeassistant/components/notify/group.py diff --git a/homeassistant/components/notify/flock.py b/homeassistant/components/notify/flock.py new file mode 100644 index 00000000000..d26f629809f --- /dev/null +++ b/homeassistant/components/notify/flock.py @@ -0,0 +1,61 @@ +""" +Flock platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.flock/ +""" +import asyncio +import logging + +import async_timeout +import voluptuous as vol + +from homeassistant.components.notify import ( + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) +_RESOURCE = 'https://api.flock.com/hooks/sendMessage/' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, +}) + + +async def get_service(hass, config, discovery_info=None): + """Get the Flock notification service.""" + access_token = config.get(CONF_ACCESS_TOKEN) + url = '{}{}'.format(_RESOURCE, access_token) + session = async_get_clientsession(hass) + + return FlockNotificationService(url, session, hass.loop) + + +class FlockNotificationService(BaseNotificationService): + """Implement the notification service for Flock.""" + + def __init__(self, url, session, loop): + """Initialize the Flock notification service.""" + self._loop = loop + self._url = url + self._session = session + + async def async_send_message(self, message, **kwargs): + """Send the message to the user.""" + payload = {'text': message} + + _LOGGER.debug("Attempting to call Flock at %s", self._url) + + try: + with async_timeout.timeout(10, loop=self._loop): + response = await self._session.post(self._url, json=payload) + result = await response.json() + + if response.status != 200 or 'error' in result: + _LOGGER.error( + "Flock service returned HTTP status %d, response %s", + response.status, result) + except asyncio.TimeoutError: + _LOGGER.error("Timeout accessing Flock at %s", self._url) From a58a566ae842a2b68c045ad60411a8be5085c673 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:25:35 -0400 Subject: [PATCH 876/924] Bump frontend to 20180531.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2bd7283e38e..5ebf6e8762f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.4'] +REQUIREMENTS = ['home-assistant-frontend==20180531.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 3a190a29ed8..6354e9c49c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.4 +home-assistant-frontend==20180531.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56b7eec6c5e..00b3d1f82e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.4 +home-assistant-frontend==20180531.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f32b50cb809ad91f09eae78f75d5db9e4e711b53 Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 1 Jun 2018 00:26:59 +0300 Subject: [PATCH 877/924] Fix Eco mode display on Nest (#14706) * Fix Eco mode display on Nest * Fix Hound problems --- homeassistant/components/climate/nest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 0a5344fdf98..28e8020ab90 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -134,7 +134,9 @@ class NestThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: + if self._mode != NEST_MODE_HEAT_COOL and \ + self._mode != STATE_ECO and \ + not self.is_away_mode_on: return self._target_temperature return None From 0eddd287c5e68cc3ed2d27245a0d54f76b3a1544 Mon Sep 17 00:00:00 2001 From: MizterB <5458030+MizterB@users.noreply.github.com> Date: Mon, 28 May 2018 10:21:00 -0400 Subject: [PATCH 878/924] Update Hue platform to aiohue 1.5.0, and re-implement logic for duplicate scene names. (#14653) --- homeassistant/components/hue/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 18 +++++++++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 0aed854d4e4..251d8cba095 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -17,7 +17,7 @@ from .bridge import HueBridge # Loading the config flow file will register the flow from .config_flow import configured_hosts -REQUIREMENTS = ['aiohue==1.3.0'] +REQUIREMENTS = ['aiohue==1.5.0'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5ff5e2dbf6f..d7a8dc7f730 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -124,9 +124,21 @@ class HueBridge(object): (group for group in self.api.groups.values() if group.name == group_name), None) - scene_id = next( - (scene.id for scene in self.api.scenes.values() - if scene.name == scene_name), None) + # The same scene name can exist in multiple groups. + # In this case, activate first scene that contains the + # the exact same light IDs as the group + scenes = [] + for scene in self.api.scenes.values(): + if scene.name == scene_name: + scenes.append(scene) + if len(scenes) == 1: + scene_id = scenes[0].id + else: + group_lights = sorted(group.lights) + for scene in scenes: + if group_lights == scene.lights: + scene_id = scene.id + break # If we can't find it, fetch latest info. if not updated and (group is None or scene_id is None): diff --git a/requirements_all.txt b/requirements_all.txt index ae16651d8e9..d6dc1725ba2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ aiodns==1.1.1 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.3.0 +aiohue==1.5.0 # homeassistant.components.sensor.imap aioimaplib==0.7.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce458995d2a..cb0dbc0689f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -35,7 +35,7 @@ aioautomatic==0.6.5 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==1.3.0 +aiohue==1.5.0 # homeassistant.components.notify.apns apns2==0.3.0 From 64f157a036dae9c7cfdb14113480f73df206e1d1 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 29 May 2018 10:15:30 +0200 Subject: [PATCH 879/924] Ignore unsupported Sonos favorite lists (#14665) --- .../components/media_player/sonos.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 06e5f3befe4..0f536e1edfb 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -427,15 +427,18 @@ class SonosDevice(MediaPlayerDevice): self.update_volume() self._favorites = [] - for fav in self.soco.music_library.get_sonos_favorites(): - # SoCo 0.14 raises a generic Exception on invalid xml in favorites. - # Filter those out now so our list is safe to use. - try: - if fav.reference.get_uri(): - self._favorites.append(fav) - # pylint: disable=broad-except - except Exception: - _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) + # SoCo 0.14 raises a generic Exception on invalid xml in favorites. + # Filter those out now so our list is safe to use. + # pylint: disable=broad-except + try: + for fav in self.soco.music_library.get_sonos_favorites(): + try: + if fav.reference.get_uri(): + self._favorites.append(fav) + except Exception: + _LOGGER.debug("Ignoring invalid favorite '%s'", fav.title) + except Exception: + _LOGGER.debug("Ignoring invalid favorite list") def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" From 40aba3d7850855f75ecc8db12d47c1aa63183d93 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 29 May 2018 15:03:45 +0200 Subject: [PATCH 880/924] MQTT Cover Fix Assumed State (#14672) --- homeassistant/components/cover/mqtt.py | 5 +++++ tests/components/cover/test_mqtt.py | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 0f31d3a9fe0..235ff5799cc 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -235,6 +235,11 @@ class MqttCover(MqttAvailability, CoverDevice): """No polling needed.""" return False + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + @property def name(self): """Return the name of the cover.""" diff --git a/tests/components/cover/test_mqtt.py b/tests/components/cover/test_mqtt.py index 23a7b32fc28..aea6398e3ae 100644 --- a/tests/components/cover/test_mqtt.py +++ b/tests/components/cover/test_mqtt.py @@ -2,8 +2,8 @@ import unittest from homeassistant.setup import setup_component -from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN,\ - STATE_UNAVAILABLE +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_UNKNOWN, \ + STATE_UNAVAILABLE, ATTR_ASSUMED_STATE import homeassistant.components.cover as cover from homeassistant.components.cover.mqtt import MqttCover @@ -40,6 +40,7 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) fire_mqtt_message(self.hass, 'state-topic', '0') self.hass.block_till_done() @@ -112,6 +113,7 @@ class TestCoverMQTT(unittest.TestCase): state = self.hass.states.get('cover.test') self.assertEqual(STATE_UNKNOWN, state.state) + self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) cover.open_cover(self.hass, 'cover.test') self.hass.block_till_done() From 753ffdaffdbe1bf18048484dc8e14955b8a1db9a Mon Sep 17 00:00:00 2001 From: Marius Date: Fri, 1 Jun 2018 00:26:59 +0300 Subject: [PATCH 881/924] Fix Eco mode display on Nest (#14706) * Fix Eco mode display on Nest * Fix Hound problems --- homeassistant/components/climate/nest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 0a5344fdf98..28e8020ab90 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -134,7 +134,9 @@ class NestThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._mode != NEST_MODE_HEAT_COOL and not self.is_away_mode_on: + if self._mode != NEST_MODE_HEAT_COOL and \ + self._mode != STATE_ECO and \ + not self.is_away_mode_on: return self._target_temperature return None From f7f0138cff6eea549000e8d5bd9852d01fceea74 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:25:35 -0400 Subject: [PATCH 882/924] Bump frontend to 20180531.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2bd7283e38e..5ebf6e8762f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.4'] +REQUIREMENTS = ['home-assistant-frontend==20180531.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d6dc1725ba2..9749557873d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.4 +home-assistant-frontend==20180531.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb0dbc0689f..1e04d3fdb03 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.4 +home-assistant-frontend==20180531.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d4a4938fce1caf08bb651f07a0955163ffc23fbb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:27:55 -0400 Subject: [PATCH 883/924] Version bump to 0.70.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 84088c4511c..bb60a42fff9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ed9cf994c202ef3b051e110bed60fbe511239d06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:58:03 -0400 Subject: [PATCH 884/924] Revert "Remove simplepush.io (#14358)" This reverts commit 612a37b2dd37f4856ac7103bb7bc6f7dc6d8b970. --- .coveragerc | 1 + homeassistant/components/notify/simplepush.py | 59 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 63 insertions(+) create mode 100644 homeassistant/components/notify/simplepush.py diff --git a/.coveragerc b/.coveragerc index 26744ad6952..dfbbb232efc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -547,6 +547,7 @@ omit = homeassistant/components/notify/rest.py homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py + homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/stride.py diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py new file mode 100644 index 00000000000..9d5c58fc5b1 --- /dev/null +++ b/homeassistant/components/notify/simplepush.py @@ -0,0 +1,59 @@ +""" +Simplepush notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.simplepush/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_PASSWORD + +REQUIREMENTS = ['simplepush==1.1.4'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_ENCRYPTED = 'encrypted' + +CONF_DEVICE_KEY = 'device_key' +CONF_EVENT = 'event' +CONF_SALT = 'salt' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_KEY): cv.string, + vol.Optional(CONF_EVENT): cv.string, + vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, + vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Simplepush notification service.""" + return SimplePushNotificationService(config) + + +class SimplePushNotificationService(BaseNotificationService): + """Implementation of the notification service for Simplepush.""" + + def __init__(self, config): + """Initialize the Simplepush notification service.""" + self._device_key = config.get(CONF_DEVICE_KEY) + self._event = config.get(CONF_EVENT) + self._password = config.get(CONF_PASSWORD) + self._salt = config.get(CONF_SALT) + + def send_message(self, message='', **kwargs): + """Send a message to a Simplepush user.""" + from simplepush import send, send_encrypted + + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + + if self._password: + send_encrypted(self._device_key, self._password, self._salt, title, + message, event=self._event) + else: + send(self._device_key, title, message, event=self._event) diff --git a/requirements_all.txt b/requirements_all.txt index 6354e9c49c7..a901c9cb153 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1204,6 +1204,9 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan shodan==1.7.7 +# homeassistant.components.notify.simplepush +simplepush==1.1.4 + # homeassistant.components.alarm_control_panel.simplisafe simplisafe-python==1.0.5 From d31e01b8778780a79ce254fd091fb098f4ac8923 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 31 May 2018 17:58:03 -0400 Subject: [PATCH 885/924] Revert "Remove simplepush.io (#14358)" This reverts commit 612a37b2dd37f4856ac7103bb7bc6f7dc6d8b970. --- .coveragerc | 1 + homeassistant/components/notify/simplepush.py | 59 +++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 63 insertions(+) create mode 100644 homeassistant/components/notify/simplepush.py diff --git a/.coveragerc b/.coveragerc index d361cf2ddad..80ca261f32d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -542,6 +542,7 @@ omit = homeassistant/components/notify/rest.py homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py + homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/stride.py diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py new file mode 100644 index 00000000000..9d5c58fc5b1 --- /dev/null +++ b/homeassistant/components/notify/simplepush.py @@ -0,0 +1,59 @@ +""" +Simplepush notification service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.simplepush/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import CONF_PASSWORD + +REQUIREMENTS = ['simplepush==1.1.4'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_ENCRYPTED = 'encrypted' + +CONF_DEVICE_KEY = 'device_key' +CONF_EVENT = 'event' +CONF_SALT = 'salt' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_DEVICE_KEY): cv.string, + vol.Optional(CONF_EVENT): cv.string, + vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, + vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the Simplepush notification service.""" + return SimplePushNotificationService(config) + + +class SimplePushNotificationService(BaseNotificationService): + """Implementation of the notification service for Simplepush.""" + + def __init__(self, config): + """Initialize the Simplepush notification service.""" + self._device_key = config.get(CONF_DEVICE_KEY) + self._event = config.get(CONF_EVENT) + self._password = config.get(CONF_PASSWORD) + self._salt = config.get(CONF_SALT) + + def send_message(self, message='', **kwargs): + """Send a message to a Simplepush user.""" + from simplepush import send, send_encrypted + + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + + if self._password: + send_encrypted(self._device_key, self._password, self._salt, title, + message, event=self._event) + else: + send(self._device_key, title, message, event=self._event) diff --git a/requirements_all.txt b/requirements_all.txt index 9749557873d..3bed3fa9fdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1186,6 +1186,9 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan shodan==1.7.7 +# homeassistant.components.notify.simplepush +simplepush==1.1.4 + # homeassistant.components.alarm_control_panel.simplisafe simplisafe-python==1.0.5 From 99fdd3e358ad0fa2d04c3703eaf2be4af1ffd29a Mon Sep 17 00:00:00 2001 From: glenn20 Date: Fri, 1 Jun 2018 08:32:09 +1000 Subject: [PATCH 886/924] Add device_descriptor and device_name to keyboard event (#14642) * Add device_descriptor and device_name to keyboard event This allows automations to identify which device has generated the keypress. This is especially useful for bluetooth remotes to control different devices. * Remove line breaks * Fix --- homeassistant/components/keyboard_remote.py | 49 +++++++-------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/keyboard_remote.py b/homeassistant/components/keyboard_remote.py index d737c555873..af45bd3d4f9 100644 --- a/homeassistant/components/keyboard_remote.py +++ b/homeassistant/components/keyboard_remote.py @@ -50,10 +50,7 @@ def setup(hass, config): """Set up the keyboard_remote.""" config = config.get(DOMAIN) - keyboard_remote = KeyboardRemote( - hass, - config - ) + keyboard_remote = KeyboardRemote(hass, config) def _start_keyboard_remote(_event): keyboard_remote.run() @@ -61,14 +58,8 @@ def setup(hass, config): def _stop_keyboard_remote(_event): keyboard_remote.stop() - hass.bus.listen_once( - EVENT_HOMEASSISTANT_START, - _start_keyboard_remote - ) - hass.bus.listen_once( - EVENT_HOMEASSISTANT_STOP, - _stop_keyboard_remote - ) + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, _start_keyboard_remote) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_keyboard_remote) return True @@ -93,10 +84,8 @@ class KeyboardRemoteThread(threading.Thread): _LOGGER.debug("Keyboard connected, %s", self.device_id) else: _LOGGER.debug( - 'Keyboard not connected, %s.\n\ - Check /dev/input/event* permissions.', - self.device_id - ) + "Keyboard not connected, %s. " + "Check /dev/input/event* permissions", self.device_id) id_folder = '/dev/input/by-id/' @@ -105,12 +94,9 @@ class KeyboardRemoteThread(threading.Thread): device_names = [InputDevice(file_name).name for file_name in list_devices()] _LOGGER.debug( - 'Possible device names are:\n %s.\n \ - Possible device descriptors are %s:\n %s', - device_names, - id_folder, - os.listdir(id_folder) - ) + "Possible device names are: %s. " + "Possible device descriptors are %s: %s", + device_names, id_folder, os.listdir(id_folder)) threading.Thread.__init__(self) self.stopped = threading.Event() @@ -149,9 +135,7 @@ class KeyboardRemoteThread(threading.Thread): self.dev = self._get_keyboard_device() if self.dev is not None: self.dev.grab() - self.hass.bus.fire( - KEYBOARD_REMOTE_CONNECTED - ) + self.hass.bus.fire(KEYBOARD_REMOTE_CONNECTED) _LOGGER.debug("Keyboard re-connected, %s", self.device_id) else: continue @@ -160,9 +144,7 @@ class KeyboardRemoteThread(threading.Thread): event = self.dev.read_one() except IOError: # Keyboard Disconnected self.dev = None - self.hass.bus.fire( - KEYBOARD_REMOTE_DISCONNECTED - ) + self.hass.bus.fire(KEYBOARD_REMOTE_DISCONNECTED) _LOGGER.debug("Keyboard disconnected, %s", self.device_id) continue @@ -174,7 +156,11 @@ class KeyboardRemoteThread(threading.Thread): _LOGGER.debug(categorize(event)) self.hass.bus.fire( KEYBOARD_REMOTE_COMMAND_RECEIVED, - {KEY_CODE: event.code} + { + KEY_CODE: event.code, + DEVICE_DESCRIPTOR: self.device_descriptor, + DEVICE_NAME: self.device_name + } ) @@ -191,9 +177,8 @@ class KeyboardRemote(object): if device_descriptor is not None\ or device_name is not None: - thread = KeyboardRemoteThread(hass, device_name, - device_descriptor, - key_value) + thread = KeyboardRemoteThread( + hass, device_name, device_descriptor, key_value) self.threads.append(thread) def run(self): From de56a0d021bb803f2a980bdace6d614c8f28045f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 08:40:27 +0200 Subject: [PATCH 887/924] Upgrade shodan to 1.8.0 (#14717) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 720158e1029..1cc2ba30866 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.7.7'] +REQUIREMENTS = ['shodan==1.8.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a901c9cb153..60b010ab306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ sense_energy==0.3.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.7.7 +shodan==1.8.0 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 6cd69b413cfd6d4cd6ac3f53c4af82dd902f1e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 1 Jun 2018 02:41:40 -0400 Subject: [PATCH 888/924] Bump pyatv to 0.3.10 (#14736) * Bump pyatv to 0.3.10 * Update requirements_all.txt --- homeassistant/components/apple_tv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py index a9bd5c9c8bc..68445092db7 100644 --- a/homeassistant/components/apple_tv.py +++ b/homeassistant/components/apple_tv.py @@ -17,7 +17,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pyatv==0.3.9'] +REQUIREMENTS = ['pyatv==0.3.10'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 60b010ab306..1c2f927eaf5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -731,7 +731,7 @@ pyasn1-modules==0.1.5 pyasn1==0.3.7 # homeassistant.components.apple_tv -pyatv==0.3.9 +pyatv==0.3.10 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox From ab3717af767d3c740b3e9567299afd363ccaf607 Mon Sep 17 00:00:00 2001 From: roiff Date: Fri, 1 Jun 2018 19:49:16 +0800 Subject: [PATCH 889/924] Homekit Thermostat: Better support for temperature ranges (#14679) * Support for obtaining temperature range * Fallback to Defaults * Fixed unit conversion * Added test --- .../components/homekit/type_thermostats.py | 39 +++++++++++---- .../homekit/test_type_thermostats.py | 48 +++++++++++++++++-- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index d6555d5056d..73a29990fba 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,15 +4,16 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, + ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES from .accessories import debounce, HomeAccessory @@ -20,7 +21,7 @@ from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, - CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) + CHAR_TEMP_DISPLAY_UNITS, PROP_MAX_VALUE, PROP_MIN_VALUE, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -42,17 +43,18 @@ class Thermostat(HomeAccessory): def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) - self._unit = TEMP_CELSIUS + self._unit = self.hass.config.units.temperature_unit self.support_power_state = False self.heat_cool_flag_target_state = False self.temperature_flag_target_state = False self.coolingthresh_flag_target_state = False self.heatingthresh_flag_target_state = False + min_temp, max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported self.chars = [] features = self.hass.states.get(self.entity_id) \ - .attributes.get(ATTR_SUPPORTED_FEATURES) + .attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_ON_OFF: self.support_power_state = True if features & SUPPORT_TEMP_RANGE: @@ -73,6 +75,8 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_TEMPERATURE, value=21.0) self.char_target_temp = serv_thermostat.configure_char( CHAR_TARGET_TEMPERATURE, value=21.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_target_temperature) # Display units characteristic @@ -85,12 +89,30 @@ class Thermostat(HomeAccessory): if CHAR_COOLING_THRESHOLD_TEMPERATURE in self.chars: self.char_cooling_thresh_temp = serv_thermostat.configure_char( CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_cooling_threshold) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: self.char_heating_thresh_temp = serv_thermostat.configure_char( CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, + properties={PROP_MIN_VALUE: min_temp, + PROP_MAX_VALUE: max_temp}, setter_callback=self.set_heating_threshold) + def get_temperature_range(self): + """Return min and max temperature range.""" + max_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MAX_TEMP) + max_temp = temperature_to_homekit(max_temp, self._unit) if max_temp \ + else DEFAULT_MAX_TEMP + + min_temp = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_MIN_TEMP) + min_temp = temperature_to_homekit(min_temp, self._unit) if min_temp \ + else DEFAULT_MIN_TEMP + + return min_temp, max_temp + def set_heat_cool(self, value): """Move operation mode to value if call came from HomeKit.""" if value in HC_HOMEKIT_TO_HASS: @@ -147,9 +169,6 @@ class Thermostat(HomeAccessory): def update_state(self, new_state): """Update security state after state changed.""" - self._unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS) - # Update current temperature current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) if isinstance(current_temp, (int, float)): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 1f6554496a9..00e3e2d22fc 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,12 +1,16 @@ """Test different accessory types: Thermostats.""" from collections import namedtuple +from unittest.mock import patch import pytest from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) + ATTR_CURRENT_TEMPERATURE, ATTR_MAX_TEMP, ATTR_MIN_TEMP, ATTR_TEMPERATURE, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, + ATTR_OPERATION_LIST, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, + STATE_AUTO, STATE_COOL, STATE_HEAT) +from homeassistant.components.homekit.const import ( + PROP_MAX_VALUE, PROP_MIN_VALUE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -31,7 +35,7 @@ async def test_default_thermostat(hass, hk_driver, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'climate.test' - hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + hass.states.async_set(entity_id, STATE_OFF) await hass.async_block_till_done() acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -48,6 +52,9 @@ async def test_default_thermostat(hass, hk_driver, cls): assert acc.char_cooling_thresh_temp is None assert acc.char_heating_thresh_temp is None + assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP + assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_HEAT, {ATTR_OPERATION_MODE: STATE_HEAT, ATTR_TEMPERATURE: 22.0, @@ -181,6 +188,15 @@ async def test_auto_thermostat(hass, hk_driver, cls): assert acc.char_cooling_thresh_temp.value == 23.0 assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] \ + == DEFAULT_MAX_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] \ + == DEFAULT_MIN_TEMP + hass.states.async_set(entity_id, STATE_AUTO, {ATTR_OPERATION_MODE: STATE_AUTO, ATTR_TARGET_TEMP_HIGH: 22.0, @@ -307,7 +323,9 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + with patch.object(hass.config.units, 'temperature_unit', + new=TEMP_FAHRENHEIT): + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -349,3 +367,23 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls): assert call_set_temperature[2] assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 + + +async def test_get_temperature_range(hass, hk_driver, cls): + """Test if temperature range is evaluated correctly.""" + entity_id = 'climate.test' + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, 'Climate', entity_id, 2, None) + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (20, 25) + + acc._unit = TEMP_FAHRENHEIT + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70}) + await hass.async_block_till_done() + assert acc.get_temperature_range() == (15.6, 21.1) From f6eb9e79d5c4ead4d81ea04aafb165c7027fadab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jun 2018 10:06:17 -0400 Subject: [PATCH 890/924] Custom panel (#14708) * Add support for custom panels in JS * Allow specifying JS custom panels * Add trust external option * Fix tests * Do I/O outside event loop * Change config to avoid breaking change --- homeassistant/components/panel_custom.py | 49 ++++++++++---- tests/components/test_panel_custom.py | 83 +++++++++++++++++------- 2 files changed, 99 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom.py index 473d44f3b55..4659578ae27 100644 --- a/homeassistant/components/panel_custom.py +++ b/homeassistant/components/panel_custom.py @@ -4,7 +4,6 @@ Register a custom front end panel. For more details about this component, please refer to the documentation at https://home-assistant.io/components/panel_custom/ """ -import asyncio import logging import os @@ -21,27 +20,33 @@ CONF_SIDEBAR_ICON = 'sidebar_icon' CONF_URL_PATH = 'url_path' CONF_CONFIG = 'config' CONF_WEBCOMPONENT_PATH = 'webcomponent_path' +CONF_JS_URL = 'js_url' +CONF_EMBED_IFRAME = 'embed_iframe' +CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script' DEFAULT_ICON = 'mdi:bookmark' +LEGACY_URL = '/api/panel_custom/{}' PANEL_DIR = 'panels' CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.All(cv.ensure_list, [{ - vol.Required(CONF_COMPONENT_NAME): cv.slug, + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_COMPONENT_NAME): cv.string, vol.Optional(CONF_SIDEBAR_TITLE): cv.string, vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, vol.Optional(CONF_URL_PATH): cv.string, - vol.Optional(CONF_CONFIG): cv.match_all, + vol.Optional(CONF_CONFIG): dict, vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile, - }]) + vol.Optional(CONF_JS_URL): cv.string, + vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean, + vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean, + })]) }, extra=vol.ALLOW_EXTRA) _LOGGER = logging.getLogger(__name__) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize custom panel.""" success = False @@ -52,17 +57,39 @@ def async_setup(hass, config): if panel_path is None: panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name)) - if not os.path.isfile(panel_path): + custom_panel_config = { + 'name': name, + 'embed_iframe': panel[CONF_EMBED_IFRAME], + 'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT], + } + + if CONF_JS_URL in panel: + custom_panel_config['js_url'] = panel[CONF_JS_URL] + + elif not await hass.async_add_job(os.path.isfile, panel_path): _LOGGER.error('Unable to find webcomponent for %s: %s', name, panel_path) continue - yield from hass.components.frontend.async_register_panel( - name, panel_path, + else: + url = LEGACY_URL.format(name) + hass.http.register_static_path(url, panel_path) + custom_panel_config['html_url'] = LEGACY_URL.format(name) + + if CONF_CONFIG in panel: + # Make copy because we're mutating it + config = dict(panel[CONF_CONFIG]) + else: + config = {} + + config['_panel_custom'] = custom_panel_config + + await hass.components.frontend.async_register_built_in_panel( + component_name='custom', sidebar_title=panel.get(CONF_SIDEBAR_TITLE), sidebar_icon=panel.get(CONF_SIDEBAR_ICON), frontend_url_path=panel.get(CONF_URL_PATH), - config=panel.get(CONF_CONFIG), + config=config ) success = True diff --git a/tests/components/test_panel_custom.py b/tests/components/test_panel_custom.py index d33221da2a7..596aa1b3c0b 100644 --- a/tests/components/test_panel_custom.py +++ b/tests/components/test_panel_custom.py @@ -1,23 +1,11 @@ """The tests for the panel_custom component.""" -import asyncio from unittest.mock import Mock, patch -import pytest - from homeassistant import setup from homeassistant.components import frontend -from tests.common import mock_component - -@pytest.fixture(autouse=True) -def mock_frontend_loaded(hass): - """Mock frontend is loaded.""" - mock_component(hass, 'frontend') - - -@asyncio.coroutine -def test_webcomponent_custom_path_not_found(hass): +async def test_webcomponent_custom_path_not_found(hass): """Test if a web component is found in config panels dir.""" filename = 'mock.file' @@ -33,45 +21,96 @@ def test_webcomponent_custom_path_not_found(hass): } with patch('os.path.isfile', Mock(return_value=False)): - result = yield from setup.async_setup_component( + result = await setup.async_setup_component( hass, 'panel_custom', config ) assert not result assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0 -@asyncio.coroutine -def test_webcomponent_custom_path(hass): +async def test_webcomponent_custom_path(hass): """Test if a web component is found in config panels dir.""" filename = 'mock.file' config = { 'panel_custom': { - 'name': 'todomvc', + 'name': 'todo-mvc', 'webcomponent_path': filename, 'sidebar_title': 'Sidebar Title', 'sidebar_icon': 'mdi:iconicon', 'url_path': 'nice_url', - 'config': 5, + 'config': { + 'hello': 'world', + } } } with patch('os.path.isfile', Mock(return_value=True)): with patch('os.access', Mock(return_value=True)): - result = yield from setup.async_setup_component( + result = await setup.async_setup_component( hass, 'panel_custom', config ) assert result panels = hass.data.get(frontend.DATA_PANELS, []) - assert len(panels) == 1 + assert panels assert 'nice_url' in panels panel = panels['nice_url'] - assert panel.config == 5 + assert panel.config == { + 'hello': 'world', + '_panel_custom': { + 'html_url': '/api/panel_custom/todo-mvc', + 'name': 'todo-mvc', + 'embed_iframe': False, + 'trust_external': False, + }, + } assert panel.frontend_url_path == 'nice_url' assert panel.sidebar_icon == 'mdi:iconicon' assert panel.sidebar_title == 'Sidebar Title' - assert panel.path == filename + + +async def test_js_webcomponent(hass): + """Test if a web component is found in config panels dir.""" + config = { + 'panel_custom': { + 'name': 'todo-mvc', + 'js_url': '/local/bla.js', + 'sidebar_title': 'Sidebar Title', + 'sidebar_icon': 'mdi:iconicon', + 'url_path': 'nice_url', + 'config': { + 'hello': 'world', + }, + 'embed_iframe': True, + 'trust_external_script': True, + } + } + + result = await setup.async_setup_component( + hass, 'panel_custom', config + ) + assert result + + panels = hass.data.get(frontend.DATA_PANELS, []) + + assert panels + assert 'nice_url' in panels + + panel = panels['nice_url'] + + assert panel.config == { + 'hello': 'world', + '_panel_custom': { + 'js_url': '/local/bla.js', + 'name': 'todo-mvc', + 'embed_iframe': True, + 'trust_external': True, + } + } + assert panel.frontend_url_path == 'nice_url' + assert panel.sidebar_icon == 'mdi:iconicon' + assert panel.sidebar_title == 'Sidebar Title' From fcbc399809e16dd9d79c1982c9ea16381cd4f88c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 1 Jun 2018 16:27:12 +0200 Subject: [PATCH 891/924] Disallow automation.trigger without entity_id (#14724) --- homeassistant/components/automation/__init__.py | 2 +- tests/components/automation/test_init.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 2f510fd33d6..2a7a3887b34 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -98,7 +98,7 @@ SERVICE_SCHEMA = vol.Schema({ }) TRIGGER_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_VARIABLES, default={}): dict, }) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 7a8c097a730..33f1a7aa704 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -207,6 +207,7 @@ class TestAutomation(unittest.TestCase): """Test triggers.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { + 'alias': 'test', 'trigger': [ { 'platform': 'event', @@ -228,7 +229,9 @@ class TestAutomation(unittest.TestCase): self.hass.block_till_done() assert len(self.calls) == 0 - self.hass.services.call('automation', 'trigger', blocking=True) + self.hass.services.call('automation', 'trigger', + {'entity_id': 'automation.test'}, + blocking=True) self.hass.block_till_done() assert len(self.calls) == 1 From cba8333a1389410fce11aaf8afbdf3e358a6a871 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Fri, 1 Jun 2018 07:44:58 -0700 Subject: [PATCH 892/924] Change nest to cloud push (#14656) * Change nest component to Cloud Push Change sensors.nest, binary_sensors.nest and climate.nest to push mode nest camera still need poll to update snapshot image Also change nest component to async * Flake8 lint * Fix async_notify_errors, it is not a coroutine * Fix pylint * Fix pylint, function name should shall shorter than 32 * Use dispatcher helper instead event bus * Use async_update_ha_state(True) * Refactoring load_platform Move service registration into async_setup_nest(), resolve an issue that before the first time configuration done, set_mode service should not be registered * Fix an issue that authorization failure may leave a blocked thread * Pylinting * async_nest_update_callback => async_update_state to avoid confusion * Move signal handler register to async_added_to_hass * Better handle nest api error * Remove unnecessary register for binary_sensor * Remove unused import * Upgrade to python-nest 4.0.1 Fix a thread race condition issue * Address my own comments * Address my own comment --- .../components/binary_sensor/nest.py | 2 +- homeassistant/components/climate/nest.py | 40 +++-- homeassistant/components/nest.py | 150 ++++++++++++------ homeassistant/components/sensor/nest.py | 18 ++- requirements_all.txt | 2 +- 5 files changed, 149 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 2a1732cd9f0..008b6eed1e4 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -8,9 +8,9 @@ from itertools import chain import logging from homeassistant.components.binary_sensor import (BinarySensorDevice) +from homeassistant.components.nest import DATA_NEST from homeassistant.components.sensor.nest import NestSensor from homeassistant.const import CONF_MONITORED_CONDITIONS -from homeassistant.components.nest import DATA_NEST DEPENDENCIES = ['nest'] diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index 28e8020ab90..696f1479c08 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol -from homeassistant.components.nest import DATA_NEST +from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE from homeassistant.components.climate import ( STATE_AUTO, STATE_COOL, STATE_HEAT, STATE_ECO, ClimateDevice, PLATFORM_SCHEMA, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -18,6 +18,7 @@ from homeassistant.components.climate import ( from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_SCAN_INTERVAL, STATE_ON, STATE_OFF, STATE_UNKNOWN) +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['nest'] _LOGGER = logging.getLogger(__name__) @@ -37,11 +38,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): temp_unit = hass.config.units.temperature_unit - add_devices( - [NestThermostat(structure, device, temp_unit) - for structure, device in hass.data[DATA_NEST].thermostats()], - True - ) + all_devices = [NestThermostat(structure, device, temp_unit) + for structure, device in hass.data[DATA_NEST].thermostats()] + + add_devices(all_devices, True) class NestThermostat(ClimateDevice): @@ -97,6 +97,20 @@ class NestThermostat(ClimateDevice): self._min_temperature = None self._max_temperature = None + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update device state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) + @property def supported_features(self): """Return the list of supported features.""" @@ -170,18 +184,24 @@ class NestThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" import nest + temp = None target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) if self._mode == NEST_MODE_HEAT_COOL: if target_temp_low is not None and target_temp_high is not None: temp = (target_temp_low, target_temp_high) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) else: temp = kwargs.get(ATTR_TEMPERATURE) - _LOGGER.debug("Nest set_temperature-output-value=%s", temp) + _LOGGER.debug("Nest set_temperature-output-value=%s", temp) try: - self.device.target = temp - except nest.nest.APIError: - _LOGGER.error("An error occurred while setting the temperature") + if temp is not None: + self.device.target = temp + except nest.nest.APIError as api_error: + _LOGGER.error("An error occurred while setting temperature: %s", + api_error) + # restore target temperature + self.schedule_update_ha_state(True) def set_operation_mode(self, operation_mode): """Set operation mode.""" diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index f474bfa7a26..365f0593c8d 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -4,18 +4,20 @@ Support for Nest devices. For more details about this component, please refer to the documentation at https://home-assistant.io/components/nest/ """ +from concurrent.futures import ThreadPoolExecutor import logging import socket import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import ( CONF_STRUCTURE, CONF_FILENAME, CONF_BINARY_SENSORS, CONF_SENSORS, - CONF_MONITORED_CONDITIONS) + CONF_MONITORED_CONDITIONS, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) +from homeassistant.helpers import discovery, config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['python-nest==4.0.0'] +REQUIREMENTS = ['python-nest==4.0.1'] _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -24,6 +26,8 @@ DOMAIN = 'nest' DATA_NEST = 'nest' +SIGNAL_NEST_UPDATE = 'nest_update' + NEST_CONFIG_FILE = 'nest.conf' CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' @@ -51,23 +55,44 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -def request_configuration(nest, hass, config): +async def async_nest_update_event_broker(hass, nest): + """ + Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. + + nest.update_event.wait will block the thread in most of time, + so specific an executor to save default thread pool. + """ + _LOGGER.debug("listening nest.update_event") + with ThreadPoolExecutor(max_workers=1) as executor: + while True: + await hass.loop.run_in_executor(executor, nest.update_event.wait) + if hass.is_running: + nest.update_event.clear() + _LOGGER.debug("dispatching nest data update") + async_dispatcher_send(hass, SIGNAL_NEST_UPDATE) + else: + return + + +async def async_request_configuration(nest, hass, config): """Request configuration steps from the user.""" configurator = hass.components.configurator if 'nest' in _CONFIGURING: _LOGGER.debug("configurator failed") - configurator.notify_errors( + configurator.async_notify_errors( _CONFIGURING['nest'], "Failed to configure, please try again.") return - def nest_configuration_callback(data): + async def async_nest_config_callback(data): """Run when the configuration callback is called.""" _LOGGER.debug("configurator callback") pin = data.get('pin') - setup_nest(hass, nest, config, pin=pin) + if await async_setup_nest(hass, nest, config, pin=pin): + # start nest update event listener as we missed startup hook + hass.async_add_job(async_nest_update_event_broker, hass, nest) - _CONFIGURING['nest'] = configurator.request_config( - "Nest", nest_configuration_callback, + _CONFIGURING['nest'] = configurator.async_request_config( + "Nest", async_nest_config_callback, description=('To configure Nest, click Request Authorization below, ' 'log into your Nest account, ' 'and then enter the resulting PIN'), @@ -78,60 +103,47 @@ def request_configuration(nest, hass, config): ) -def setup_nest(hass, nest, config, pin=None): +async def async_setup_nest(hass, nest, config, pin=None): """Set up the Nest devices.""" + from nest.nest import AuthorizationError, APIError if pin is not None: _LOGGER.debug("pin acquired, requesting access token") - nest.request_token(pin) + error_message = None + try: + nest.request_token(pin) + except AuthorizationError as auth_error: + error_message = "Nest authorization failed: {}".format(auth_error) + except APIError as api_error: + error_message = "Failed to call Nest API: {}".format(api_error) + + if error_message is not None: + _LOGGER.warning(error_message) + hass.components.configurator.async_notify_errors( + _CONFIGURING['nest'], error_message) + return False if nest.access_token is None: _LOGGER.debug("no access_token, requesting configuration") - request_configuration(nest, hass, config) - return + await async_request_configuration(nest, hass, config) + return False if 'nest' in _CONFIGURING: _LOGGER.debug("configuration done") configurator = hass.components.configurator - configurator.request_done(_CONFIGURING.pop('nest')) + configurator.async_request_done(_CONFIGURING.pop('nest')) _LOGGER.debug("proceeding with setup") conf = config[DOMAIN] hass.data[DATA_NEST] = NestDevice(hass, conf, nest) - _LOGGER.debug("proceeding with discovery") - discovery.load_platform(hass, 'climate', DOMAIN, {}, config) - discovery.load_platform(hass, 'camera', DOMAIN, {}, config) - - sensor_config = conf.get(CONF_SENSORS, {}) - discovery.load_platform(hass, 'sensor', DOMAIN, sensor_config, config) - - binary_sensor_config = conf.get(CONF_BINARY_SENSORS, {}) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, - binary_sensor_config, config) - - _LOGGER.debug("setup done") - - return True - - -def setup(hass, config): - """Set up the Nest thermostat component.""" - import nest - - if 'nest' in _CONFIGURING: - return - - conf = config[DOMAIN] - client_id = conf[CONF_CLIENT_ID] - client_secret = conf[CONF_CLIENT_SECRET] - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - - access_token_cache_file = hass.config.path(filename) - - nest = nest.Nest( - access_token_cache_file=access_token_cache_file, - client_id=client_id, client_secret=client_secret) - setup_nest(hass, nest, config) + for component, discovered in [ + ('climate', {}), + ('camera', {}), + ('sensor', conf.get(CONF_SENSORS, {})), + ('binary_sensor', conf.get(CONF_BINARY_SENSORS, {}))]: + _LOGGER.debug("proceeding with discovery -- %s", component) + hass.async_add_job(discovery.async_load_platform, + hass, component, DOMAIN, discovered, config) def set_mode(service): """Set the home/away mode for a Nest structure.""" @@ -148,9 +160,47 @@ def setup(hass, config): _LOGGER.error("Invalid structure %s", service.data[ATTR_STRUCTURE]) - hass.services.register( + hass.services.async_register( DOMAIN, 'set_mode', set_mode, schema=AWAY_SCHEMA) + def start_up(event): + """Start Nest update event listener.""" + hass.async_add_job(async_nest_update_event_broker, hass, nest) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + + def shut_down(event): + """Stop Nest update event listener.""" + if nest: + nest.update_event.set() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + + _LOGGER.debug("async_setup_nest is done") + + return True + + +async def async_setup(hass, config): + """Set up Nest components.""" + from nest import Nest + + if 'nest' in _CONFIGURING: + return + + conf = config[DOMAIN] + client_id = conf[CONF_CLIENT_ID] + client_secret = conf[CONF_CLIENT_SECRET] + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + + access_token_cache_file = hass.config.path(filename) + + nest = Nest( + access_token_cache_file=access_token_cache_file, + client_id=client_id, client_secret=client_secret) + + await async_setup_nest(hass, nest, config) + return True diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 0de2e2e0cdb..46a2206a9f7 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -7,13 +7,15 @@ https://home-assistant.io/components/sensor.nest/ from itertools import chain import logging -from homeassistant.components.nest import DATA_NEST +from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, DEVICE_CLASS_TEMPERATURE) DEPENDENCIES = ['nest'] + SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] @@ -130,6 +132,20 @@ class NestSensor(Entity): """Return the unit the value is expressed in.""" return self._unit + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) + class NestBasicSensor(NestSensor): """Representation a basic Nest sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index 1c2f927eaf5..711defddb6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1033,7 +1033,7 @@ python-mpd2==1.0.0 python-mystrom==0.4.2 # homeassistant.components.nest -python-nest==4.0.0 +python-nest==4.0.1 # homeassistant.components.device_tracker.nmap_tracker python-nmap==0.6.1 From 0a724a54734f127eec0925538ab30ae70aa644ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jun 2018 10:52:25 -0400 Subject: [PATCH 893/924] Update frontend --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 5ebf6e8762f..fca9f33578a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180531.0'] +REQUIREMENTS = ['home-assistant-frontend==20180601.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 711defddb6e..4ed33a6dc5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180531.0 +home-assistant-frontend==20180601.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00b3d1f82e7..bbf01e1bfb4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180531.0 +home-assistant-frontend==20180601.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f5d74e07d51b0992d37105e1eb0fbbeca99a32ef Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 1 Jun 2018 12:04:54 -0400 Subject: [PATCH 894/924] Add support for outlets in HomeKit (#14628) --- homeassistant/components/homekit/__init__.py | 13 ++++-- homeassistant/components/homekit/const.py | 6 +++ .../components/homekit/type_switches.py | 39 ++++++++++++++++- homeassistant/components/homekit/util.py | 13 +++++- .../homekit/test_get_accessories.py | 23 +++++----- .../components/homekit/test_type_switches.py | 42 ++++++++++++++++++- tests/components/homekit/test_util.py | 9 ++-- 7 files changed, 123 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ce3b79e6c72..34372b8b6a8 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.components.cover as cover from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, DEVICE_CLASS_HUMIDITY, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, CONF_TYPE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT) @@ -23,7 +23,7 @@ from homeassistant.util.decorator import Registry from .const import ( CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, CONF_FILTER, DEFAULT_AUTO_START, DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, - DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) + DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, TYPE_OUTLET, TYPE_SWITCH) from .util import ( show_setup_message, validate_entity_config, validate_media_player_features) @@ -38,6 +38,8 @@ STATUS_RUNNING = 1 STATUS_STOPPED = 2 STATUS_WAIT = 3 +SWITCH_TYPES = {TYPE_OUTLET: 'Outlet', + TYPE_SWITCH: 'Switch'} CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All({ @@ -149,8 +151,11 @@ def get_accessory(hass, driver, state, aid, config): elif device_class == DEVICE_CLASS_ILLUMINANCE or unit in ('lm', 'lx'): a_type = 'LightSensor' - elif state.domain in ('automation', 'input_boolean', 'remote', 'script', - 'switch'): + elif state.domain == 'switch': + switch_type = config.get(CONF_TYPE, TYPE_SWITCH) + a_type = SWITCH_TYPES[switch_type] + + elif state.domain in ('automation', 'input_boolean', 'remote', 'script'): a_type = 'Switch' if a_type is None: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 6d49c806e0f..dec6353850e 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -31,6 +31,10 @@ BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' MANUFACTURER = 'Home Assistant' +# #### Switch Types #### +TYPE_OUTLET = 'outlet' +TYPE_SWITCH = 'switch' + # #### Services #### SERV_ACCESSORY_INFO = 'AccessoryInformation' SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' @@ -46,6 +50,7 @@ SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' +SERV_OUTLET = 'Outlet' SERV_SECURITY_SYSTEM = 'SecuritySystem' SERV_SMOKE_SENSOR = 'SmokeSensor' SERV_SWITCH = 'Switch' @@ -84,6 +89,7 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' +CHAR_OUTLET_IN_USE = 'OutletInUse' CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 69f14821bd6..c8bf8c7ad7c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -1,19 +1,54 @@ """Class to hold all switch accessories.""" import logging -from pyhap.const import CATEGORY_SWITCH +from pyhap.const import CATEGORY_OUTLET, CATEGORY_SWITCH +from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON) from homeassistant.core import split_entity_id from . import TYPES from .accessories import HomeAccessory -from .const import SERV_SWITCH, CHAR_ON +from .const import CHAR_ON, CHAR_OUTLET_IN_USE, SERV_OUTLET, SERV_SWITCH _LOGGER = logging.getLogger(__name__) +@TYPES.register('Outlet') +class Outlet(HomeAccessory): + """Generate an Outlet accessory.""" + + def __init__(self, *args): + """Initialize an Outlet accessory object.""" + super().__init__(*args, category=CATEGORY_OUTLET) + self.flag_target_state = False + + serv_outlet = self.add_preload_service(SERV_OUTLET) + self.char_on = serv_outlet.configure_char( + CHAR_ON, value=False, setter_callback=self.set_state) + self.char_outlet_in_use = serv_outlet.configure_char( + CHAR_OUTLET_IN_USE, value=True) + + def set_state(self, value): + """Move switch state to value if call came from HomeKit.""" + _LOGGER.debug('%s: Set switch state to %s', + self.entity_id, value) + self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF + self.hass.services.call(SWITCH, service, params) + + def update_state(self, new_state): + """Update switch state after state changed.""" + current_state = (new_state.state == STATE_ON) + if not self.flag_target_state: + _LOGGER.debug('%s: Set current state to %s', + self.entity_id, current_state) + self.char_on.set_value(current_state) + self.flag_target_state = False + + @TYPES.register('Switch') class Switch(HomeAccessory): """Generate a Switch accessory.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 50095844757..6a43a0c6228 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -6,12 +6,13 @@ import voluptuous as vol import homeassistant.components.media_player as media_player from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, TEMP_CELSIUS) + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE) + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, TYPE_OUTLET, + TYPE_SWITCH) _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,11 @@ MEDIA_PLAYER_SCHEMA = vol.Schema({ FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE))), }) +SWITCH_TYPE_SCHEMA = BASIC_INFO_SCHEMA.extend({ + vol.Optional(CONF_TYPE, default=TYPE_SWITCH): vol.All( + cv.string, vol.In((TYPE_OUTLET, TYPE_SWITCH))), +}) + def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" @@ -62,6 +68,9 @@ def validate_entity_config(values): feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list + elif domain == 'switch': + config = SWITCH_TYPE_SCHEMA(config) + else: config = BASIC_INFO_SCHEMA(config) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 3b7f307fce7..4de68057084 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -9,10 +9,11 @@ import homeassistant.components.climate as climate import homeassistant.components.media_player as media_player from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.components.homekit.const import ( - CONF_FEATURE_LIST, FEATURE_ON_OFF) + CONF_FEATURE_LIST, FEATURE_ON_OFF, TYPE_OUTLET, TYPE_SWITCH) from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_TYPE, TEMP_CELSIUS, + TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -129,17 +130,19 @@ def test_type_sensors(type_name, entity_id, state, attrs): assert mock_type.called -@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ - ('Switch', 'automation.test', 'on', {}), - ('Switch', 'input_boolean.test', 'on', {}), - ('Switch', 'remote.test', 'on', {}), - ('Switch', 'script.test', 'on', {}), - ('Switch', 'switch.test', 'on', {}), +@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Outlet', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_OUTLET}), + ('Switch', 'automation.test', 'on', {}, {}), + ('Switch', 'input_boolean.test', 'on', {}, {}), + ('Switch', 'remote.test', 'on', {}, {}), + ('Switch', 'script.test', 'on', {}, {}), + ('Switch', 'switch.test', 'on', {}, {}), + ('Switch', 'switch.test', 'on', {}, {CONF_TYPE: TYPE_SWITCH}), ]) -def test_type_switches(type_name, entity_id, state, attrs): +def test_type_switches(type_name, entity_id, state, attrs, config): """Test if switch types are associated correctly.""" mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, None, entity_state, 2, {}) + get_accessory(None, None, entity_state, 2, config) assert mock_type.called diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index b1830d1926f..3a09d2715d1 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -2,12 +2,51 @@ import pytest from homeassistant.core import split_entity_id -from homeassistant.components.homekit.type_switches import Switch +from homeassistant.components.homekit.type_switches import Outlet, Switch from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service +async def test_outlet_set_state(hass, hk_driver): + """Test if Outlet accessory and HA are updated accordingly.""" + entity_id = 'switch.outlet_test' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Outlet(hass, hk_driver, 'Outlet', entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 7 # Outlet + + assert acc.char_on.value is False + assert acc.char_outlet_in_use.value is True + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value is True + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value is False + + # Set from HomeKit + call_turn_on = async_mock_service(hass, 'switch', 'turn_on') + call_turn_off = async_mock_service(hass, 'switch', 'turn_off') + + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + @pytest.mark.parametrize('entity_id', [ 'automation.test', 'input_boolean.test', @@ -23,6 +62,7 @@ async def test_switch_set_state(hass, hk_driver, entity_id): await hass.async_block_till_done() acc = Switch(hass, hk_driver, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) + await hass.async_block_till_done() assert acc.aid == 2 assert acc.category == 8 # Switch diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0bc1eb96841..fa9fddee5fc 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.core import State from homeassistant.components.homekit.const import ( CONF_FEATURE, CONF_FEATURE_LIST, HOMEKIT_NOTIFY_ID, FEATURE_ON_OFF, - FEATURE_PLAY_PAUSE) + FEATURE_PLAY_PAUSE, TYPE_OUTLET) from homeassistant.components.homekit.util import ( convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, temperature_to_states, @@ -15,7 +15,7 @@ from homeassistant.components.homekit.util import validate_entity_config \ from homeassistant.components.persistent_notification import ( ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, STATE_UNKNOWN, + ATTR_CODE, ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_TYPE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service @@ -30,7 +30,8 @@ def test_validate_entity_config(): {CONF_FEATURE: 'invalid_feature'}]}}, {'media_player.test': {CONF_FEATURE_LIST: [ {CONF_FEATURE: FEATURE_ON_OFF}, - {CONF_FEATURE: FEATURE_ON_OFF}]}}, ] + {CONF_FEATURE: FEATURE_ON_OFF}]}}, + {'switch.test': {CONF_TYPE: 'invalid_type'}}] for conf in configs: with pytest.raises(vol.Invalid): @@ -56,6 +57,8 @@ def test_validate_entity_config(): assert vec({'media_player.demo': config}) == \ {'media_player.demo': {CONF_FEATURE_LIST: {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}}} + assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ + {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} def test_validate_media_player_features(): From 4935043f4a17fd855f2f3e29e0b963700b191273 Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Fri, 1 Jun 2018 13:41:04 -0400 Subject: [PATCH 895/924] Add battery attribute to Sensibo (#14735) * Added battery attribute * Simplify current_battery --- homeassistant/components/climate/sensibo.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index 94d9612755c..b3fff0dd796 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -154,7 +154,8 @@ class SensiboClimate(ClimateDevice): @property def device_state_attributes(self): """Return the state attributes.""" - return {ATTR_CURRENT_HUMIDITY: self.current_humidity} + return {ATTR_CURRENT_HUMIDITY: self.current_humidity, + 'battery': self.current_battery} @property def temperature_unit(self): @@ -191,6 +192,11 @@ class SensiboClimate(ClimateDevice): """Return the current humidity.""" return self._measurements['humidity'] + @property + def current_battery(self): + """Return the current battery voltage.""" + return self._measurements.get('batteryVoltage') + @property def current_temperature(self): """Return the current temperature.""" From 3b8ee196bef4e1e272dad5a7548d4f83d7e8bb59 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 19:41:20 +0200 Subject: [PATCH 896/924] Update syntax (#14742) --- homeassistant/components/counter/__init__.py | 36 ++++++++------------ 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 2df17a4e50a..03e5b273468 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -9,9 +9,9 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import (ATTR_ENTITY_ID, CONF_ICON, CONF_NAME) +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import async_get_last_state @@ -94,9 +94,8 @@ def async_reset(hass, entity_id): DOMAIN, SERVICE_RESET, {ATTR_ENTITY_ID: entity_id})) -@asyncio.coroutine -def async_setup(hass, config): - """Set up a counter.""" +async def async_setup(hass, config): + """Set up the counters.""" component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] @@ -115,8 +114,7 @@ def async_setup(hass, config): if not entities: return False - @asyncio.coroutine - def async_handler_service(service): + async def async_handler_service(service): """Handle a call to the counter services.""" target_counters = component.async_extract_from_service(service) @@ -129,7 +127,7 @@ def async_setup(hass, config): tasks = [getattr(counter, attr)() for counter in target_counters] if tasks: - yield from asyncio.wait(tasks, loop=hass.loop) + await asyncio.wait(tasks, loop=hass.loop) hass.services.async_register( DOMAIN, SERVICE_INCREMENT, async_handler_service) @@ -138,7 +136,7 @@ def async_setup(hass, config): hass.services.async_register( DOMAIN, SERVICE_RESET, async_handler_service) - yield from component.async_add_entities(entities) + await component.async_add_entities(entities) return True @@ -181,30 +179,26 @@ class Counter(Entity): ATTR_STEP: self._step, } - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" # If not None, we got an initial value. if self._state is not None: return - state = yield from async_get_last_state(self.hass, self.entity_id) + state = await async_get_last_state(self.hass, self.entity_id) self._state = state and state.state == state - @asyncio.coroutine - def async_decrement(self): + async def async_decrement(self): """Decrement the counter.""" self._state -= self._step - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_increment(self): + async def async_increment(self): """Increment a counter.""" self._state += self._step - yield from self.async_update_ha_state() + await self.async_update_ha_state() - @asyncio.coroutine - def async_reset(self): + async def async_reset(self): """Reset a counter.""" self._state = self._initial - yield from self.async_update_ha_state() + await self.async_update_ha_state() From 77dca8272c4bd41a91c1e6266b85104cd3f72a18 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 19:41:35 +0200 Subject: [PATCH 897/924] Upgrade blockchain to 1.4.4 (#14738) --- homeassistant/components/sensor/bitcoin.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/bitcoin.py b/homeassistant/components/sensor/bitcoin.py index 8bed72a67c2..38d2226012c 100644 --- a/homeassistant/components/sensor/bitcoin.py +++ b/homeassistant/components/sensor/bitcoin.py @@ -15,7 +15,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['blockchain==1.4.0'] +REQUIREMENTS = ['blockchain==1.4.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4ed33a6dc5c..c2aa16483da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ blinkstick==1.1.8 # blinkt==0.1.0 # homeassistant.components.sensor.bitcoin -blockchain==1.4.0 +blockchain==1.4.4 # homeassistant.components.light.decora # bluepy==1.1.4 From d6e76969ccd0a7943dec5d0096170d732ed16953 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 1 Jun 2018 23:33:04 +0200 Subject: [PATCH 898/924] Tweak about the requirements --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 9a8e6812cf3..c2f65f9a8be 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,7 @@ If user exposed functionality or configuration variables are added/changed: If the code communicates with devices, web services, or third-party tools: - [ ] New dependencies have been added to the `REQUIREMENTS` variable ([example][ex-requir]). - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - - [ ] New dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. + - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. If the code does not interact with devices: From cfac537f5115307519466ccc57b79b7ecb99ca99 Mon Sep 17 00:00:00 2001 From: austinlg96 Date: Sat, 2 Jun 2018 03:23:51 -0400 Subject: [PATCH 899/924] Added option to block Osram Lightify individual lights in the same way that groups can be (#14470) --- .../components/light/osramlightify.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/light/osramlightify.py b/homeassistant/components/light/osramlightify.py index 2c44620caca..939d0fe6988 100644 --- a/homeassistant/components/light/osramlightify.py +++ b/homeassistant/components/light/osramlightify.py @@ -27,8 +27,10 @@ REQUIREMENTS = ['lightify==1.0.6.1'] _LOGGER = logging.getLogger(__name__) +CONF_ALLOW_LIGHTIFY_NODES = 'allow_lightify_nodes' CONF_ALLOW_LIGHTIFY_GROUPS = 'allow_lightify_groups' +DEFAULT_ALLOW_LIGHTIFY_NODES = True DEFAULT_ALLOW_LIGHTIFY_GROUPS = True MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -40,6 +42,8 @@ SUPPORT_OSRAMLIGHTIFY = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ALLOW_LIGHTIFY_NODES, + default=DEFAULT_ALLOW_LIGHTIFY_NODES): cv.boolean, vol.Optional(CONF_ALLOW_LIGHTIFY_GROUPS, default=DEFAULT_ALLOW_LIGHTIFY_GROUPS): cv.boolean, }) @@ -50,6 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): import lightify host = config.get(CONF_HOST) + add_nodes = config.get(CONF_ALLOW_LIGHTIFY_NODES) add_groups = config.get(CONF_ALLOW_LIGHTIFY_GROUPS) try: @@ -60,10 +65,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.exception(msg) return - setup_bridge(bridge, add_devices, add_groups) + setup_bridge(bridge, add_devices, add_nodes, add_groups) -def setup_bridge(bridge, add_devices, add_groups): +def setup_bridge(bridge, add_devices, add_nodes, add_groups): """Set up the Lightify bridge.""" lights = {} @@ -80,14 +85,15 @@ def setup_bridge(bridge, add_devices, add_groups): new_lights = [] - for (light_id, light) in bridge.lights().items(): - if light_id not in lights: - osram_light = OsramLightifyLight( - light_id, light, update_lights) - lights[light_id] = osram_light - new_lights.append(osram_light) - else: - lights[light_id].light = light + if add_nodes: + for (light_id, light) in bridge.lights().items(): + if light_id not in lights: + osram_light = OsramLightifyLight( + light_id, light, update_lights) + lights[light_id] = osram_light + new_lights.append(osram_light) + else: + lights[light_id].light = light if add_groups: for (group_name, group) in bridge.groups().items(): From e7985c970b6b1b01abf665322ab6e18d731e624a Mon Sep 17 00:00:00 2001 From: Tristan Caulfield Date: Sat, 2 Jun 2018 02:30:15 -0500 Subject: [PATCH 900/924] Upgrade directpy to 0.5 (#14750) * Version Requirement bump Bump required version to 0.5 to allow component to work with Genie Mini clients using the clientAddr variable. * Ran script/gen_requirements_all.py as requested. --- homeassistant/components/media_player/directv.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/directv.py b/homeassistant/components/media_player/directv.py index 25d13e3017a..0adb02b6a65 100644 --- a/homeassistant/components/media_player/directv.py +++ b/homeassistant/components/media_player/directv.py @@ -16,7 +16,7 @@ from homeassistant.const import ( CONF_DEVICE, CONF_HOST, CONF_NAME, STATE_OFF, STATE_PLAYING, CONF_PORT) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['directpy==0.2'] +REQUIREMENTS = ['directpy==0.5'] DEFAULT_DEVICE = '0' DEFAULT_NAME = 'DirecTV Receiver' diff --git a/requirements_all.txt b/requirements_all.txt index c2aa16483da..8e9c37b75e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -249,7 +249,7 @@ deluge-client==1.4.0 denonavr==0.7.2 # homeassistant.components.media_player.directv -directpy==0.2 +directpy==0.5 # homeassistant.components.sensor.discogs discogs_client==2.2.1 From ad86e68c1eafd6109c887801d977658f737bcb5c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 12:00:01 +0200 Subject: [PATCH 901/924] Update syntax of platform random (#14767) --- homeassistant/components/binary_sensor/random.py | 8 +++----- homeassistant/components/sensor/random.py | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/random.py b/homeassistant/components/binary_sensor/random.py index 162d0480389..ab6c1e5d479 100644 --- a/homeassistant/components/binary_sensor/random.py +++ b/homeassistant/components/binary_sensor/random.py @@ -4,7 +4,6 @@ Support for showing random states. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.random/ """ -import asyncio import logging import voluptuous as vol @@ -24,8 +23,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Random binary sensor.""" name = config.get(CONF_NAME) device_class = config.get(CONF_DEVICE_CLASS) @@ -57,8 +56,7 @@ class RandomSensor(BinarySensorDevice): """Return the sensor class of the sensor.""" return self._device_class - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get new state and update the sensor's state.""" from random import getrandbits self._state = bool(getrandbits(1)) diff --git a/homeassistant/components/sensor/random.py b/homeassistant/components/sensor/random.py index e57bbcc3955..c3ff08a5781 100644 --- a/homeassistant/components/sensor/random.py +++ b/homeassistant/components/sensor/random.py @@ -4,7 +4,6 @@ Support for showing random numbers. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.random/ """ -import asyncio import logging import voluptuous as vol @@ -34,8 +33,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Random number sensor.""" name = config.get(CONF_NAME) minimum = config.get(CONF_MINIMUM) @@ -84,8 +83,7 @@ class RandomSensor(Entity): ATTR_MINIMUM: self._minimum, } - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get a new number and updates the states.""" from random import randrange self._state = randrange(self._minimum, self._maximum + 1) From fe0e49db4bfca77fa36f408bbcf32b55598863b0 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 2 Jun 2018 13:45:48 +0200 Subject: [PATCH 902/924] Update postnl api to 1.0.2 (#14769) --- homeassistant/components/sensor/postnl.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/postnl.py b/homeassistant/components/sensor/postnl.py index c38f58b7916..63a9c1d67d5 100644 --- a/homeassistant/components/sensor/postnl.py +++ b/homeassistant/components/sensor/postnl.py @@ -16,7 +16,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['postnl_api==1.0.1'] +REQUIREMENTS = ['postnl_api==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 8e9c37b75e1..4a6560133ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -654,7 +654,7 @@ pmsensor==0.4 pocketcasts==0.1 # homeassistant.components.sensor.postnl -postnl_api==1.0.1 +postnl_api==1.0.2 # homeassistant.components.climate.proliphix proliphix==0.4.1 From 875e05ff387e0fe1a99e3fad14075b13688021fb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:29:38 +0200 Subject: [PATCH 903/924] Remove swagger file (#14762) --- docs/swagger.yaml | 606 ---------------------------------------------- 1 file changed, 606 deletions(-) delete mode 100644 docs/swagger.yaml diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 488d6bddd46..00000000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,606 +0,0 @@ -swagger: '2.0' -info: - title: Home Assistant - description: Home Assistant REST API - version: "1.0.1" -# the domain of the service -host: localhost:8123 - -# array of all schemes that your API supports -schemes: - - http - - https - -securityDefinitions: - #api_key: - # type: apiKey - # description: API password - # name: api_password - # in: query - - api_key: - type: apiKey - description: API password - name: x-ha-access - in: header - -# will be prefixed to all paths -basePath: /api - -consumes: - - application/json -produces: - - application/json -paths: - /: - get: - summary: API alive message - description: Returns message if API is up and running. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: API is up and running - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /config: - get: - summary: API alive message - description: Returns the current configuration as JSON. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: Current configuration - schema: - $ref: '#/definitions/ApiConfig' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /discovery_info: - get: - summary: Basic information about Home Assistant instance - tags: - - Core - responses: - 200: - description: Basic information - schema: - $ref: '#/definitions/DiscoveryInfo' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /bootstrap: - get: - summary: Returns all data needed to bootstrap Home Assistant. - tags: - - Core - security: - - api_key: [] - responses: - 200: - description: Bootstrap information - schema: - $ref: '#/definitions/BootstrapInfo' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /events: - get: - summary: Array of event objects. - description: Returns an array of event objects. Each event object contain event name and listener count. - tags: - - Events - security: - - api_key: [] - responses: - 200: - description: Events - schema: - type: array - items: - $ref: '#/definitions/Event' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /services: - get: - summary: Array of service objects. - description: Returns an array of service objects. Each object contains the domain and which services it contains. - tags: - - Services - security: - - api_key: [] - responses: - 200: - description: Services - schema: - type: array - items: - $ref: '#/definitions/Service' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /history: - get: - summary: Array of state changes in the past. - description: Returns an array of state changes in the past. Each object contains further detail for the entities. - tags: - - State - security: - - api_key: [] - responses: - 200: - description: State changes - schema: - type: array - items: - $ref: '#/definitions/History' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /states: - get: - summary: Array of state objects. - description: | - Returns an array of state objects. Each state has the following attributes: entity_id, state, last_changed and attributes. - tags: - - State - security: - - api_key: [] - responses: - 200: - description: States - schema: - type: array - items: - $ref: '#/definitions/State' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /states/{entity_id}: - get: - summary: Specific state object. - description: | - Returns a state object for specified entity_id. - tags: - - State - security: - - api_key: [] - parameters: - - name: entity_id - in: path - description: entity_id of the entity to query - required: true - type: string - responses: - 200: - description: State - schema: - $ref: '#/definitions/State' - 404: - description: Not found - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - post: - description: | - Updates or creates the current state of an entity. - tags: - - State - consumes: - - application/json - parameters: - - name: entity_id - in: path - description: entity_id to set the state of - required: true - type: string - - $ref: '#/parameters/State' - responses: - 200: - description: State of existing entity was set - schema: - $ref: '#/definitions/State' - 201: - description: State of new entity was set - schema: - $ref: '#/definitions/State' - headers: - location: - type: string - description: location of the new entity - default: - description: Error - schema: - $ref: '#/definitions/Message' - /error_log: - get: - summary: Error log - description: | - Retrieve all errors logged during the current session of Home Assistant as a plaintext response. - tags: - - Core - security: - - api_key: [] - produces: - - text/plain - responses: - 200: - description: Plain text error log - default: - description: Error - schema: - $ref: '#/definitions/Message' - /camera_proxy/camera.{entity_id}: - get: - summary: Camera image. - description: | - Returns the data (image) from the specified camera entity_id. - tags: - - Camera - security: - - api_key: [] - produces: - - image/jpeg - parameters: - - name: entity_id - in: path - description: entity_id of the camera to query - required: true - type: string - responses: - 200: - description: Camera image - schema: - type: file - default: - description: Error - schema: - $ref: '#/definitions/Message' - /events/{event_type}: - post: - description: | - Fires an event with event_type - tags: - - Events - security: - - api_key: [] - consumes: - - application/json - parameters: - - name: event_type - in: path - description: event_type to fire event with - required: true - type: string - - $ref: '#/parameters/EventData' - responses: - 200: - description: Response message - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /services/{domain}/{service}: - post: - description: | - Calls a service within a specific domain. Will return when the service has been executed or 10 seconds has past, whichever comes first. - tags: - - Services - security: - - api_key: [] - consumes: - - application/json - parameters: - - name: domain - in: path - description: domain of the service - required: true - type: string - - name: service - in: path - description: service to call - required: true - type: string - - $ref: '#/parameters/ServiceData' - responses: - 200: - description: List of states that have changed while the service was being executed. The result will include any changed states that changed while the service was being executed, even if their change was the result of something else happening in the system. - schema: - type: array - items: - $ref: '#/definitions/State' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /template: - post: - description: | - Render a Home Assistant template. - tags: - - Template - security: - - api_key: [] - consumes: - - application/json - produces: - - text/plain - parameters: - - $ref: '#/parameters/Template' - responses: - 200: - description: Returns the rendered template in plain text. - schema: - type: string - default: - description: Error - schema: - $ref: '#/definitions/Message' - /event_forwarding: - post: - description: | - Setup event forwarding to another Home Assistant instance. - tags: - - Core - security: - - api_key: [] - consumes: - - application/json - parameters: - - $ref: '#/parameters/EventForwarding' - responses: - 200: - description: It will return a message if event forwarding was setup successful. - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - delete: - description: | - Cancel event forwarding to another Home Assistant instance. - tags: - - Core - consumes: - - application/json - parameters: - - $ref: '#/parameters/EventForwarding' - responses: - 200: - description: It will return a message if event forwarding was cancelled successful. - schema: - $ref: '#/definitions/Message' - default: - description: Error - schema: - $ref: '#/definitions/Message' - /stream: - get: - summary: Server-sent events - description: The server-sent events feature is a one-way channel from your Home Assistant server to a client which is acting as a consumer. - tags: - - Core - - Events - security: - - api_key: [] - produces: - - text/event-stream - parameters: - - name: restrict - in: query - description: comma-separated list of event_types to filter - required: false - type: string - responses: - default: - description: Stream of events - schema: - type: object - x-events: - state_changed: - type: object - properties: - entity_id: - type: string - old_state: - $ref: '#/definitions/State' - new_state: - $ref: '#/definitions/State' -definitions: - ApiConfig: - type: object - properties: - components: - type: array - description: List of component types - items: - type: string - description: Component type - latitude: - type: number - format: float - description: Latitude of Home Assistant server - longitude: - type: number - format: float - description: Longitude of Home Assistant server - location_name: - type: string - unit_system: - type: object - properties: - length: - type: string - mass: - type: string - temperature: - type: string - volume: - type: string - time_zone: - type: string - version: - type: string - DiscoveryInfo: - type: object - properties: - base_url: - type: string - location_name: - type: string - requires_api_password: - type: boolean - version: - type: string - BootstrapInfo: - type: object - properties: - config: - $ref: '#/definitions/ApiConfig' - events: - type: array - items: - $ref: '#/definitions/Event' - services: - type: array - items: - $ref: '#/definitions/Service' - states: - type: array - items: - $ref: '#/definitions/State' - Event: - type: object - properties: - event: - type: string - listener_count: - type: integer - Service: - type: object - properties: - domain: - type: string - services: - type: object - additionalProperties: - $ref: '#/definitions/DomainService' - DomainService: - type: object - properties: - description: - type: string - fields: - type: object - description: Object with service fields that can be called - State: - type: object - properties: - attributes: - $ref: '#/definitions/StateAttributes' - state: - type: string - entity_id: - type: string - last_changed: - type: string - format: date-time - StateAttributes: - type: object - additionalProperties: - type: string - History: - allOf: - - $ref: '#/definitions/State' - - type: object - properties: - last_updated: - type: string - format: date-time - Message: - type: object - properties: - message: - type: string -parameters: - State: - name: body - in: body - description: State parameter - required: false - schema: - type: object - required: - - state - properties: - attributes: - $ref: '#/definitions/StateAttributes' - state: - type: string - EventData: - name: body - in: body - description: event_data - required: false - schema: - type: object - ServiceData: - name: body - in: body - description: service_data - required: false - schema: - type: object - Template: - name: body - in: body - description: Template to render - required: true - schema: - type: object - required: - - template - properties: - template: - description: Jinja2 template string - type: string - EventForwarding: - name: body - in: body - description: Event Forwarding parameter - required: true - schema: - type: object - required: - - host - - api_password - properties: - host: - type: string - api_password: - type: string - port: - type: integer From 1ce4c2092a6dd8662d91a3cf4e5df66c5c68098c Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:30:07 +0200 Subject: [PATCH 904/924] Update syntax (#14771) --- homeassistant/components/sensor/version.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/version.py b/homeassistant/components/sensor/version.py index c19d2743563..db61d059783 100644 --- a/homeassistant/components/sensor/version.py +++ b/homeassistant/components/sensor/version.py @@ -4,7 +4,6 @@ Support for displaying the current version of Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.version/ """ -import asyncio import logging import voluptuous as vol @@ -23,8 +22,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Version sensor platform.""" name = config.get(CONF_NAME) From 74b7dabf2d38b636962ff10a28e4207bf9d446eb Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:30:54 +0200 Subject: [PATCH 905/924] Update syntax (#14768) --- homeassistant/components/sensor/worldclock.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/worldclock.py b/homeassistant/components/sensor/worldclock.py index 839b5776b3c..1240480d4a3 100644 --- a/homeassistant/components/sensor/worldclock.py +++ b/homeassistant/components/sensor/worldclock.py @@ -4,7 +4,6 @@ Support for showing the time in a different time zone. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.worldclock/ """ -import asyncio import logging import voluptuous as vol @@ -29,8 +28,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the World clock sensor.""" name = config.get(CONF_NAME) time_zone = dt_util.get_time_zone(config.get(CONF_TIME_ZONE)) @@ -62,8 +61,7 @@ class WorldClockSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the time and updates the states.""" self._state = dt_util.now(time_zone=self._time_zone).strftime( TIME_STR_FORMAT) From b86cd325fe31997a202dd67871f2f37254412997 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:31:06 +0200 Subject: [PATCH 906/924] Update syntax (#14770) --- homeassistant/components/sensor/uptime.py | 31 ++++++++++++----------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensor/uptime.py b/homeassistant/components/sensor/uptime.py index 91746af71f1..7e893899815 100644 --- a/homeassistant/components/sensor/uptime.py +++ b/homeassistant/components/sensor/uptime.py @@ -1,25 +1,25 @@ """ -Component to retrieve uptime for Home Assistant. +Platform to retrieve uptime for Home Assistant. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.uptime/ """ -import asyncio import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, CONF_UNIT_OF_MEASUREMENT) +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Uptime' +ICON = 'mdi:clock' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='days'): @@ -27,22 +27,22 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the uptime sensor platform.""" name = config.get(CONF_NAME) units = config.get(CONF_UNIT_OF_MEASUREMENT) + async_add_devices([UptimeSensor(name, units)], True) class UptimeSensor(Entity): """Representation of an uptime sensor.""" - def __init__(self, name, units): + def __init__(self, name, unit): """Initialize the uptime sensor.""" self._name = name - self._icon = 'mdi:clock' - self._units = units + self._unit = unit self.initial = dt_util.now() self._state = None @@ -54,27 +54,28 @@ class UptimeSensor(Entity): @property def icon(self): """Icon to display in the front end.""" - return self._icon + return ICON @property def unit_of_measurement(self): """Return the unit of measurement the value is expressed in.""" - return self._units + return self._unit @property def state(self): """Return the state of the sensor.""" return self._state - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the state of the sensor.""" delta = dt_util.now() - self.initial div_factor = 3600 + if self.unit_of_measurement == 'days': div_factor *= 24 elif self.unit_of_measurement == 'minutes': div_factor /= 60 + delta = delta.total_seconds() / div_factor self._state = round(delta, 2) _LOGGER.debug("New value: %s", delta) From 5aaf81f2c990e8b74948e348458b5f71f3189403 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:31:43 +0200 Subject: [PATCH 907/924] Upgrade Sphinx to 1.7.5 (#14764) --- requirements_docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index 5ef38e1537e..0556b35fc08 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.4 +Sphinx==1.7.5 sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 From f2dacb25701cb81418aae075b5d3708f72b0e1ec Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:33:48 +0200 Subject: [PATCH 908/924] Upgrade youtube_dl to 2018.06.02 (#14763) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index 73837ce2ca1..75b90b084fc 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.05.26'] +REQUIREMENTS = ['youtube_dl==2018.06.02'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 4a6560133ce..09526e3cd74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1397,7 +1397,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.05.26 +youtube_dl==2018.06.02 # homeassistant.components.light.zengge zengge==0.2 From a8413249c2c09f0d3960500417381b6a6cdb5b7a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:34:30 +0200 Subject: [PATCH 909/924] Upgrade sqlalchemy to 1.2.8 (#14765) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/sensor/sql.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 9b5bea043f4..38ba593261f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -35,7 +35,7 @@ from . import migration, purge from .const import DATA_INSTANCE from .util import session_scope -REQUIREMENTS = ['sqlalchemy==1.2.7'] +REQUIREMENTS = ['sqlalchemy==1.2.8'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/sql.py b/homeassistant/components/sensor/sql.py index b7ece1bdb87..7fefb0f450b 100644 --- a/homeassistant/components/sensor/sql.py +++ b/homeassistant/components/sensor/sql.py @@ -20,7 +20,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['sqlalchemy==1.2.7'] +REQUIREMENTS = ['sqlalchemy==1.2.8'] CONF_COLUMN_NAME = 'column' CONF_QUERIES = 'queries' diff --git a/requirements_all.txt b/requirements_all.txt index 09526e3cd74..fd11f6d48cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1251,7 +1251,7 @@ spotcrime==1.0.3 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.7 +sqlalchemy==1.2.8 # homeassistant.components.statsd statsd==3.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bbf01e1bfb4..3d870737aac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -188,7 +188,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.scripts.db_migrator # homeassistant.components.sensor.sql -sqlalchemy==1.2.7 +sqlalchemy==1.2.8 # homeassistant.components.statsd statsd==3.2.1 From 27df4cca6c43df63ad8c78cc362065961a33c809 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 14:34:47 +0200 Subject: [PATCH 910/924] Upgrade shodan to 1.8.1 (#14760) --- homeassistant/components/sensor/shodan.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/shodan.py b/homeassistant/components/sensor/shodan.py index 1cc2ba30866..bc3e127508b 100644 --- a/homeassistant/components/sensor/shodan.py +++ b/homeassistant/components/sensor/shodan.py @@ -14,7 +14,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['shodan==1.8.0'] +REQUIREMENTS = ['shodan==1.8.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fd11f6d48cf..820a77531df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ sense_energy==0.3.1 sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan -shodan==1.8.0 +shodan==1.8.1 # homeassistant.components.notify.simplepush simplepush==1.1.4 From 28ef94c3fa383830d3f3b9ce9f52565afa6f9356 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 2 Jun 2018 15:08:10 +0200 Subject: [PATCH 911/924] Update syntax (#14772) --- homeassistant/components/sensor/simulated.py | 38 +++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index ae2d4939eab..9dac0b48bc2 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -4,51 +4,48 @@ Adds a simulated sensor. For more details about this platform, refer to the documentation at https://home-assistant.io/components/sensor.simulated/ """ -import asyncio -import datetime as datetime +import logging import math from random import Random -import logging import voluptuous as vol -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = datetime.timedelta(seconds=30) -ICON = 'mdi:chart-line' -CONF_UNIT = 'unit' CONF_AMP = 'amplitude' +CONF_FWHM = 'spread' CONF_MEAN = 'mean' CONF_PERIOD = 'period' CONF_PHASE = 'phase' -CONF_FWHM = 'spread' CONF_SEED = 'seed' +CONF_UNIT = 'unit' -DEFAULT_NAME = 'simulated' -DEFAULT_UNIT = 'value' DEFAULT_AMP = 1 +DEFAULT_FWHM = 0 DEFAULT_MEAN = 0 +DEFAULT_NAME = 'simulated' DEFAULT_PERIOD = 60 DEFAULT_PHASE = 0 -DEFAULT_FWHM = 0 DEFAULT_SEED = 999 +DEFAULT_UNIT = 'value' +ICON = 'mdi:chart-line' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, vol.Optional(CONF_AMP, default=DEFAULT_AMP): vol.Coerce(float), + vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), vol.Optional(CONF_MEAN, default=DEFAULT_MEAN): vol.Coerce(float), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PERIOD, default=DEFAULT_PERIOD): cv.positive_int, vol.Optional(CONF_PHASE, default=DEFAULT_PHASE): vol.Coerce(float), - vol.Optional(CONF_FWHM, default=DEFAULT_FWHM): vol.Coerce(float), vol.Optional(CONF_SEED, default=DEFAULT_SEED): cv.positive_int, + vol.Optional(CONF_UNIT, default=DEFAULT_UNIT): cv.string, }) @@ -63,9 +60,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): fwhm = config.get(CONF_FWHM) seed = config.get(CONF_SEED) - sensor = SimulatedSensor( - name, unit, amp, mean, period, phase, fwhm, seed - ) + sensor = SimulatedSensor(name, unit, amp, mean, period, phase, fwhm, seed) add_devices([sensor], True) @@ -107,8 +102,7 @@ class SimulatedSensor(Entity): noise = self._random.gauss(mu=0, sigma=fwhm) return mean + periodic + noise - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Update the sensor.""" self._state = self.signal_calc() From 12e679c14daf9fac8c68a3e8c314cde1a980f21f Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Sat, 2 Jun 2018 18:54:48 -0700 Subject: [PATCH 912/924] Assign device class to nest sensors (#14746) * Assign device class to nest sensors sensor/nest.NestSensor => /nest.NestSensorDevice, so that BinarySensor platform do not reference Sensor platform anymore * Resolve code review comment * Follow code review comment --- .../components/binary_sensor/nest.py | 54 ++++--- homeassistant/components/nest.py | 55 ++++++- homeassistant/components/sensor/nest.py | 151 ++++++------------ 3 files changed, 130 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/binary_sensor/nest.py b/homeassistant/components/binary_sensor/nest.py index 008b6eed1e4..882ff142e8c 100644 --- a/homeassistant/components/binary_sensor/nest.py +++ b/homeassistant/components/binary_sensor/nest.py @@ -7,32 +7,31 @@ https://home-assistant.io/components/binary_sensor.nest/ from itertools import chain import logging -from homeassistant.components.binary_sensor import (BinarySensorDevice) -from homeassistant.components.nest import DATA_NEST -from homeassistant.components.sensor.nest import NestSensor +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.nest import DATA_NEST, NestSensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS DEPENDENCIES = ['nest'] -BINARY_TYPES = ['online'] +BINARY_TYPES = {'online': 'connectivity'} -CLIMATE_BINARY_TYPES = [ - 'fan', - 'is_using_emergency_heat', - 'is_locked', - 'has_leaf', -] +CLIMATE_BINARY_TYPES = { + 'fan': None, + 'is_using_emergency_heat': 'heat', + 'is_locked': None, + 'has_leaf': None, +} -CAMERA_BINARY_TYPES = [ - 'motion_detected', - 'sound_detected', - 'person_detected', -] +CAMERA_BINARY_TYPES = { + 'motion_detected': 'motion', + 'sound_detected': 'sound', + 'person_detected': 'occupancy', +} -STRUCTURE_BINARY_TYPES = [ - 'away', - # 'security_state', # wait for pending python-nest update -] +STRUCTURE_BINARY_TYPES = { + 'away': None, + # 'security_state', # pending python-nest update +} STRUCTURE_BINARY_STATE_MAP = { 'away': {'away': True, 'home': False}, @@ -50,8 +49,8 @@ _BINARY_TYPES_DEPRECATED = [ 'hvac_emer_heat_state', ] -_VALID_BINARY_SENSOR_TYPES = BINARY_TYPES + CLIMATE_BINARY_TYPES \ - + CAMERA_BINARY_TYPES + STRUCTURE_BINARY_TYPES +_VALID_BINARY_SENSOR_TYPES = {**BINARY_TYPES, **CLIMATE_BINARY_TYPES, + **CAMERA_BINARY_TYPES, **STRUCTURE_BINARY_TYPES} _LOGGER = logging.getLogger(__name__) @@ -105,7 +104,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices(sensors, True) -class NestBinarySensor(NestSensor, BinarySensorDevice): +class NestBinarySensor(NestSensorDevice, BinarySensorDevice): """Represents a Nest binary sensor.""" @property @@ -113,6 +112,11 @@ class NestBinarySensor(NestSensor, BinarySensorDevice): """Return true if the binary sensor is on.""" return self._state + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return _VALID_BINARY_SENSOR_TYPES.get(self.variable) + def update(self): """Retrieve latest state.""" value = getattr(self.device, self.variable) @@ -133,9 +137,9 @@ class NestActivityZoneSensor(NestBinarySensor): self._name = "{} {} activity".format(self._name, self.zone.name) @property - def name(self): - """Return the name of the nest, if any.""" - return self._name + def device_class(self): + """Return the device class of the binary sensor.""" + return 'motion' def update(self): """Retrieve latest state.""" diff --git a/homeassistant/components/nest.py b/homeassistant/components/nest.py index 365f0593c8d..16a0b80d1fd 100644 --- a/homeassistant/components/nest.py +++ b/homeassistant/components/nest.py @@ -15,7 +15,9 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import discovery, config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send, \ + async_dispatcher_connect +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['python-nest==4.0.1'] @@ -272,3 +274,54 @@ class NestDevice(object): except socket.error: _LOGGER.error( "Connection error logging into the nest web service.") + + +class NestSensorDevice(Entity): + """Representation of a Nest sensor.""" + + def __init__(self, structure, device, variable): + """Initialize the sensor.""" + self.structure = structure + self.variable = variable + + if device is not None: + # device specific + self.device = device + self._name = "{} {}".format(self.device.name_long, + self.variable.replace('_', ' ')) + else: + # structure only + self.device = structure + self._name = "{} {}".format(self.structure.name, + self.variable.replace('_', ' ')) + + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + def update(self): + """Do not use NestSensorDevice directly.""" + raise NotImplementedError + + async def async_added_to_hass(self): + """Register update signal handler.""" + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, + async_update_state) diff --git a/homeassistant/components/sensor/nest.py b/homeassistant/components/sensor/nest.py index 46a2206a9f7..00d18c7fe10 100644 --- a/homeassistant/components/sensor/nest.py +++ b/homeassistant/components/sensor/nest.py @@ -4,49 +4,44 @@ Support for Nest Thermostat Sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.nest/ """ -from itertools import chain import logging -from homeassistant.components.nest import DATA_NEST, SIGNAL_NEST_UPDATE -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.components.nest import DATA_NEST, NestSensorDevice from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_MONITORED_CONDITIONS, - DEVICE_CLASS_TEMPERATURE) + DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY) DEPENDENCIES = ['nest'] -SENSOR_TYPES = ['humidity', - 'operation_mode', - 'hvac_state'] +SENSOR_TYPES = ['humidity', 'operation_mode', 'hvac_state'] -SENSOR_TYPES_DEPRECATED = ['last_ip', - 'local_ip', - 'last_connection'] +TEMP_SENSOR_TYPES = ['temperature', 'target'] -DEPRECATED_WEATHER_VARS = {'weather_humidity': 'humidity', - 'weather_temperature': 'temperature', - 'weather_condition': 'condition', - 'wind_speed': 'kph', - 'wind_direction': 'direction'} - -SENSOR_UNITS = {'humidity': '%', 'temperature': '°C'} - -PROTECT_VARS = ['co_status', 'smoke_status', 'battery_health'] - -PROTECT_VARS_DEPRECATED = ['battery_level'] - -SENSOR_TEMP_TYPES = ['temperature', 'target'] +PROTECT_SENSOR_TYPES = ['co_status', 'smoke_status', 'battery_health'] STRUCTURE_SENSOR_TYPES = ['eta'] +_VALID_SENSOR_TYPES = SENSOR_TYPES + TEMP_SENSOR_TYPES + PROTECT_SENSOR_TYPES \ + + STRUCTURE_SENSOR_TYPES + +SENSOR_UNITS = {'humidity': '%'} + +SENSOR_DEVICE_CLASSES = {'humidity': DEVICE_CLASS_HUMIDITY} + VARIABLE_NAME_MAPPING = {'eta': 'eta_begin', 'operation_mode': 'mode'} -_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED \ - + list(DEPRECATED_WEATHER_VARS.keys()) + PROTECT_VARS_DEPRECATED +SENSOR_TYPES_DEPRECATED = ['last_ip', + 'local_ip', + 'last_connection', + 'battery_level'] -_VALID_SENSOR_TYPES = SENSOR_TYPES + SENSOR_TEMP_TYPES + PROTECT_VARS \ - + STRUCTURE_SENSOR_TYPES +DEPRECATED_WEATHER_VARS = ['weather_humidity', + 'weather_temperature', + 'weather_condition', + 'wind_speed', + 'wind_direction'] + +_SENSOR_TYPES_DEPRECATED = SENSOR_TYPES_DEPRECATED + DEPRECATED_WEATHER_VARS _LOGGER = logging.getLogger(__name__) @@ -76,7 +71,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): "monitored_conditions. See " "https://home-assistant.io/components/" "binary_sensor.nest/ for valid options.") - _LOGGER.error(wstr) all_sensors = [] @@ -84,70 +78,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): all_sensors += [NestBasicSensor(structure, None, variable) for variable in conditions if variable in STRUCTURE_SENSOR_TYPES] - for structure, device in chain(nest.thermostats(), nest.smoke_co_alarms()): - sensors = [NestBasicSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TYPES and device.is_thermostat] - sensors += [NestTempSensor(structure, device, variable) - for variable in conditions - if variable in SENSOR_TEMP_TYPES and device.is_thermostat] - sensors += [NestProtectSensor(structure, device, variable) - for variable in conditions - if variable in PROTECT_VARS and device.is_smoke_co_alarm] - all_sensors.extend(sensors) + + for structure, device in nest.thermostats(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in SENSOR_TYPES] + all_sensors += [NestTempSensor(structure, device, variable) + for variable in conditions + if variable in TEMP_SENSOR_TYPES] + + for structure, device in nest.smoke_co_alarms(): + all_sensors += [NestBasicSensor(structure, device, variable) + for variable in conditions + if variable in PROTECT_SENSOR_TYPES] add_devices(all_sensors, True) -class NestSensor(Entity): - """Representation of a Nest sensor.""" - - def __init__(self, structure, device, variable): - """Initialize the sensor.""" - self.structure = structure - self.variable = variable - - if device is not None: - # device specific - self.device = device - self._location = self.device.where - self._name = "{} {}".format(self.device.name_long, - self.variable.replace('_', ' ')) - else: - # structure only - self.device = structure - self._name = "{} {}".format(self.structure.name, - self.variable.replace('_', ' ')) - - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - - async def async_added_to_hass(self): - """Register update signal handler.""" - async def async_update_state(): - """Update sensor state.""" - await self.async_update_ha_state(True) - - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, - async_update_state) - - -class NestBasicSensor(NestSensor): +class NestBasicSensor(NestSensorDevice): """Representation a basic Nest sensor.""" @property @@ -155,18 +103,26 @@ class NestBasicSensor(NestSensor): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_DEVICE_CLASSES.get(self.variable) + def update(self): """Retrieve latest state.""" - self._unit = SENSOR_UNITS.get(self.variable, None) + self._unit = SENSOR_UNITS.get(self.variable) if self.variable in VARIABLE_NAME_MAPPING: self._state = getattr(self.device, VARIABLE_NAME_MAPPING[self.variable]) + elif self.variable in PROTECT_SENSOR_TYPES: + # keep backward compatibility + self._state = getattr(self.device, self.variable).capitalize() else: self._state = getattr(self.device, self.variable) -class NestTempSensor(NestSensor): +class NestTempSensor(NestSensorDevice): """Representation of a Nest Temperature sensor.""" @property @@ -195,16 +151,3 @@ class NestTempSensor(NestSensor): self._state = "%s-%s" % (int(low), int(high)) else: self._state = round(temp, 1) - - -class NestProtectSensor(NestSensor): - """Return the state of nest protect.""" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - def update(self): - """Retrieve latest state.""" - self._state = getattr(self.device, self.variable).capitalize() From 1ac3f0da639c90bc83a2db9cbd357e42380e3c69 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 3 Jun 2018 11:54:03 +0200 Subject: [PATCH 913/924] Ignore the mistaken long_click event of the 86sw (Closes: #14694) (#14785) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 1c0b903d868..72a4cfdfbaa 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -330,6 +330,8 @@ class XiaomiButton(XiaomiBinarySensor): click_type = 'both' elif value == 'shake': click_type = 'shake' + elif value == 'long_click': + return False else: _LOGGER.warning("Unsupported click_type detected: %s", value) return False From 7f59a8ea0c95d4545542f13f2225df2401eb882b Mon Sep 17 00:00:00 2001 From: Jason Woodford Date: Sun, 3 Jun 2018 06:55:49 -0500 Subject: [PATCH 914/924] Update total-connect-client to 0.18 for Honeywell Lynx Touch-Wifi support (#14778) --- homeassistant/components/alarm_control_panel/totalconnect.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 1f383e32f92..674eac97f8c 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS) -REQUIREMENTS = ['total_connect_client==0.17'] +REQUIREMENTS = ['total_connect_client==0.18'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 820a77531df..5836d0f820c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1299,7 +1299,7 @@ todoist-python==7.0.17 toonlib==1.0.2 # homeassistant.components.alarm_control_panel.totalconnect -total_connect_client==0.17 +total_connect_client==0.18 # homeassistant.components.sensor.transmission # homeassistant.components.switch.transmission From 919b431a2459dd36d21cffc498f345a30c1edce7 Mon Sep 17 00:00:00 2001 From: quthla Date: Sun, 3 Jun 2018 15:26:23 +0200 Subject: [PATCH 915/924] Add Kodi OnResume event (#14790) --- homeassistant/components/media_player/kodi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 770d57b5b8e..2322f966eae 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -294,6 +294,7 @@ class KodiDevice(MediaPlayerDevice): # Register notification listeners self._ws_server.Player.OnPause = self.async_on_speed_event self._ws_server.Player.OnPlay = self.async_on_speed_event + self._ws_server.Player.OnResume = self.async_on_speed_event self._ws_server.Player.OnSpeedChanged = self.async_on_speed_event self._ws_server.Player.OnStop = self.async_on_stop self._ws_server.Application.OnVolumeChanged = \ From e35d4beb95a660769eb28dcc4a7b573e6e247ab1 Mon Sep 17 00:00:00 2001 From: quthla Date: Sun, 3 Jun 2018 15:27:17 +0200 Subject: [PATCH 916/924] Fix media_title empty when title is empty but label is set (#14791) --- homeassistant/components/media_player/kodi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 2322f966eae..68a9da55ae4 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -542,8 +542,8 @@ class KodiDevice(MediaPlayerDevice): def media_title(self): """Title of current playing media.""" # find a string we can use as a title - return self._item.get( - 'title', self._item.get('label', self._item.get('file'))) + item = self._item + return item.get('title') or item.get('label') or item.get('file') @property def media_series_title(self): From 70edb2492ac9ac5de34c05e04ed5b684926e7cd1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jun 2018 12:29:57 -0400 Subject: [PATCH 917/924] Version bump to 20180603.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index fca9f33578a..5dad77f64ce 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180601.0'] +REQUIREMENTS = ['home-assistant-frontend==20180603.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 5836d0f820c..fd2bb5b4f5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180601.0 +home-assistant-frontend==20180603.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d870737aac..47f54954cd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180601.0 +home-assistant-frontend==20180603.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 8f696193f08e6642adb4d504f8a2f8052727113b Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Sun, 3 Jun 2018 18:48:51 +0200 Subject: [PATCH 918/924] Add homematicip_cloud illuminance sensor (#14720) * Add iluminance sensor and device_class for sensors * Fix lint --- .../components/sensor/homematicip_cloud.py | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/homematicip_cloud.py b/homeassistant/components/sensor/homematicip_cloud.py index aa350f7be5d..ccd1949cc3b 100644 --- a/homeassistant/components/sensor/homematicip_cloud.py +++ b/homeassistant/components/sensor/homematicip_cloud.py @@ -10,7 +10,9 @@ import logging from homeassistant.components.homematicip_cloud import ( HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, ATTR_HOME_ID) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE) _LOGGER = logging.getLogger(__name__) @@ -36,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_devices, """Set up the HomematicIP sensors devices.""" from homematicip.device import ( HeatingThermostat, TemperatureHumiditySensorWithoutDisplay, - TemperatureHumiditySensorDisplay) + TemperatureHumiditySensorDisplay, MotionDetectorIndoor) if discovery_info is None: return @@ -50,6 +52,8 @@ async def async_setup_platform(hass, config, async_add_devices, TemperatureHumiditySensorWithoutDisplay)): devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipHumiditySensor(home, device)) + if isinstance(device, MotionDetectorIndoor): + devices.append(HomematicipIlluminanceSensor(home, device)) if devices: async_add_devices(devices) @@ -149,6 +153,11 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): """Initialize the thermometer device.""" super().__init__(home, device, 'Humidity') + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_HUMIDITY + @property def icon(self): """Return the icon.""" @@ -172,6 +181,11 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): """Initialize the thermometer device.""" super().__init__(home, device, 'Temperature') + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_TEMPERATURE + @property def icon(self): """Return the icon.""" @@ -186,3 +200,26 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): def unit_of_measurement(self): """Return the unit this state is expressed in.""" return TEMP_CELSIUS + + +class HomematicipIlluminanceSensor(HomematicipGenericDevice): + """MomematicIP the thermometer device.""" + + def __init__(self, home, device): + """Initialize the device.""" + super().__init__(home, device, 'Illuminance') + + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_ILLUMINANCE + + @property + def state(self): + """Return the state.""" + return self._device.illumination + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return 'lx' From 855ed2b4e423147ce07fa69d6295ba561e6cb349 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jun 2018 16:54:23 -0400 Subject: [PATCH 919/924] Version bump to 0.72.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4c9757b3260..5644c3d0a1f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 71 +MINOR_VERSION = 72 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From aec425d1f6ff35dabae71a296d1a4d64729265b2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sun, 3 Jun 2018 22:01:48 +0100 Subject: [PATCH 920/924] Weather Platform - IPMA (#14716) * initial commit * lint * update with pyipma * Added test * Added test * lint * missing dep * address comments * lint * make sure list is iterable * don't bother with list * mock dependency * no need to add test requirements * last correction --- homeassistant/components/weather/ipma.py | 172 +++++++++++++++++++++++ requirements_all.txt | 3 + tests/components/weather/test_ipma.py | 85 +++++++++++ 3 files changed, 260 insertions(+) create mode 100644 homeassistant/components/weather/ipma.py create mode 100644 tests/components/weather/test_ipma.py diff --git a/homeassistant/components/weather/ipma.py b/homeassistant/components/weather/ipma.py new file mode 100644 index 00000000000..ef4f1b349d7 --- /dev/null +++ b/homeassistant/components/weather/ipma.py @@ -0,0 +1,172 @@ +""" +Support for IPMA weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.ipma/ +""" +import logging +from datetime import timedelta + +import async_timeout +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) +from homeassistant.const import \ + CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['pyipma==1.1.3'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Instituto Português do Mar e Atmosfera' + +ATTR_WEATHER_DESCRIPTION = "description" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +CONDITION_CLASSES = { + 'cloudy': [4, 5, 24, 25, 27], + 'fog': [16, 17, 26], + 'hail': [21, 22], + 'lightning': [19], + 'lightning-rainy': [20, 23], + 'partlycloudy': [2, 3], + 'pouring': [8, 11], + 'rainy': [6, 7, 9, 10, 12, 13, 14, 15], + 'snowy': [18], + 'snowy-rainy': [], + 'sunny': [1], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the ipma platform.""" + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + if None in (latitude, longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return + + from pyipma import Station + + websession = async_get_clientsession(hass) + with async_timeout.timeout(10, loop=hass.loop): + station = await Station.get(websession, float(latitude), + float(longitude)) + + _LOGGER.debug("Initializing ipma weather: coordinates %s, %s", + latitude, longitude) + + async_add_devices([IPMAWeather(station, config)], True) + + +class IPMAWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, station, config): + """Initialise the platform with a data instance and station name.""" + self._station_name = config.get(CONF_NAME, station.local) + self._station = station + self._condition = None + self._forecast = None + self._description = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition and Forecast.""" + with async_timeout.timeout(10, loop=self.hass.loop): + self._condition = await self._station.observation() + self._forecast = await self._station.forecast() + self._description = self._forecast[0].description + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self._station_name + + @property + def condition(self): + """Return the current condition.""" + return next((k for k, v in CONDITION_CLASSES.items() + if self._forecast[0].idWeatherType in v), None) + + @property + def temperature(self): + """Return the current temperature.""" + return self._condition.temperature + + @property + def pressure(self): + """Return the current pressure.""" + return self._condition.pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + return self._condition.humidity + + @property + def wind_speed(self): + """Return the current windspeed.""" + return self._condition.windspeed + + @property + def wind_bearing(self): + """Return the current wind bearing (degrees).""" + return self._condition.winddirection + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def forecast(self): + """Return the forecast array.""" + if self._forecast: + fcdata_out = [] + for data_in in self._forecast: + data_out = {} + data_out[ATTR_FORECAST_TIME] = data_in.forecastDate + data_out[ATTR_FORECAST_CONDITION] =\ + next((k for k, v in CONDITION_CLASSES.items() + if int(data_in.idWeatherType) in v), None) + data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin + data_out[ATTR_FORECAST_TEMP] = data_in.tMax + data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb + + fcdata_out.append(data_out) + + return fcdata_out + + @property + def device_state_attributes(self): + """Return the state attributes.""" + data = dict() + + if self._description: + data[ATTR_WEATHER_DESCRIPTION] = self._description + + return data diff --git a/requirements_all.txt b/requirements_all.txt index fd2bb5b4f5a..fcd1e3726e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,6 +834,9 @@ pyialarm==0.2 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 +# homeassistant.components.weather.ipma +pyipma==1.1.3 + # homeassistant.components.sensor.irish_rail_transport pyirishrail==0.0.2 diff --git a/tests/components/weather/test_ipma.py b/tests/components/weather/test_ipma.py new file mode 100644 index 00000000000..7df6166a2b6 --- /dev/null +++ b/tests/components/weather/test_ipma.py @@ -0,0 +1,85 @@ +"""The tests for the IPMA weather component.""" +import unittest +from unittest.mock import patch +from collections import namedtuple + +from homeassistant.components import weather +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED) +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant, MockDependency + + +class MockStation(): + """Mock Station from pyipma.""" + + @classmethod + async def get(cls, websession, lat, lon): + """Mock Factory.""" + return MockStation() + + async def observation(self): + """Mock Observation.""" + Observation = namedtuple('Observation', ['temperature', 'humidity', + 'windspeed', 'winddirection', + 'precipitation', 'pressure', + 'description']) + + return Observation(18, 71.0, 3.94, 'NW', 0, 1000.0, '---') + + async def forecast(self): + """Mock Forecast.""" + Forecast = namedtuple('Forecast', ['precipitaProb', 'tMin', 'tMax', + 'predWindDir', 'idWeatherType', + 'classWindSpeed', 'longitude', + 'forecastDate', 'classPrecInt', + 'latitude', 'description']) + + return [Forecast(73.0, 13.7, 18.7, 'NW', 6, 2, -8.64, + '2018-05-31', 2, 40.61, + 'Aguaceiros, com vento Moderado de Noroeste')] + + @property + def local(self): + """Mock location.""" + return "HomeTown" + + +class TestIPMA(unittest.TestCase): + """Test the IPMA weather component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = METRIC_SYSTEM + self.lat = self.hass.config.latitude = 40.00 + self.lon = self.hass.config.longitude = -8.00 + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + @MockDependency("pyipma") + @patch("pyipma.Station", new=MockStation) + def test_setup(self, mock_pyipma): + """Test for successfully setting up the IPMA platform.""" + self.assertTrue(setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeTown', + 'platform': 'ipma', + } + })) + + state = self.hass.states.get('weather.hometown') + self.assertEqual(state.state, 'rainy') + + data = state.attributes + self.assertEqual(data.get(ATTR_WEATHER_TEMPERATURE), 18.0) + self.assertEqual(data.get(ATTR_WEATHER_HUMIDITY), 71) + self.assertEqual(data.get(ATTR_WEATHER_PRESSURE), 1000.0) + self.assertEqual(data.get(ATTR_WEATHER_WIND_SPEED), 3.94) + self.assertEqual(data.get(ATTR_WEATHER_WIND_BEARING), 'NW') + self.assertEqual(state.attributes.get('friendly_name'), 'HomeTown') From 39843a73de1b8e2a42be1e9300580726baecd18f Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Mon, 4 Jun 2018 07:39:50 +0200 Subject: [PATCH 921/924] Add additional 86sw model identifier of the LAN protocol V2 (#14799) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 4 ++-- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 72a4cfdfbaa..ebdcdc6ca70 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -43,10 +43,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): data_key = 'channel_0' devices.append(XiaomiButton(device, 'Switch', data_key, hass, gateway)) - elif model in ['86sw1', 'sensor_86sw1.aq1']: + elif model in ['86sw1', 'sensor_86sw1', 'sensor_86sw1.aq1']: devices.append(XiaomiButton(device, 'Wall Switch', 'channel_0', hass, gateway)) - elif model in ['86sw2', 'sensor_86sw2.aq1']: + elif model in ['86sw2', 'sensor_86sw2', 'sensor_86sw2.aq1']: devices.append(XiaomiButton(device, 'Wall Switch (Left)', 'channel_0', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Right)', diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index ae3a4e0be72..2090f522709 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.4'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.5'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index fcd1e3726e9..00ed2f88cb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.4 +PyXiaomiGateway==0.9.5 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 1d23f7f9003e9fa88e5554aa9027e3a975f981ad Mon Sep 17 00:00:00 2001 From: quthla Date: Mon, 4 Jun 2018 13:24:28 +0200 Subject: [PATCH 922/924] Allow Kodi live streams to be recognized as paused (#14623) --- homeassistant/components/media_player/kodi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/kodi.py b/homeassistant/components/media_player/kodi.py index 68a9da55ae4..7fa8d5b3fe8 100644 --- a/homeassistant/components/media_player/kodi.py +++ b/homeassistant/components/media_player/kodi.py @@ -393,7 +393,7 @@ class KodiDevice(MediaPlayerDevice): if not self._players: return STATE_IDLE - if self._properties['speed'] == 0 and not self._properties['live']: + if self._properties['speed'] == 0: return STATE_PAUSED return STATE_PLAYING From bd1b1a9ff9dccf8f3000cec1888aa094b28a0c71 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Jun 2018 14:44:55 +0200 Subject: [PATCH 923/924] Update syntax (#14812) --- homeassistant/components/sensor/moon.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensor/moon.py b/homeassistant/components/sensor/moon.py index 75b8a1f72bd..0c57c98c0af 100644 --- a/homeassistant/components/sensor/moon.py +++ b/homeassistant/components/sensor/moon.py @@ -4,7 +4,6 @@ Support for tracking the moon phases. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.moon/ """ -import asyncio import logging import voluptuous as vol @@ -26,8 +25,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the Moon sensor.""" name = config.get(CONF_NAME) @@ -71,8 +70,7 @@ class MoonSensor(Entity): """Icon to use in the frontend, if any.""" return ICON - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Get the time and updates the states.""" from astral import Astral From 816efa02d1487daae1f2253060513f1ace7a9710 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 4 Jun 2018 18:49:26 +0200 Subject: [PATCH 924/924] Use pihole module to get data (#14809) --- homeassistant/components/sensor/pi_hole.py | 148 +++++++++++---------- requirements_all.txt | 3 + 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/sensor/pi_hole.py b/homeassistant/components/sensor/pi_hole.py index 027c12569a6..8e8c784e68b 100644 --- a/homeassistant/components/sensor/pi_hole.py +++ b/homeassistant/components/sensor/pi_hole.py @@ -1,23 +1,26 @@ """ -Support for getting statistical data from a Pi-Hole system. +Support for getting statistical data from a Pi-hole system. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.pi_hole/ """ -import logging -import json from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_SSL, CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS) + CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +REQUIREMENTS = ['pihole==0.1.2'] _LOGGER = logging.getLogger(__name__) -_ENDPOINT = '/api.php' ATTR_BLOCKED_DOMAINS = 'domains_blocked' ATTR_PERCENTAGE_TODAY = 'percentage_today' @@ -32,25 +35,27 @@ DEFAULT_NAME = 'Pi-Hole' DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True -SCAN_INTERVAL = timedelta(minutes=5) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MONITORED_CONDITIONS = { - 'dns_queries_today': ['DNS Queries Today', - 'queries', 'mdi:comment-question-outline'], - 'ads_blocked_today': ['Ads Blocked Today', - 'ads', 'mdi:close-octagon-outline'], - 'ads_percentage_today': ['Ads Percentage Blocked Today', - '%', 'mdi:close-octagon-outline'], - 'domains_being_blocked': ['Domains Blocked', - 'domains', 'mdi:block-helper'], - 'queries_cached': ['DNS Queries Cached', - 'queries', 'mdi:comment-question-outline'], - 'queries_forwarded': ['DNS Queries Forwarded', - 'queries', 'mdi:comment-question-outline'], - 'unique_clients': ['DNS Unique Clients', - 'clients', 'mdi:account-outline'], - 'unique_domains': ['DNS Unique Domains', - 'domains', 'mdi:domain'], + 'ads_blocked_today': + ['Ads Blocked Today', 'ads', 'mdi:close-octagon-outline'], + 'ads_percentage_today': + ['Ads Percentage Blocked Today', '%', 'mdi:close-octagon-outline'], + 'clients_ever_seen': + ['Seen Clients', 'clients', 'mdi:account-outline'], + 'dns_queries_today': + ['DNS Queries Today', 'queries', 'mdi:comment-question-outline'], + 'domains_being_blocked': + ['Domains Blocked', 'domains', 'mdi:block-helper'], + 'queries_cached': + ['DNS Queries Cached', 'queries', 'mdi:comment-question-outline'], + 'queries_forwarded': + ['DNS Queries Forwarded', 'queries', 'mdi:comment-question-outline'], + 'unique_clients': + ['DNS Unique Clients', 'clients', 'mdi:account-outline'], + 'unique_domains': + ['DNS Unique Domains', 'domains', 'mdi:domain'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -65,100 +70,105 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up the Pi-Hole sensor.""" +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the Pi-hole sensor.""" + from pihole import PiHole + name = config.get(CONF_NAME) host = config.get(CONF_HOST) - use_ssl = config.get(CONF_SSL) + use_tls = config.get(CONF_SSL) location = config.get(CONF_LOCATION) - verify_ssl = config.get(CONF_VERIFY_SSL) + verify_tls = config.get(CONF_VERIFY_SSL) - api = PiHoleAPI('{}/{}'.format(host, location), use_ssl, verify_ssl) + session = async_get_clientsession(hass) + pi_hole = PiHoleData(PiHole( + host, hass.loop, session, location=location, tls=use_tls, + verify_tls=verify_tls)) - sensors = [PiHoleSensor(hass, api, name, condition) + await pi_hole.async_update() + + if pi_hole.api.data is None: + raise PlatformNotReady + + sensors = [PiHoleSensor(pi_hole, name, condition) for condition in config[CONF_MONITORED_CONDITIONS]] - add_devices(sensors, True) + async_add_devices(sensors, True) class PiHoleSensor(Entity): - """Representation of a Pi-Hole sensor.""" + """Representation of a Pi-hole sensor.""" - def __init__(self, hass, api, name, variable): - """Initialize a Pi-Hole sensor.""" - self._hass = hass - self._api = api + def __init__(self, pi_hole, name, condition): + """Initialize a Pi-hole sensor.""" + self.pi_hole = pi_hole self._name = name - self._var_id = variable + self._condition = condition - variable_info = MONITORED_CONDITIONS[variable] - self._var_name = variable_info[0] - self._var_units = variable_info[1] - self._var_icon = variable_info[2] + variable_info = MONITORED_CONDITIONS[condition] + self._condition_name = variable_info[0] + self._unit_of_measurement = variable_info[1] + self._icon = variable_info[2] + self.data = {} @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._var_name) + return "{} {}".format(self._name, self._condition_name) @property def icon(self): """Icon to use in the frontend, if any.""" - return self._var_icon + return self._icon @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._var_units + return self._unit_of_measurement - # pylint: disable=no-member @property def state(self): """Return the state of the device.""" try: - return round(self._api.data[self._var_id], 2) + return round(self.data[self._condition], 2) except TypeError: - return self._api.data[self._var_id] + return self.data[self._condition] - # pylint: disable=no-member @property def device_state_attributes(self): """Return the state attributes of the Pi-Hole.""" return { - ATTR_BLOCKED_DOMAINS: self._api.data['domains_being_blocked'], + ATTR_BLOCKED_DOMAINS: self.data['domains_being_blocked'], } @property def available(self): """Could the device be accessed during the last update call.""" - return self._api.available + return self.pi_hole.available - def update(self): - """Get the latest data from the Pi-Hole API.""" - self._api.update() + async def async_update(self): + """Get the latest data from the Pi-hole API.""" + await self.pi_hole.async_update() + self.data = self.pi_hole.api.data -class PiHoleAPI(object): +class PiHoleData(object): """Get the latest data and update the states.""" - def __init__(self, host, use_ssl, verify_ssl): + def __init__(self, api): """Initialize the data object.""" - from homeassistant.components.sensor.rest import RestData - - uri_scheme = 'https://' if use_ssl else 'http://' - resource = "{}{}{}".format(uri_scheme, host, _ENDPOINT) - - self._rest = RestData('GET', resource, None, None, None, verify_ssl) - self.data = None + self.api = api self.available = True - self.update() - def update(self): - """Get the latest data from the Pi-Hole.""" + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest data from the Pi-hole.""" + from pihole.exceptions import PiHoleError + try: - self._rest.update() - self.data = json.loads(self._rest.data) + await self.api.get_data() self.available = True - except TypeError: - _LOGGER.error("Unable to fetch data from Pi-Hole") + except PiHoleError: + _LOGGER.error("Unable to fetch data from Pi-hole") self.available = False diff --git a/requirements_all.txt b/requirements_all.txt index 00ed2f88cb7..59cec2c1e6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -633,6 +633,9 @@ pifacedigitalio==3.0.5 # homeassistant.components.light.piglow piglow==1.2.4 +# homeassistant.components.sensor.pi_hole +pihole==0.1.2 + # homeassistant.components.pilight pilight==0.1.1