From 723d00d33a3a7279ebef5ff5c7278cfe901a0019 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Apr 2019 10:49:25 -0700 Subject: [PATCH 001/346] Bumped version to 0.93.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e4f1ac95af4..9176c1b8939 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 = 92 +MINOR_VERSION = 93 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 0afa01609c59fe585012f53ba11e91ea7f4c0c4e Mon Sep 17 00:00:00 2001 From: Ingo Theiss Date: Wed, 17 Apr 2019 21:35:16 +0200 Subject: [PATCH 002/346] Pass configured host string instead of always forcing an ip-address (#23164) * Pass host string instead of forcing an ip-address Pass the configured host (https://www.home-assistant.io/components/homematic/#host) instead of always forcing an ip-address. This is required to get SSL certificate validation working. * Remove unused 'socket' import --- homeassistant/components/homematic/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 747b23bb970..578fae064f8 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -2,7 +2,6 @@ from datetime import timedelta from functools import partial import logging -import socket import voluptuous as vol @@ -263,7 +262,7 @@ def setup(hass, config): # Create hosts-dictionary for pyhomematic for rname, rconfig in conf[CONF_INTERFACES].items(): remotes[rname] = { - 'ip': socket.gethostbyname(rconfig.get(CONF_HOST)), + 'ip': rconfig.get(CONF_HOST), 'port': rconfig.get(CONF_PORT), 'path': rconfig.get(CONF_PATH), 'resolvenames': rconfig.get(CONF_RESOLVENAMES), @@ -279,7 +278,7 @@ def setup(hass, config): for sname, sconfig in conf[CONF_HOSTS].items(): remotes[sname] = { - 'ip': socket.gethostbyname(sconfig.get(CONF_HOST)), + 'ip': sconfig.get(CONF_HOST), 'port': DEFAULT_PORT, 'username': sconfig.get(CONF_USERNAME), 'password': sconfig.get(CONF_PASSWORD), From fa73b8e37a508c3c43df61643e19fec94d774ecf Mon Sep 17 00:00:00 2001 From: zewelor Date: Wed, 17 Apr 2019 22:05:49 +0200 Subject: [PATCH 003/346] Make less imports from yeelight (#23124) --- homeassistant/components/yeelight/__init__.py | 7 +++---- homeassistant/components/yeelight/light.py | 19 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 8c2c9c957c6..dd89ed27f53 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -184,15 +184,14 @@ class YeelightDevice: def bulb(self): """Return bulb device.""" if self._bulb_device is None: - import yeelight + from yeelight import Bulb, BulbException try: - self._bulb_device = yeelight.Bulb(self._ipaddr, - model=self._model) + self._bulb_device = Bulb(self._ipaddr, model=self._model) # force init for type self.update() self._available = True - except yeelight.BulbException as ex: + except BulbException as ex: self._available = False _LOGGER.error("Failed to connect to bulb %s, %s: %s", self._ipaddr, self._name, ex) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index fa62bdc35d7..8d48e695b31 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -92,12 +92,12 @@ def _transitions_config_parser(transitions): def _parse_custom_effects(effects_config): - import yeelight + from yeelight import Flow effects = {} for config in effects_config: params = config[CONF_FLOW_PARAMS] - action = yeelight.Flow.actions[params[ATTR_ACTION]] + action = Flow.actions[params[ATTR_ACTION]] transitions = _transitions_config_parser( params[ATTR_TRANSITIONS]) @@ -113,11 +113,11 @@ def _parse_custom_effects(effects_config): def _cmd(func): """Define a wrapper to catch exceptions from the bulb.""" def _wrap(self, *args, **kwargs): - import yeelight + from yeelight import BulbException try: _LOGGER.debug("Calling %s with %s %s", func, args, kwargs) return func(self, *args, **kwargs) - except yeelight.BulbException as ex: + except BulbException as ex: _LOGGER.error("Error when calling %s: %s", func, ex) return _wrap @@ -347,15 +347,14 @@ class YeelightLight(Light): def update(self) -> None: """Update properties from the bulb.""" - import yeelight + from yeelight import BulbType, enums bulb_type = self._bulb.bulb_type - if bulb_type == yeelight.BulbType.Color: + if bulb_type == BulbType.Color: self._supported_features = SUPPORT_YEELIGHT_RGB - elif self.light_type == yeelight.enums.LightType.Ambient: + elif self.light_type == enums.LightType.Ambient: self._supported_features = SUPPORT_YEELIGHT_RGB - elif bulb_type in (yeelight.BulbType.WhiteTemp, - yeelight.BulbType.WhiteTempMood): + elif bulb_type in (BulbType.WhiteTemp, BulbType.WhiteTempMood): if self._is_nightlight_enabled: self._supported_features = SUPPORT_YEELIGHT else: @@ -368,7 +367,7 @@ class YeelightLight(Light): self._max_mireds = \ kelvin_to_mired(model_specs['color_temp']['min']) - if bulb_type == yeelight.BulbType.WhiteTempMood: + if bulb_type == BulbType.WhiteTempMood: self._is_on = self._get_property('main_power') == 'on' else: self._is_on = self._get_property('power') == 'on' From f6349a6cf4cb27f3b64953478ec851d95f7f88d2 Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Wed, 17 Apr 2019 21:48:17 +0100 Subject: [PATCH 004/346] Kill bluetooth LE scanning gracefully when asked to shut down. (#22586) * Kill bluetooth LE scanning gracefully when asked to shut down. * Add missing argument. * Refactor to use data instead of passing nonlocal variables about. * Fix typo. --- .../bluetooth_le_tracker/device_tracker.py | 15 +++++++++++++++ .../components/bluetooth_le_tracker/manifest.json | 2 +- homeassistant/components/skybeacon/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f1aab4e1fd5..f24b943f188 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -6,10 +6,13 @@ from homeassistant.components.device_tracker import ( YAML_DEVICES, CONF_TRACK_NEW, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, load_config, SOURCE_TYPE_BLUETOOTH_LE ) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) +DATA_BLE = 'BLE' +DATA_BLE_ADAPTER = 'ADAPTER' BLE_PREFIX = 'BLE_' MIN_SEEN_NEW = 5 @@ -19,6 +22,17 @@ def setup_scanner(hass, config, see, discovery_info=None): # pylint: disable=import-error import pygatt new_devices = {} + hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) + + async def async_stop(event): + """Try to shut down the bluetooth child process nicely.""" + # These should never be unset at the point this runs, but just for + # safety's sake, use `get`. + adapter = hass.data.get(DATA_BLE, {}).get(DATA_BLE_ADAPTER) + if adapter is not None: + adapter.kill() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) def see_device(address, name, new_device=False): """Mark a device as seen.""" @@ -48,6 +62,7 @@ def setup_scanner(hass, config, see, discovery_info=None): _LOGGER.debug("Discovering Bluetooth LE devices") try: adapter = pygatt.GATTToolBackend() + hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter devs = adapter.scan() devices = {x['address']: x['name'] for x in devs} diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index cd67ec31536..d2f8f10290e 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -3,7 +3,7 @@ "name": "Bluetooth le tracker", "documentation": "https://www.home-assistant.io/components/bluetooth_le_tracker", "requirements": [ - "pygatt[GATTTOOL]==3.2.0" + "pygatt[GATTTOOL]==4.0.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index 8d2f758aed2..893a1f3469e 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -3,7 +3,7 @@ "name": "Skybeacon", "documentation": "https://www.home-assistant.io/components/skybeacon", "requirements": [ - "pygatt[GATTTOOL]==3.2.0" + "pygatt[GATTTOOL]==4.0.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index b8b81d90017..b861e8e2a49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1061,7 +1061,7 @@ pyfttt==0.3 # homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon -pygatt[GATTTOOL]==3.2.0 +pygatt[GATTTOOL]==4.0.1 # homeassistant.components.gogogate2 pygogogate2==0.1.1 From 6bb4199824179be72de35df79e92e59b543ee10c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 18 Apr 2019 03:16:20 +0100 Subject: [PATCH 005/346] Add @Jc2k to codeowners for homekit_controller (#23173) --- CODEOWNERS | 1 + homeassistant/components/homekit_controller/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 30269be9051..68720c2821b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -99,6 +99,7 @@ homeassistant/components/history_graph/* @andrey-git homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @cdce8p +homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/html5/* @robbiet480 homeassistant/components/http/* @home-assistant/core diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index e724f680b60..3e447f08f4b 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -6,5 +6,7 @@ "homekit[IP]==0.13.0" ], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [ + "@Jc2k" + ] } From 77244eab1ed226a854b7a48524b6b5883ddcb119 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Apr 2019 19:17:13 -0700 Subject: [PATCH 006/346] Fix empty components (#23177) --- homeassistant/loader.py | 8 ++++---- tests/common.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ecc39f8db8f..ed2ea83afb0 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -51,11 +51,11 @@ CUSTOM_WARNING = ( _UNDEF = object() -def manifest_from_legacy_module(module: ModuleType) -> Dict: +def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict: """Generate a manifest from a legacy module.""" return { - 'domain': module.DOMAIN, # type: ignore - 'name': module.DOMAIN, # type: ignore + 'domain': domain, + 'name': domain, 'documentation': None, 'requirements': getattr(module, 'REQUIREMENTS', []), 'dependencies': getattr(module, 'DEPENDENCIES', []), @@ -106,7 +106,7 @@ class Integration: return cls( hass, comp.__name__, pathlib.Path(comp.__file__).parent, - manifest_from_legacy_module(comp) + manifest_from_legacy_module(domain, comp) ) def __init__(self, hass: 'HomeAssistant', pkg_path: str, diff --git a/tests/common.py b/tests/common.py index 99afd4fdb95..2467dae04b9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -487,7 +487,7 @@ class MockModule: def mock_manifest(self): """Generate a mock manifest to represent this module.""" return { - **loader.manifest_from_legacy_module(self), + **loader.manifest_from_legacy_module(self.DOMAIN, self), **(self._partial_manifest or {}) } From 474ac8b09ee369916061bf1991f7f8956612a0fe Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Thu, 18 Apr 2019 06:13:03 +0100 Subject: [PATCH 007/346] Add basic support for native Hue sensors (#22598) * Add basic support for native Hue sensors * Update coveragerc * Simplify attributes * Remove config option * Refactor and document device-ness and update mechanism * Entity docstrings * Remove lingering config for sensors * Whitespace * Remove redundant entity ID generation and hass assignment. * More meaningful variable name. * Add new 'not-darkness' pseudo-sensor. * Refactor sensors into separate binary, non-binary, and shared modules. * formatting * make linter happy. * Refactor again, fix update mechanism, and address comments. * Remove unnecessary assignment * Small fixes. * docstring * Another refactor: only call API once and make testing easier * Tests & test fixes * Flake & lint * Use gather and dispatcher * Remove unnecessary whitespace change. * Move component related stuff out of the shared module * Remove unused remnant of failed approach. * Increase test coverage * Don't get too upset if we're already trying to update an entity before it has finished adding * relative imports --- homeassistant/components/hue/binary_sensor.py | 27 + homeassistant/components/hue/bridge.py | 16 +- homeassistant/components/hue/sensor.py | 57 ++ homeassistant/components/hue/sensor_base.py | 283 ++++++++++ tests/components/hue/test_bridge.py | 14 +- tests/components/hue/test_sensor_base.py | 485 ++++++++++++++++++ 6 files changed, 875 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/hue/binary_sensor.py create mode 100644 homeassistant/components/hue/sensor.py create mode 100644 homeassistant/components/hue/sensor_base.py create mode 100644 tests/components/hue/test_sensor_base.py diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py new file mode 100644 index 00000000000..d60750721ac --- /dev/null +++ b/homeassistant/components/hue/binary_sensor.py @@ -0,0 +1,27 @@ +"""Hue binary sensor entities.""" +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.hue.sensor_base import ( + GenericZLLSensor, async_setup_entry as shared_async_setup_entry) + + +PRESENCE_NAME_FORMAT = "{} presence" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer binary sensor setup to the shared sensor module.""" + await shared_async_setup_entry( + hass, config_entry, async_add_entities, binary=True) + + +class HuePresence(GenericZLLSensor, BinarySensorDevice): + """The presence sensor entity for a Hue motion sensor device.""" + + device_class = 'presence' + + async def _async_update_ha_state(self, *args, **kwargs): + await self.async_update_ha_state(self, *args, **kwargs) + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.sensor.presence diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 9e99d219316..25db031e6bf 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -69,6 +69,10 @@ class HueBridge: hass.async_create_task(hass.config_entries.async_forward_entry_setup( self.config_entry, 'light')) + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + self.config_entry, 'binary_sensor')) + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + self.config_entry, 'sensor')) hass.services.async_register( DOMAIN, SERVICE_HUE_SCENE, self.hue_activate_scene, @@ -94,8 +98,16 @@ class HueBridge: # 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') + results = await asyncio.gather( + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'light'), + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'binary_sensor'), + self.hass.config_entries.async_forward_entry_unload( + self.config_entry, 'sensor') + ) + # None and True are OK + return False not in results async def hue_activate_scene(self, call, updated=False): """Service to call directly into bridge to set scenes.""" diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py new file mode 100644 index 00000000000..555c16a0be7 --- /dev/null +++ b/homeassistant/components/hue/sensor.py @@ -0,0 +1,57 @@ +"""Hue sensor entities.""" +from homeassistant.const import ( + DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS) +from homeassistant.helpers.entity import Entity +from homeassistant.components.hue.sensor_base import ( + GenericZLLSensor, async_setup_entry as shared_async_setup_entry) + + +LIGHT_LEVEL_NAME_FORMAT = "{} light level" +TEMPERATURE_NAME_FORMAT = "{} temperature" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + await shared_async_setup_entry( + hass, config_entry, async_add_entities, binary=False) + + +class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity): + """Parent class for all 'gauge' Hue device sensors.""" + + async def _async_update_ha_state(self, *args, **kwargs): + await self.async_update_ha_state(self, *args, **kwargs) + + +class HueLightLevel(GenericHueGaugeSensorEntity): + """The light level sensor entity for a Hue motion sensor device.""" + + device_class = DEVICE_CLASS_ILLUMINANCE + unit_of_measurement = "Lux" + + @property + def state(self): + """Return the state of the device.""" + return self.sensor.lightlevel + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = super().device_state_attributes + attributes.update({ + "threshold_dark": self.sensor.tholddark, + "threshold_offset": self.sensor.tholdoffset, + }) + return attributes + + +class HueTemperature(GenericHueGaugeSensorEntity): + """The temperature sensor entity for a Hue motion sensor device.""" + + device_class = DEVICE_CLASS_TEMPERATURE + unit_of_measurement = TEMP_CELSIUS + + @property + def state(self): + """Return the state of the device.""" + return self.sensor.temperature / 100 diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py new file mode 100644 index 00000000000..1d6fa2d34b4 --- /dev/null +++ b/homeassistant/components/hue/sensor_base.py @@ -0,0 +1,283 @@ +"""Support for the Philips Hue sensors as a platform.""" +import asyncio +from datetime import timedelta +import logging +from time import monotonic + +import async_timeout + +from homeassistant.components import hue +from homeassistant.exceptions import NoEntitySpecifiedError +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util.dt import utcnow + + +CURRENT_SENSORS = 'current_sensors' +SENSOR_MANAGER = 'sensor_manager' + +_LOGGER = logging.getLogger(__name__) + + +def _device_id(aiohue_sensor): + # Work out the shared device ID, as described below + device_id = aiohue_sensor.uniqueid + if device_id and len(device_id) > 23: + device_id = device_id[:23] + return device_id + + +async def async_setup_entry(hass, config_entry, async_add_entities, + binary=False): + """Set up the Hue sensors from a config entry.""" + bridge = hass.data[hue.DOMAIN][config_entry.data['host']] + hass.data[hue.DOMAIN].setdefault(CURRENT_SENSORS, {}) + + manager = hass.data[hue.DOMAIN].get(SENSOR_MANAGER) + if manager is None: + manager = SensorManager(hass, bridge) + hass.data[hue.DOMAIN][SENSOR_MANAGER] = manager + + manager.register_component(binary, async_add_entities) + await manager.start() + + +class SensorManager: + """Class that handles registering and updating Hue sensor entities. + + Intended to be a singleton. + """ + + SCAN_INTERVAL = timedelta(seconds=5) + sensor_config_map = {} + + def __init__(self, hass, bridge): + """Initialize the sensor manager.""" + import aiohue + from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT + from .sensor import ( + HueLightLevel, HueTemperature, LIGHT_LEVEL_NAME_FORMAT, + TEMPERATURE_NAME_FORMAT) + + self.hass = hass + self.bridge = bridge + self._component_add_entities = {} + self._started = False + + self.sensor_config_map.update({ + aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: { + "binary": False, + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + aiohue.sensors.TYPE_ZLL_TEMPERATURE: { + "binary": False, + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + aiohue.sensors.TYPE_ZLL_PRESENCE: { + "binary": True, + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + }, + }) + + def register_component(self, binary, async_add_entities): + """Register async_add_entities methods for components.""" + self._component_add_entities[binary] = async_add_entities + + async def start(self): + """Start updating sensors from the bridge on a schedule.""" + # but only if it's not already started, and when we've got both + # async_add_entities methods + if self._started or len(self._component_add_entities) < 2: + return + + self._started = True + _LOGGER.info('Starting sensor polling loop with %s second interval', + self.SCAN_INTERVAL.total_seconds()) + + async def async_update_bridge(now): + """Will update sensors from the bridge.""" + await self.async_update_items() + + async_track_point_in_utc_time( + self.hass, async_update_bridge, utcnow() + self.SCAN_INTERVAL) + + await async_update_bridge(None) + + async def async_update_items(self): + """Update sensors from the bridge.""" + import aiohue + + api = self.bridge.api.sensors + + try: + start = monotonic() + with async_timeout.timeout(4): + await api.update() + except (asyncio.TimeoutError, aiohue.AiohueException) as err: + _LOGGER.debug('Failed to fetch sensor: %s', err) + + if not self.bridge.available: + return + + _LOGGER.error('Unable to reach bridge %s (%s)', self.bridge.host, + err) + self.bridge.available = False + + return + + finally: + _LOGGER.debug('Finished sensor request in %.3f seconds', + monotonic() - start) + + if not self.bridge.available: + _LOGGER.info('Reconnected to bridge %s', self.bridge.host) + self.bridge.available = True + + new_sensors = [] + new_binary_sensors = [] + primary_sensor_devices = {} + current = self.hass.data[hue.DOMAIN][CURRENT_SENSORS] + + # Physical Hue motion sensors present as three sensors in the API: a + # presence sensor, a temperature sensor, and a light level sensor. Of + # these, only the presence sensor is assigned the user-friendly name + # that the user has given to the device. Each of these sensors is + # linked by a common device_id, which is the first twenty-three + # characters of the unique id (then followed by a hyphen and an ID + # specific to the individual sensor). + # + # To set up neat values, and assign the sensor entities to the same + # device, we first, iterate over all the sensors and find the Hue + # presence sensors, then iterate over all the remaining sensors - + # finding the remaining ones that may or may not be related to the + # presence sensors. + for item_id in api: + if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE: + continue + + primary_sensor_devices[_device_id(api[item_id])] = api[item_id] + + # Iterate again now we have all the presence sensors, and add the + # related sensors with nice names where appropriate. + for item_id in api: + existing = current.get(api[item_id].uniqueid) + if existing is not None: + self.hass.async_create_task( + existing.async_maybe_update_ha_state()) + continue + + primary_sensor = None + sensor_config = self.sensor_config_map.get(api[item_id].type) + if sensor_config is None: + continue + + base_name = api[item_id].name + primary_sensor = primary_sensor_devices.get( + _device_id(api[item_id])) + if primary_sensor is not None: + base_name = primary_sensor.name + name = sensor_config["name_format"].format(base_name) + + current[api[item_id].uniqueid] = sensor_config["class"]( + api[item_id], name, self.bridge, primary_sensor=primary_sensor) + if sensor_config['binary']: + new_binary_sensors.append(current[api[item_id].uniqueid]) + else: + new_sensors.append(current[api[item_id].uniqueid]) + + async_add_sensor_entities = self._component_add_entities.get(False) + async_add_binary_entities = self._component_add_entities.get(True) + if new_sensors and async_add_sensor_entities: + async_add_sensor_entities(new_sensors) + if new_binary_sensors and async_add_binary_entities: + async_add_binary_entities(new_binary_sensors) + + +class GenericHueSensor: + """Representation of a Hue sensor.""" + + should_poll = False + + def __init__(self, sensor, name, bridge, primary_sensor=None): + """Initialize the sensor.""" + self.sensor = sensor + self._name = name + self._primary_sensor = primary_sensor + self.bridge = bridge + + async def _async_update_ha_state(self, *args, **kwargs): + raise NotImplementedError + + @property + def primary_sensor(self): + """Return the primary sensor entity of the physical device.""" + return self._primary_sensor or self.sensor + + @property + def device_id(self): + """Return the ID of the physical device this sensor is part of.""" + return self.unique_id[:23] + + @property + def unique_id(self): + """Return the ID of this Hue sensor.""" + return self.sensor.uniqueid + + @property + def name(self): + """Return a friendly name for the sensor.""" + return self._name + + @property + def available(self): + """Return if sensor is available.""" + return self.bridge.available and (self.bridge.allow_unreachable or + self.sensor.config['reachable']) + + @property + def swupdatestate(self): + """Return detail of available software updates for this device.""" + return self.primary_sensor.raw.get('swupdate', {}).get('state') + + async def async_maybe_update_ha_state(self): + """Try to update Home Assistant with current state of entity. + + But if it's not been added to hass yet, then don't throw an error. + """ + try: + await self._async_update_ha_state() + except (RuntimeError, NoEntitySpecifiedError): + _LOGGER.debug( + "Hue sensor update requested before it has been added.") + + @property + def device_info(self): + """Return the device info. + + Links individual entities together in the hass device registry. + """ + return { + 'identifiers': { + (hue.DOMAIN, self.device_id) + }, + 'name': self.primary_sensor.name, + 'manufacturer': self.primary_sensor.manufacturername, + 'model': ( + self.primary_sensor.productname or + self.primary_sensor.modelid), + 'sw_version': self.primary_sensor.swversion, + 'via_hub': (hue.DOMAIN, self.bridge.api.config.bridgeid), + } + + +class GenericZLLSensor(GenericHueSensor): + """Representation of a Hue-brand, physical sensor.""" + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return { + "battery_level": self.sensor.battery + } diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 855a12e2620..5b383afc53d 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -21,9 +21,13 @@ 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_setup.mock_calls) == 1 - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ - (entry, 'light') + forward_entries = set( + c[1][1] + for c in + hass.config_entries.async_forward_entry_setup.mock_calls + ) + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 + assert forward_entries == set(['light', 'binary_sensor', 'sensor']) async def test_bridge_setup_invalid_username(): @@ -84,11 +88,11 @@ async def test_reset_unloads_entry_if_setup(): 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 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 3 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.config_entries.async_forward_entry_unload.mock_calls) == 3 assert len(hass.services.async_remove.mock_calls) == 1 diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py new file mode 100644 index 00000000000..99829c59666 --- /dev/null +++ b/tests/components/hue/test_sensor_base.py @@ -0,0 +1,485 @@ +"""Philips Hue sensors platform tests.""" +import asyncio +from collections import deque +import datetime +import logging +from unittest.mock import Mock + +import aiohue +from aiohue.sensors import Sensors +import pytest + +from homeassistant import config_entries +from homeassistant.components import hue +from homeassistant.components.hue import sensor_base as hue_sensor_base + +_LOGGER = logging.getLogger(__name__) + +PRESENCE_SENSOR_1_PRESENT = { + "state": { + "presence": True, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T00:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "ledindication": False, + "usertest": False, + "sensitivity": 2, + "sensitivitymax": 2, + "pending": [] + }, + "name": "Living room sensor", + "type": "ZLLPresence", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue motion sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:77-02-0406", + "capabilities": { + "certified": True + } +} +LIGHT_LEVEL_SENSOR_1 = { + "state": { + "lightlevel": 0, + "dark": True, + "daylight": True, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T00:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "tholddark": 12467, + "tholdoffset": 7000, + "ledindication": False, + "usertest": False, + "pending": [] + }, + "name": "Hue ambient light sensor 1", + "type": "ZLLLightLevel", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue ambient light sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:77-02-0400", + "capabilities": { + "certified": True + } +} +TEMPERATURE_SENSOR_1 = { + "state": { + "temperature": 1775, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T01:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "ledindication": False, + "usertest": False, + "pending": [] + }, + "name": "Hue temperature sensor 1", + "type": "ZLLTemperature", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue temperature sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:77-02-0402", + "capabilities": { + "certified": True + } +} +PRESENCE_SENSOR_2_NOT_PRESENT = { + "state": { + "presence": False, + "lastupdated": "2019-01-01T00:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T01:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "ledindication": False, + "usertest": False, + "sensitivity": 2, + "sensitivitymax": 2, + "pending": [] + }, + "name": "Kitchen sensor", + "type": "ZLLPresence", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue motion sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:88-02-0406", + "capabilities": { + "certified": True + } +} +LIGHT_LEVEL_SENSOR_2 = { + "state": { + "lightlevel": 100, + "dark": True, + "daylight": True, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T00:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "tholddark": 12467, + "tholdoffset": 7000, + "ledindication": False, + "usertest": False, + "pending": [] + }, + "name": "Hue ambient light sensor 2", + "type": "ZLLLightLevel", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue ambient light sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:88-02-0400", + "capabilities": { + "certified": True + } +} +TEMPERATURE_SENSOR_2 = { + "state": { + "temperature": 1875, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T01:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "ledindication": False, + "usertest": False, + "pending": [] + }, + "name": "Hue temperature sensor 2", + "type": "ZLLTemperature", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue temperature sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:88-02-0402", + "capabilities": { + "certified": True + } +} +PRESENCE_SENSOR_3_PRESENT = { + "state": { + "presence": True, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T00:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "ledindication": False, + "usertest": False, + "sensitivity": 2, + "sensitivitymax": 2, + "pending": [] + }, + "name": "Bedroom sensor", + "type": "ZLLPresence", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue motion sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:99-02-0406", + "capabilities": { + "certified": True + } +} +LIGHT_LEVEL_SENSOR_3 = { + "state": { + "lightlevel": 0, + "dark": True, + "daylight": True, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T00:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "tholddark": 12467, + "tholdoffset": 7000, + "ledindication": False, + "usertest": False, + "pending": [] + }, + "name": "Hue ambient light sensor 3", + "type": "ZLLLightLevel", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue ambient light sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:99-02-0400", + "capabilities": { + "certified": True + } +} +TEMPERATURE_SENSOR_3 = { + "state": { + "temperature": 1775, + "lastupdated": "2019-01-01T01:00:00" + }, + "swupdate": { + "state": "noupdates", + "lastinstall": "2019-01-01T01:00:00" + }, + "config": { + "on": True, + "battery": 100, + "reachable": True, + "alert": "none", + "ledindication": False, + "usertest": False, + "pending": [] + }, + "name": "Hue temperature sensor 3", + "type": "ZLLTemperature", + "modelid": "SML001", + "manufacturername": "Philips", + "productname": "Hue temperature sensor", + "swversion": "6.1.1.27575", + "uniqueid": "00:11:22:33:44:55:66:99-02-0402", + "capabilities": { + "certified": True + } +} +UNSUPPORTED_SENSOR = { + "state": { + "status": 0, + "lastupdated": "2019-01-01T01:00:00" + }, + "config": { + "on": True, + "reachable": True + }, + "name": "Unsupported sensor", + "type": "CLIPGenericStatus", + "modelid": "PHWA01", + "manufacturername": "Philips", + "swversion": "1.0", + "uniqueid": "arbitrary", + "recycle": True +} +SENSOR_RESPONSE = { + "1": PRESENCE_SENSOR_1_PRESENT, + "2": LIGHT_LEVEL_SENSOR_1, + "3": TEMPERATURE_SENSOR_1, + "4": PRESENCE_SENSOR_2_NOT_PRESENT, + "5": LIGHT_LEVEL_SENSOR_2, + "6": TEMPERATURE_SENSOR_2, +} + + +@pytest.fixture +def mock_bridge(hass): + """Mock a Hue bridge.""" + 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 + # than expected. + bridge.mock_sensor_responses = deque() + + async def mock_request(method, path, **kwargs): + kwargs['method'] = method + kwargs['path'] = path + bridge.mock_requests.append(kwargs) + + if path == 'sensors': + return bridge.mock_sensor_responses.popleft() + return None + + bridge.api.config.apiversion = '9.9.9' + bridge.api.sensors = Sensors({}, mock_request) + + return bridge + + +@pytest.fixture +def increase_scan_interval(hass): + """Increase the SCAN_INTERVAL to prevent unexpected scans during tests.""" + hue_sensor_base.SensorManager.SCAN_INTERVAL = datetime.timedelta(days=365) + + +async def setup_bridge(hass, mock_bridge): + """Load the Hue platform with the provided bridge.""" + hass.config.components.add(hue.DOMAIN) + hass.data[hue.DOMAIN] = {'mock-host': mock_bridge} + config_entry = config_entries.ConfigEntry(1, hue.DOMAIN, 'Mock Title', { + 'host': 'mock-host' + }, 'test', config_entries.CONN_CLASS_LOCAL_POLL) + await hass.config_entries.async_forward_entry_setup( + config_entry, 'binary_sensor') + await hass.config_entries.async_forward_entry_setup( + config_entry, 'sensor') + # and make sure it completes before going further + await hass.async_block_till_done() + + +async def test_no_sensors(hass, mock_bridge): + """Test the update_items function when no sensors are found.""" + mock_bridge.allow_groups = True + mock_bridge.mock_sensor_responses.append({}) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 0 + + +async def test_sensors(hass, mock_bridge): + """Test the update_items function with some sensors.""" + mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + # 2 "physical" sensors with 3 virtual sensors each + assert len(hass.states.async_all()) == 6 + + presence_sensor_1 = hass.states.get( + 'binary_sensor.living_room_sensor_presence') + light_level_sensor_1 = hass.states.get( + 'sensor.living_room_sensor_light_level') + temperature_sensor_1 = hass.states.get( + 'sensor.living_room_sensor_temperature') + assert presence_sensor_1 is not None + assert presence_sensor_1.state == 'on' + assert light_level_sensor_1 is not None + assert light_level_sensor_1.state == '0' + assert light_level_sensor_1.name == 'Living room sensor light level' + assert temperature_sensor_1 is not None + assert temperature_sensor_1.state == '17.75' + assert temperature_sensor_1.name == 'Living room sensor temperature' + + presence_sensor_2 = hass.states.get( + 'binary_sensor.kitchen_sensor_presence') + light_level_sensor_2 = hass.states.get( + 'sensor.kitchen_sensor_light_level') + temperature_sensor_2 = hass.states.get( + 'sensor.kitchen_sensor_temperature') + assert presence_sensor_2 is not None + assert presence_sensor_2.state == 'off' + assert light_level_sensor_2 is not None + assert light_level_sensor_2.state == '100' + assert light_level_sensor_2.name == 'Kitchen sensor light level' + assert temperature_sensor_2 is not None + assert temperature_sensor_2.state == '18.75' + assert temperature_sensor_2.name == 'Kitchen sensor temperature' + + +async def test_unsupported_sensors(hass, mock_bridge): + """Test that unsupported sensors don't get added and don't fail.""" + response_with_unsupported = dict(SENSOR_RESPONSE) + response_with_unsupported['7'] = UNSUPPORTED_SENSOR + mock_bridge.mock_sensor_responses.append(response_with_unsupported) + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + # 2 "physical" sensors with 3 virtual sensors each + assert len(hass.states.async_all()) == 6 + + +async def test_new_sensor_discovered(hass, mock_bridge): + """Test if 2nd update has a new sensor.""" + mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 6 + + new_sensor_response = dict(SENSOR_RESPONSE) + new_sensor_response.update({ + "7": PRESENCE_SENSOR_3_PRESENT, + "8": LIGHT_LEVEL_SENSOR_3, + "9": TEMPERATURE_SENSOR_3, + }) + + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + sm = hass.data[hue.DOMAIN][hue_sensor_base.SENSOR_MANAGER] + await sm.async_update_items() + + # To flush out the service call to update the group + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 9 + + presence = hass.states.get('binary_sensor.bedroom_sensor_presence') + assert presence is not None + assert presence.state == 'on' + temperature = hass.states.get('sensor.bedroom_sensor_temperature') + assert temperature is not None + assert temperature.state == '17.75' + + +async def test_update_timeout(hass, mock_bridge): + """Test bridge marked as not available if timeout error during update.""" + mock_bridge.api.sensors.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.sensors.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 From ce8ec3acb174f71f4393d9a4bce1f2bcd39ed9f2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Apr 2019 22:27:11 -0700 Subject: [PATCH 008/346] Don't warn for missing services (#23182) --- homeassistant/helpers/service.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 1cfbf9e3c5f..8c576f58c14 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -210,9 +210,8 @@ async def async_get_all_descriptions(hass): domain_yaml = loaded[domain] yaml_description = domain_yaml.get(service, {}) - if not yaml_description: - _LOGGER.warning("Missing service description for %s/%s", - domain, service) + # Don't warn for missing services, because it triggers false + # positives for things like scripts, that register as a service description = descriptions_cache[cache_key] = { 'description': yaml_description.get('description', ''), From 4a2a130bfac2178f1c562c6dcb2e5d1b1ac8f903 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 18 Apr 2019 07:37:39 +0200 Subject: [PATCH 009/346] Google assistant skip missing type (#23174) * Skip entity if no device type found * Add test for potentially skipped binary sensors * Reorg code, add tests to ensure all exposed things have types * Lint * Fix tests * Lint --- homeassistant/components/cloud/http_api.py | 5 +- .../components/google_assistant/const.py | 46 ++++ .../components/google_assistant/error.py | 13 + .../components/google_assistant/helpers.py | 195 ++++++++++++++- .../components/google_assistant/smart_home.py | 229 +----------------- .../components/google_assistant/trait.py | 2 +- tests/components/cloud/test_http_api.py | 2 +- .../google_assistant/test_smart_home.py | 6 +- .../components/google_assistant/test_trait.py | 27 +++ 9 files changed, 285 insertions(+), 240 deletions(-) create mode 100644 homeassistant/components/google_assistant/error.py diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index fe13172d7fe..6ab7d911d47 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,7 +14,8 @@ from homeassistant.components.http.data_validator import ( RequestDataValidator) from homeassistant.components import websocket_api from homeassistant.components.alexa import smart_home as alexa_sh -from homeassistant.components.google_assistant import smart_home as google_sh +from homeassistant.components.google_assistant import ( + const as google_const) from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, @@ -415,7 +416,7 @@ def _account_data(cloud): 'cloud': cloud.iot.state, 'prefs': client.prefs.as_dict(), 'google_entities': client.google_user_config['filter'].config, - 'google_domains': list(google_sh.DOMAIN_TO_GOOGLE_TYPES), + 'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES), 'alexa_entities': client.alexa_config.should_expose.config, 'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS), 'remote_domain': remote.instance_domain, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 477e67ab75a..67c767c080b 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -1,4 +1,20 @@ """Constants for Google Assistant.""" +from homeassistant.components import ( + binary_sensor, + camera, + climate, + cover, + fan, + group, + input_boolean, + light, + lock, + media_player, + scene, + script, + switch, + vacuum, +) DOMAIN = 'google_assistant' GOOGLE_ASSISTANT_API_ENDPOINT = '/api/google_assistant' @@ -32,6 +48,7 @@ TYPE_LOCK = PREFIX_TYPES + 'LOCK' TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' TYPE_OUTLET = PREFIX_TYPES + 'OUTLET' +TYPE_SENSOR = PREFIX_TYPES + 'SENSOR' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' @@ -51,3 +68,32 @@ ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' EVENT_COMMAND_RECEIVED = 'google_assistant_command' EVENT_QUERY_RECEIVED = 'google_assistant_query' EVENT_SYNC_RECEIVED = 'google_assistant_sync' + +DOMAIN_TO_GOOGLE_TYPES = { + camera.DOMAIN: TYPE_CAMERA, + climate.DOMAIN: TYPE_THERMOSTAT, + cover.DOMAIN: TYPE_BLINDS, + fan.DOMAIN: TYPE_FAN, + group.DOMAIN: TYPE_SWITCH, + input_boolean.DOMAIN: TYPE_SWITCH, + light.DOMAIN: TYPE_LIGHT, + lock.DOMAIN: TYPE_LOCK, + media_player.DOMAIN: TYPE_SWITCH, + scene.DOMAIN: TYPE_SCENE, + script.DOMAIN: TYPE_SCENE, + switch.DOMAIN: TYPE_SWITCH, + vacuum.DOMAIN: TYPE_VACUUM, +} + +DEVICE_CLASS_TO_GOOGLE_TYPES = { + (cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE, + (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, + (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR): + TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, + +} diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py new file mode 100644 index 00000000000..2225bb58242 --- /dev/null +++ b/homeassistant/components/google_assistant/error.py @@ -0,0 +1,13 @@ +"""Errors for Google Assistant.""" + + +class SmartHomeError(Exception): + """Google Assistant Smart Home errors. + + https://developers.google.com/actions/smarthome/create-app#error_responses + """ + + def __init__(self, code, msg): + """Log error code.""" + super().__init__(msg) + self.code = code diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 8afa55acc5c..982b840393e 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,17 +1,19 @@ """Helper classes for Google Assistant integration.""" -from homeassistant.core import Context +from asyncio import gather +from collections.abc import Mapping +from homeassistant.core import Context, callback +from homeassistant.const import ( + CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES, + ATTR_DEVICE_CLASS +) -class SmartHomeError(Exception): - """Google Assistant Smart Home errors. - - https://developers.google.com/actions/smarthome/create-app#error_responses - """ - - def __init__(self, code, msg): - """Log error code.""" - super().__init__(msg) - self.code = code +from . import trait +from .const import ( + DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, + DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT, +) +from .error import SmartHomeError class Config: @@ -33,3 +35,174 @@ class RequestData: self.config = config self.request_id = request_id self.context = Context(user_id=user_id) + + +def get_google_type(domain, device_class): + """Google type based on domain and device class.""" + typ = DEVICE_CLASS_TO_GOOGLE_TYPES.get((domain, device_class)) + + return typ if typ is not None else DOMAIN_TO_GOOGLE_TYPES[domain] + + +class GoogleEntity: + """Adaptation of Entity expressed in Google's terms.""" + + def __init__(self, hass, config, state): + """Initialize a Google entity.""" + self.hass = hass + self.config = config + self.state = state + self._traits = None + + @property + def entity_id(self): + """Return entity ID.""" + return self.state.entity_id + + @callback + def traits(self): + """Return traits for entity.""" + if self._traits is not None: + return self._traits + + state = self.state + domain = state.domain + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + self._traits = [Trait(self.hass, state, self.config) + for Trait in trait.TRAITS + if Trait.supported(domain, features, device_class)] + return self._traits + + async def sync_serialize(self): + """Serialize entity for a SYNC response. + + https://developers.google.com/actions/smarthome/create-app#actiondevicessync + """ + 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 + + entity_config = self.config.entity_config.get(state.entity_id, {}) + name = (entity_config.get(CONF_NAME) or state.name).strip() + domain = state.domain + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + # If an empty string + if not name: + return None + + traits = self.traits() + + # Found no supported traits for this entity + if not traits: + return None + + device_type = get_google_type(domain, + device_class) + + device = { + 'id': state.entity_id, + 'name': { + 'name': name + }, + 'attributes': {}, + 'traits': [trait.name for trait in traits], + 'willReportState': False, + 'type': device_type, + } + + # use aliases + aliases = entity_config.get(CONF_ALIASES) + if aliases: + device['name']['nicknames'] = aliases + + for trt in traits: + device['attributes'].update(trt.sync_attributes()) + + room = entity_config.get(CONF_ROOM_HINT) + if room: + device['roomHint'] = room + return device + + dev_reg, ent_reg, area_reg = await gather( + self.hass.helpers.device_registry.async_get_registry(), + self.hass.helpers.entity_registry.async_get_registry(), + self.hass.helpers.area_registry.async_get_registry(), + ) + + entity_entry = ent_reg.async_get(state.entity_id) + if not (entity_entry and entity_entry.device_id): + return device + + device_entry = dev_reg.devices.get(entity_entry.device_id) + if not (device_entry and device_entry.area_id): + return device + + area_entry = area_reg.areas.get(device_entry.area_id) + if area_entry and area_entry.name: + device['roomHint'] = area_entry.name + + return device + + @callback + def query_serialize(self): + """Serialize entity for a QUERY response. + + https://developers.google.com/actions/smarthome/create-app#actiondevicesquery + """ + state = self.state + + if state.state == STATE_UNAVAILABLE: + return {'online': False} + + attrs = {'online': True} + + for trt in self.traits(): + deep_update(attrs, trt.query_attributes()) + + return attrs + + async def execute(self, command, data, params): + """Execute a command. + + https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute + """ + executed = False + for trt in self.traits(): + if trt.can_execute(command, params): + await trt.execute(command, data, params) + executed = True + break + + if not executed: + raise SmartHomeError( + ERR_FUNCTION_NOT_SUPPORTED, + 'Unable to execute {} for {}'.format(command, + self.state.entity_id)) + + @callback + def async_update(self): + """Update the entity with latest info from Home Assistant.""" + self.state = self.hass.states.get(self.entity_id) + + if self._traits is None: + return + + for trt in self._traits: + trt.state = self.state + + +def deep_update(target, source): + """Update a nested dictionary with another nested dictionary.""" + for key, value in source.items(): + if isinstance(value, Mapping): + target[key] = deep_update(target.get(key, {}), value) + else: + target[key] = value + return target diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index ab2907cf661..9edde36f09d 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -1,237 +1,22 @@ """Support for Google Assistant Smart Home API.""" -from asyncio import gather -from collections.abc import Mapping from itertools import product import logging from homeassistant.util.decorator import Registry -from homeassistant.core import callback from homeassistant.const import ( - CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES, ATTR_ENTITY_ID, ATTR_DEVICE_CLASS, -) -from homeassistant.components import ( - camera, - climate, - cover, - fan, - group, - input_boolean, - light, - lock, - media_player, - scene, - script, - switch, - vacuum, -) + CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID) - -from . import trait from .const import ( - TYPE_LIGHT, TYPE_LOCK, TYPE_SCENE, TYPE_SWITCH, TYPE_VACUUM, - TYPE_THERMOSTAT, TYPE_FAN, TYPE_CAMERA, TYPE_BLINDS, TYPE_GARAGE, - TYPE_OUTLET, - CONF_ALIASES, CONF_ROOM_HINT, - ERR_FUNCTION_NOT_SUPPORTED, ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, - ERR_UNKNOWN_ERROR, + ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR, EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED ) -from .helpers import SmartHomeError, RequestData +from .helpers import RequestData, GoogleEntity +from .error import SmartHomeError HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) -DOMAIN_TO_GOOGLE_TYPES = { - camera.DOMAIN: TYPE_CAMERA, - climate.DOMAIN: TYPE_THERMOSTAT, - cover.DOMAIN: TYPE_BLINDS, - fan.DOMAIN: TYPE_FAN, - group.DOMAIN: TYPE_SWITCH, - input_boolean.DOMAIN: TYPE_SWITCH, - light.DOMAIN: TYPE_LIGHT, - lock.DOMAIN: TYPE_LOCK, - media_player.DOMAIN: TYPE_SWITCH, - scene.DOMAIN: TYPE_SCENE, - script.DOMAIN: TYPE_SCENE, - switch.DOMAIN: TYPE_SWITCH, - vacuum.DOMAIN: TYPE_VACUUM, -} - -DEVICE_CLASS_TO_GOOGLE_TYPES = { - (cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE, - (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, - (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, -} - - -def deep_update(target, source): - """Update a nested dictionary with another nested dictionary.""" - for key, value in source.items(): - if isinstance(value, Mapping): - target[key] = deep_update(target.get(key, {}), value) - else: - target[key] = value - return target - - -def get_google_type(domain, device_class): - """Google type based on domain and device class.""" - typ = DEVICE_CLASS_TO_GOOGLE_TYPES.get((domain, device_class)) - - return typ if typ is not None else DOMAIN_TO_GOOGLE_TYPES.get(domain) - - -class _GoogleEntity: - """Adaptation of Entity expressed in Google's terms.""" - - def __init__(self, hass, config, state): - self.hass = hass - self.config = config - self.state = state - self._traits = None - - @property - def entity_id(self): - """Return entity ID.""" - return self.state.entity_id - - @callback - def traits(self): - """Return traits for entity.""" - if self._traits is not None: - return self._traits - - state = self.state - domain = state.domain - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - - self._traits = [Trait(self.hass, state, self.config) - for Trait in trait.TRAITS - if Trait.supported(domain, features, device_class)] - return self._traits - - async def sync_serialize(self): - """Serialize entity for a SYNC response. - - https://developers.google.com/actions/smarthome/create-app#actiondevicessync - """ - 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 - - entity_config = self.config.entity_config.get(state.entity_id, {}) - name = (entity_config.get(CONF_NAME) or state.name).strip() - domain = state.domain - device_class = state.attributes.get(ATTR_DEVICE_CLASS) - - # If an empty string - if not name: - return None - - traits = self.traits() - - # Found no supported traits for this entity - if not traits: - return None - - device = { - 'id': state.entity_id, - 'name': { - 'name': name - }, - 'attributes': {}, - 'traits': [trait.name for trait in traits], - 'willReportState': False, - 'type': get_google_type(domain, device_class), - } - - # use aliases - aliases = entity_config.get(CONF_ALIASES) - if aliases: - device['name']['nicknames'] = aliases - - for trt in traits: - device['attributes'].update(trt.sync_attributes()) - - room = entity_config.get(CONF_ROOM_HINT) - if room: - device['roomHint'] = room - return device - - dev_reg, ent_reg, area_reg = await gather( - self.hass.helpers.device_registry.async_get_registry(), - self.hass.helpers.entity_registry.async_get_registry(), - self.hass.helpers.area_registry.async_get_registry(), - ) - - entity_entry = ent_reg.async_get(state.entity_id) - if not (entity_entry and entity_entry.device_id): - return device - - device_entry = dev_reg.devices.get(entity_entry.device_id) - if not (device_entry and device_entry.area_id): - return device - - area_entry = area_reg.areas.get(device_entry.area_id) - if area_entry and area_entry.name: - device['roomHint'] = area_entry.name - - return device - - @callback - def query_serialize(self): - """Serialize entity for a QUERY response. - - https://developers.google.com/actions/smarthome/create-app#actiondevicesquery - """ - state = self.state - - if state.state == STATE_UNAVAILABLE: - return {'online': False} - - attrs = {'online': True} - - for trt in self.traits(): - deep_update(attrs, trt.query_attributes()) - - return attrs - - async def execute(self, command, data, params): - """Execute a command. - - https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute - """ - executed = False - for trt in self.traits(): - if trt.can_execute(command, params): - await trt.execute(command, data, params) - executed = True - break - - if not executed: - raise SmartHomeError( - ERR_FUNCTION_NOT_SUPPORTED, - 'Unable to execute {} for {}'.format(command, - self.state.entity_id)) - - @callback - def async_update(self): - """Update the entity with latest info from Home Assistant.""" - self.state = self.hass.states.get(self.entity_id) - - if self._traits is None: - return - - for trt in self._traits: - trt.state = self.state - async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" @@ -304,7 +89,7 @@ async def async_devices_sync(hass, data, payload): if not data.config.should_expose(state): continue - entity = _GoogleEntity(hass, data.config, state) + entity = GoogleEntity(hass, data.config, state) serialized = await entity.sync_serialize() if serialized is None: @@ -345,7 +130,7 @@ async def async_devices_query(hass, data, payload): devices[devid] = {'online': False} continue - entity = _GoogleEntity(hass, data.config, state) + entity = GoogleEntity(hass, data.config, state) devices[devid] = entity.query_serialize() return {'devices': devices} @@ -389,7 +174,7 @@ async def handle_devices_execute(hass, data, payload): } continue - entities[entity_id] = _GoogleEntity(hass, data.config, state) + entities[entity_id] = GoogleEntity(hass, data.config, state) try: await entities[entity_id].execute(execution['command'], diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index a79dfdd3dca..5bec683ccc7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -38,7 +38,7 @@ from .const import ( ERR_NOT_SUPPORTED, ERR_FUNCTION_NOT_SUPPORTED, ) -from .helpers import SmartHomeError +from .error import SmartHomeError _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 6c50a158cad..c147f8492d7 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -335,7 +335,7 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture, client = await hass_ws_client(hass) with patch.dict( - 'homeassistant.components.google_assistant.smart_home.' + 'homeassistant.components.google_assistant.const.' 'DOMAIN_TO_GOOGLE_TYPES', {'light': None}, clear=True ), patch.dict('homeassistant.components.alexa.smart_home.ENTITY_ADAPTERS', {'switch': None}, clear=True): diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 24c7059d5c5..30a398fccc3 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -96,7 +96,7 @@ async def test_sync_message(hass): trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, ], - 'type': sh.TYPE_LIGHT, + 'type': const.TYPE_LIGHT, 'willReportState': False, 'attributes': { 'colorModel': 'hsv', @@ -176,7 +176,7 @@ async def test_sync_in_area(hass, registries): trait.TRAIT_ONOFF, trait.TRAIT_COLOR_SETTING, ], - 'type': sh.TYPE_LIGHT, + 'type': const.TYPE_LIGHT, 'willReportState': False, 'attributes': { 'colorModel': 'hsv', @@ -489,7 +489,7 @@ async def test_serialize_input_boolean(hass): """Test serializing an input boolean entity.""" state = State('input_boolean.bla', 'on') # pylint: disable=protected-access - entity = sh._GoogleEntity(hass, BASIC_CONFIG, state) + entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) result = await entity.sync_serialize() assert result == { 'id': 'input_boolean.bla', diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 07db4c47296..12731978f57 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -49,6 +49,7 @@ UNSAFE_CONFIG = helpers.Config( async def test_brightness_light(hass): """Test brightness trait support for light domain.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None assert trait.BrightnessTrait.supported(light.DOMAIN, light.SUPPORT_BRIGHTNESS, None) @@ -87,6 +88,7 @@ async def test_brightness_light(hass): async def test_brightness_media_player(hass): """Test brightness trait support for media player domain.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.BrightnessTrait.supported(media_player.DOMAIN, media_player.SUPPORT_VOLUME_SET, None) @@ -117,6 +119,7 @@ async def test_brightness_media_player(hass): async def test_camera_stream(hass): """Test camera stream trait support for camera domain.""" hass.config.api = Mock(base_url='http://1.1.1.1:8123') + assert helpers.get_google_type(camera.DOMAIN, None) is not None assert trait.CameraStreamTrait.supported(camera.DOMAIN, camera.SUPPORT_STREAM, None) @@ -145,6 +148,7 @@ async def test_camera_stream(hass): async def test_onoff_group(hass): """Test OnOff trait support for group domain.""" + assert helpers.get_google_type(group.DOMAIN, None) is not None assert trait.OnOffTrait.supported(group.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('group.bla', STATE_ON), BASIC_CONFIG) @@ -183,6 +187,7 @@ async def test_onoff_group(hass): async def test_onoff_input_boolean(hass): """Test OnOff trait support for input_boolean domain.""" + assert helpers.get_google_type(input_boolean.DOMAIN, None) is not None assert trait.OnOffTrait.supported(input_boolean.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('input_boolean.bla', STATE_ON), @@ -223,6 +228,7 @@ async def test_onoff_input_boolean(hass): async def test_onoff_switch(hass): """Test OnOff trait support for switch domain.""" + assert helpers.get_google_type(switch.DOMAIN, None) is not None assert trait.OnOffTrait.supported(switch.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('switch.bla', STATE_ON), @@ -262,6 +268,7 @@ async def test_onoff_switch(hass): async def test_onoff_fan(hass): """Test OnOff trait support for fan domain.""" + assert helpers.get_google_type(fan.DOMAIN, None) is not None assert trait.OnOffTrait.supported(fan.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('fan.bla', STATE_ON), BASIC_CONFIG) @@ -298,6 +305,7 @@ async def test_onoff_fan(hass): async def test_onoff_light(hass): """Test OnOff trait support for light domain.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None assert trait.OnOffTrait.supported(light.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('light.bla', STATE_ON), BASIC_CONFIG) @@ -336,6 +344,7 @@ async def test_onoff_light(hass): async def test_onoff_media_player(hass): """Test OnOff trait support for media_player domain.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.OnOffTrait.supported(media_player.DOMAIN, 0, None) trt_on = trait.OnOffTrait(hass, State('media_player.bla', STATE_ON), @@ -377,12 +386,14 @@ async def test_onoff_media_player(hass): async def test_onoff_climate(hass): """Test OnOff trait not supported for climate domain.""" + assert helpers.get_google_type(climate.DOMAIN, None) is not None assert not trait.OnOffTrait.supported( climate.DOMAIN, climate.SUPPORT_ON_OFF, None) async def test_dock_vacuum(hass): """Test dock trait support for vacuum domain.""" + assert helpers.get_google_type(vacuum.DOMAIN, None) is not None assert trait.DockTrait.supported(vacuum.DOMAIN, 0, None) trt = trait.DockTrait(hass, State('vacuum.bla', vacuum.STATE_IDLE), @@ -406,6 +417,7 @@ async def test_dock_vacuum(hass): async def test_startstop_vacuum(hass): """Test startStop trait support for vacuum domain.""" + assert helpers.get_google_type(vacuum.DOMAIN, None) is not None assert trait.StartStopTrait.supported(vacuum.DOMAIN, 0, None) trt = trait.StartStopTrait(hass, State('vacuum.bla', vacuum.STATE_PAUSED, { @@ -454,6 +466,7 @@ async def test_startstop_vacuum(hass): async def test_color_setting_color_light(hass): """Test ColorSpectrum trait support for light domain.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) assert trait.ColorSettingTrait.supported(light.DOMAIN, light.SUPPORT_COLOR, None) @@ -515,6 +528,7 @@ async def test_color_setting_color_light(hass): async def test_color_setting_temperature_light(hass): """Test ColorTemperature trait support for light domain.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) assert trait.ColorSettingTrait.supported(light.DOMAIN, light.SUPPORT_COLOR_TEMP, None) @@ -568,6 +582,7 @@ async def test_color_setting_temperature_light(hass): async def test_color_light_temperature_light_bad_temp(hass): """Test ColorTemperature trait support for light domain.""" + assert helpers.get_google_type(light.DOMAIN, None) is not None assert not trait.ColorSettingTrait.supported(light.DOMAIN, 0, None) assert trait.ColorSettingTrait.supported(light.DOMAIN, light.SUPPORT_COLOR_TEMP, None) @@ -584,6 +599,7 @@ async def test_color_light_temperature_light_bad_temp(hass): async def test_scene_scene(hass): """Test Scene trait support for scene domain.""" + assert helpers.get_google_type(scene.DOMAIN, None) is not None assert trait.SceneTrait.supported(scene.DOMAIN, 0, None) trt = trait.SceneTrait(hass, State('scene.bla', scene.STATE), BASIC_CONFIG) @@ -601,6 +617,7 @@ async def test_scene_scene(hass): async def test_scene_script(hass): """Test Scene trait support for script domain.""" + assert helpers.get_google_type(script.DOMAIN, None) is not None assert trait.SceneTrait.supported(script.DOMAIN, 0, None) trt = trait.SceneTrait(hass, State('script.bla', STATE_OFF), BASIC_CONFIG) @@ -622,6 +639,7 @@ async def test_scene_script(hass): async def test_temperature_setting_climate_onoff(hass): """Test TemperatureSetting trait support for climate domain - range.""" + assert helpers.get_google_type(climate.DOMAIN, None) is not None assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert trait.TemperatureSettingTrait.supported( climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) @@ -666,6 +684,7 @@ async def test_temperature_setting_climate_onoff(hass): async def test_temperature_setting_climate_range(hass): """Test TemperatureSetting trait support for climate domain - range.""" + assert helpers.get_google_type(climate.DOMAIN, None) is not None assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert trait.TemperatureSettingTrait.supported( climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) @@ -741,6 +760,7 @@ async def test_temperature_setting_climate_range(hass): async def test_temperature_setting_climate_setpoint(hass): """Test TemperatureSetting trait support for climate domain - setpoint.""" + assert helpers.get_google_type(climate.DOMAIN, None) is not None assert not trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) assert trait.TemperatureSettingTrait.supported( climate.DOMAIN, climate.SUPPORT_OPERATION_MODE, None) @@ -841,6 +861,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): async def test_lock_unlock_lock(hass): """Test LockUnlock trait locking support for lock domain.""" + assert helpers.get_google_type(lock.DOMAIN, None) is not None assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None) @@ -867,6 +888,7 @@ async def test_lock_unlock_lock(hass): async def test_lock_unlock_unlock(hass): """Test LockUnlock trait unlocking support for lock domain.""" + assert helpers.get_google_type(lock.DOMAIN, None) is not None assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN, None) @@ -905,6 +927,7 @@ async def test_lock_unlock_unlock(hass): async def test_fan_speed(hass): """Test FanSpeed trait speed control support for fan domain.""" + assert helpers.get_google_type(fan.DOMAIN, None) is not None assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None) @@ -988,6 +1011,7 @@ async def test_fan_speed(hass): async def test_modes(hass): """Test Mode trait.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None assert trait.ModesTrait.supported( media_player.DOMAIN, media_player.SUPPORT_SELECT_SOURCE, None) @@ -1076,6 +1100,7 @@ async def test_modes(hass): async def test_openclose_cover(hass): """Test OpenClose trait support for cover domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.OpenCloseTrait.supported(cover.DOMAIN, cover.SUPPORT_SET_POSITION, None) @@ -1137,6 +1162,8 @@ async def test_openclose_cover(hass): )) async def test_openclose_binary_sensor(hass, device_class): """Test OpenClose trait support for binary_sensor domain.""" + assert helpers.get_google_type( + binary_sensor.DOMAIN, device_class) is not None assert trait.OpenCloseTrait.supported(binary_sensor.DOMAIN, 0, device_class) From 6e4083d7f4f3e2dc5828fcbd81fb7295c2106233 Mon Sep 17 00:00:00 2001 From: Dries De Peuter Date: Thu, 18 Apr 2019 10:52:48 +0200 Subject: [PATCH 010/346] Fix niko home control dependency installation (#23176) * Upgrade niko-home-control library * Fix additional feedback * Lint --- .../components/niko_home_control/light.py | 37 +++++-------------- .../niko_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index aabee41694c..17b5f60cf44 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -7,7 +7,7 @@ import voluptuous as vol # Import the device class from the component that you want to support from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, Light) -from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL +from homeassistant.const import CONF_HOST from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle @@ -18,7 +18,6 @@ SCAN_INTERVAL = timedelta(seconds=30) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, }) @@ -56,27 +55,12 @@ class NikoHomeControlLight(Light): self._name = light.name self._state = light.is_on self._brightness = None - _LOGGER.debug("Init new light: %s", light.name) @property def unique_id(self): """Return unique ID for light.""" return self._unique_id - @property - def device_info(self): - """Return device info for light.""" - return { - 'identifiers': { - ('niko_home_control', self.unique_id) - }, - 'name': self.name, - 'manufacturer': 'Niko group nv', - 'model': 'Niko connected controller', - 'sw_version': self._data.info_swversion(self._light), - 'via_hub': ('niko_home_control'), - } - @property def name(self): """Return the display name of this light.""" @@ -92,16 +76,16 @@ class NikoHomeControlLight(Light): """Return true if light is on.""" return self._state - async def async_turn_on(self, **kwargs): + def turn_on(self, **kwargs): """Instruct the light to turn on.""" self._light.brightness = kwargs.get(ATTR_BRIGHTNESS, 255) _LOGGER.debug('Turn on: %s', self.name) - await self._data.hass.async_add_executor_job(self._light.turn_on) + self._light.turn_on() - async def async_turn_off(self, **kwargs): + def turn_off(self, **kwargs): """Instruct the light to turn off.""" _LOGGER.debug('Turn off: %s', self.name) - await self._data.hass.async_add_executor_job(self._light.turn_off) + self._light.turn_off() async def async_update(self): """Get the latest data from NikoHomeControl API.""" @@ -134,10 +118,7 @@ class NikoHomeControlData: def get_state(self, aid): """Find and filter state based on action id.""" - return next(filter(lambda a: a['id'] == aid, self.data))['value1'] != 0 - - def info_swversion(self, light): - """Return software version information.""" - if self._system_info is None: - self._system_info = self._nhc.system_info() - return self._system_info['swversion'] + for state in self.data: + if state['id'] == aid: + return state['value1'] != 0 + _LOGGER.error("Failed to retrieve state off unknown light") diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index c1c095f989a..8cb58a7b74c 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -3,7 +3,7 @@ "name": "Niko home control", "documentation": "https://www.home-assistant.io/components/niko_home_control", "requirements": [ - "niko-home-control==0.2.0" + "niko-home-control==0.2.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index b861e8e2a49..c4411eb1789 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -746,7 +746,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.niko_home_control -niko-home-control==0.2.0 +niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 From f588fef3b42ab6c487b3d539f28a4c073be3eb30 Mon Sep 17 00:00:00 2001 From: Rohan Kapoor Date: Thu, 18 Apr 2019 19:02:01 +0900 Subject: [PATCH 011/346] Add minimum/maximum to counter (#22608) * Added minimum/maximum to counter * Added min/max testcases * remove duplicate * cosmetic changes * removed blank lines at eof * added newline at eof * type cv -> vol * more fixes * - fixed min/max warnings - fixed failing tests * Added linewrap * - Added cast to int - Fixed double quotes * - removed None check in __init__ - fixed failing test * copy paste fix * copy paste fix * Added possibility to change counter properties trough service call * fixed copy paste errors * Added '.' to comment * rephrased docstring * Fix tests after rebase * Clean up per previous code review comments * Replace setup service with configure * Update services description * Update tests to use configure instead of setup --- homeassistant/components/counter/__init__.py | 75 ++++++-- .../components/counter/services.yaml | 17 +- tests/components/counter/test_init.py | 165 +++++++++++++++++- 3 files changed, 238 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index ab7ada618fe..53aa21c91c6 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,7 +3,9 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_ICON, CONF_NAME,\ + CONF_MAXIMUM, CONF_MINIMUM + import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -12,6 +14,8 @@ _LOGGER = logging.getLogger(__name__) ATTR_INITIAL = 'initial' ATTR_STEP = 'step' +ATTR_MINIMUM = 'minimum' +ATTR_MAXIMUM = 'maximum' CONF_INITIAL = 'initial' CONF_RESTORE = 'restore' @@ -26,11 +30,19 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' SERVICE_DECREMENT = 'decrement' SERVICE_INCREMENT = 'increment' SERVICE_RESET = 'reset' +SERVICE_CONFIGURE = 'configure' -SERVICE_SCHEMA = vol.Schema({ +SERVICE_SCHEMA_SIMPLE = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, }) +SERVICE_SCHEMA_CONFIGURE = vol.Schema({ + ATTR_ENTITY_ID: cv.comp_entity_ids, + vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), + vol.Optional(ATTR_STEP): cv.positive_int, +}) + CONFIG_SCHEMA = vol.Schema({ DOMAIN: cv.schema_with_slug_keys( vol.Any({ @@ -38,6 +50,10 @@ CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int, vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MAXIMUM, default=None): + vol.Any(None, vol.Coerce(int)), + vol.Optional(CONF_MINIMUM, default=None): + vol.Any(None, vol.Coerce(int)), vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, None) @@ -60,21 +76,27 @@ async def async_setup(hass, config): restore = cfg.get(CONF_RESTORE) step = cfg.get(CONF_STEP) icon = cfg.get(CONF_ICON) + minimum = cfg.get(CONF_MINIMUM) + maximum = cfg.get(CONF_MAXIMUM) - entities.append(Counter(object_id, name, initial, restore, step, icon)) + entities.append(Counter(object_id, name, initial, minimum, maximum, + restore, step, icon)) if not entities: return False component.async_register_entity_service( - SERVICE_INCREMENT, SERVICE_SCHEMA, + SERVICE_INCREMENT, SERVICE_SCHEMA_SIMPLE, 'async_increment') component.async_register_entity_service( - SERVICE_DECREMENT, SERVICE_SCHEMA, + SERVICE_DECREMENT, SERVICE_SCHEMA_SIMPLE, 'async_decrement') component.async_register_entity_service( - SERVICE_RESET, SERVICE_SCHEMA, + SERVICE_RESET, SERVICE_SCHEMA_SIMPLE, 'async_reset') + component.async_register_entity_service( + SERVICE_CONFIGURE, SERVICE_SCHEMA_CONFIGURE, + 'async_configure') await component.async_add_entities(entities) return True @@ -83,13 +105,16 @@ async def async_setup(hass, config): class Counter(RestoreEntity): """Representation of a counter.""" - def __init__(self, object_id, name, initial, restore, step, icon): + def __init__(self, object_id, name, initial, minimum, maximum, + restore, step, icon): """Initialize a counter.""" self.entity_id = ENTITY_ID_FORMAT.format(object_id) self._name = name self._restore = restore self._step = step self._state = self._initial = initial + self._min = minimum + self._max = maximum self._icon = icon @property @@ -115,10 +140,24 @@ class Counter(RestoreEntity): @property def state_attributes(self): """Return the state attributes.""" - return { + ret = { ATTR_INITIAL: self._initial, ATTR_STEP: self._step, } + if self._min is not None: + ret[CONF_MINIMUM] = self._min + if self._max is not None: + ret[CONF_MAXIMUM] = self._max + return ret + + def compute_next_state(self, state): + """Keep the state within the range of min/max values.""" + if self._min is not None: + state = max(self._min, state) + if self._max is not None: + state = min(self._max, state) + + return state async def async_added_to_hass(self): """Call when entity about to be added to Home Assistant.""" @@ -128,19 +167,31 @@ class Counter(RestoreEntity): if self._restore: state = await self.async_get_last_state() if state is not None: - self._state = int(state.state) + self._state = self.compute_next_state(int(state.state)) async def async_decrement(self): """Decrement the counter.""" - self._state -= self._step + self._state = self.compute_next_state(self._state - self._step) await self.async_update_ha_state() async def async_increment(self): """Increment a counter.""" - self._state += self._step + self._state = self.compute_next_state(self._state + self._step) await self.async_update_ha_state() async def async_reset(self): """Reset a counter.""" - self._state = self._initial + self._state = self.compute_next_state(self._initial) + await self.async_update_ha_state() + + async def async_configure(self, **kwargs): + """Change the counter's settings with a service.""" + if CONF_MINIMUM in kwargs: + self._min = kwargs[CONF_MINIMUM] + if CONF_MAXIMUM in kwargs: + self._max = kwargs[CONF_MAXIMUM] + if CONF_STEP in kwargs: + self._step = kwargs[CONF_STEP] + + self._state = self.compute_next_state(self._state) await self.async_update_ha_state() diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index ef76f9b9eac..fc3f0ad36cb 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -17,4 +17,19 @@ reset: fields: entity_id: description: Entity id of the counter to reset. - example: 'counter.count0' \ No newline at end of file + example: 'counter.count0' +configure: + description: Change counter parameters + fields: + entity_id: + description: Entity id of the counter to change. + example: 'counter.count0' + minimum: + description: New minimum value for the counter or None to remove minimum + example: 0 + maximum: + description: New maximum value for the counter or None to remove maximum + example: 100 + step: + description: New value for step + example: 2 diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 97a39cdeb73..4ed303474d5 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -3,13 +3,13 @@ import asyncio import logging -from homeassistant.core import CoreState, State, Context +from homeassistant.components.counter import (CONF_ICON, CONF_INITIAL, + CONF_NAME, CONF_RESTORE, + CONF_STEP, DOMAIN) +from homeassistant.const import (ATTR_FRIENDLY_NAME, ATTR_ICON) +from homeassistant.core import Context, CoreState, State from homeassistant.setup import async_setup_component -from homeassistant.components.counter import ( - DOMAIN, CONF_INITIAL, CONF_RESTORE, CONF_STEP, CONF_NAME, CONF_ICON) -from homeassistant.const import (ATTR_ICON, ATTR_FRIENDLY_NAME) - -from tests.common import mock_restore_cache +from tests.common import (mock_restore_cache) from tests.components.counter.common import ( async_decrement, async_increment, async_reset) @@ -243,3 +243,156 @@ async def test_counter_context(hass, hass_admin_user): assert state2 is not None assert state.state != state2.state assert state2.context.user_id == hass_admin_user.id + + +async def test_counter_min(hass, hass_admin_user): + """Test that min works.""" + assert await async_setup_component(hass, 'counter', { + 'counter': { + 'test': { + 'minimum': '0', + 'initial': '0' + } + } + }) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + + await hass.services.async_call('counter', 'decrement', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '0' + + await hass.services.async_call('counter', 'increment', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '1' + + +async def test_counter_max(hass, hass_admin_user): + """Test that max works.""" + assert await async_setup_component(hass, 'counter', { + 'counter': { + 'test': { + 'maximum': '0', + 'initial': '0' + } + } + }) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + + await hass.services.async_call('counter', 'increment', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '0' + + await hass.services.async_call('counter', 'decrement', { + 'entity_id': state.entity_id, + }, True, Context(user_id=hass_admin_user.id)) + + state2 = hass.states.get('counter.test') + assert state2 is not None + assert state2.state == '-1' + + +async def test_configure(hass, hass_admin_user): + """Test that setting values through configure works.""" + assert await async_setup_component(hass, 'counter', { + 'counter': { + 'test': { + 'maximum': '10', + 'initial': '10' + } + } + }) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '10' + assert 10 == state.attributes.get('maximum') + + # update max + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'maximum': 0, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + assert 0 == state.attributes.get('maximum') + + # disable max + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'maximum': None, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '0' + assert state.attributes.get('maximum') is None + + # update min + assert state.attributes.get('minimum') is None + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'minimum': 5, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert 5 == state.attributes.get('minimum') + + # disable min + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'minimum': None, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert state.attributes.get('minimum') is None + + # update step + assert 1 == state.attributes.get('step') + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'step': 3, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert 3 == state.attributes.get('step') + + # update all + await hass.services.async_call('counter', 'configure', { + 'entity_id': state.entity_id, + 'step': 5, + 'minimum': 0, + 'maximum': 9, + }, True, Context(user_id=hass_admin_user.id)) + + state = hass.states.get('counter.test') + assert state is not None + assert state.state == '5' + assert 5 == state.attributes.get('step') + assert 0 == state.attributes.get('minimum') + assert 9 == state.attributes.get('maximum') From df475cb797696057898e73138e5b0dd0860fe8d2 Mon Sep 17 00:00:00 2001 From: Pascal Roeleven Date: Thu, 18 Apr 2019 12:43:34 +0200 Subject: [PATCH 012/346] Adds Orange Pi GPIO platform (#22541) * Adds Orange Pi GPIO platform * Add manifest.json * Remove cover platform * Apply requested changes * Remove switch platform * Update CODEOWNERS * Remove obsolete dependecies/requirements --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/orangepi_gpio/__init__.py | 81 +++++++++++++++++++ .../components/orangepi_gpio/binary_sensor.py | 68 ++++++++++++++++ .../components/orangepi_gpio/const.py | 21 +++++ .../components/orangepi_gpio/manifest.json | 12 +++ requirements_all.txt | 3 + 7 files changed, 187 insertions(+) create mode 100644 homeassistant/components/orangepi_gpio/__init__.py create mode 100644 homeassistant/components/orangepi_gpio/binary_sensor.py create mode 100644 homeassistant/components/orangepi_gpio/const.py create mode 100644 homeassistant/components/orangepi_gpio/manifest.json diff --git a/.coveragerc b/.coveragerc index 86819ef51a3..cb0c50f72fe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -418,6 +418,7 @@ omit = homeassistant/components/openweathermap/sensor.py homeassistant/components/openweathermap/weather.py homeassistant/components/opple/light.py + homeassistant/components/orangepi_gpio/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 68720c2821b..aa4b5547d2c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -161,6 +161,7 @@ homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff +homeassistant/components/orangepi_gpio/* @pascallj homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/core homeassistant/components/panel_iframe/* @home-assistant/core diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py new file mode 100644 index 00000000000..072a05e0dd7 --- /dev/null +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -0,0 +1,81 @@ +"""Support for controlling GPIO pins of a Orange Pi.""" +import logging + +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) + +_LOGGER = logging.getLogger(__name__) + +CONF_PINMODE = 'pinmode' +DOMAIN = 'orangepi_gpio' +PINMODES = ['pc', 'zeroplus', 'zeroplus2', 'deo', 'neocore2'] + + +def setup(hass, config): + """Set up the Orange Pi GPIO component.""" + from OPi import GPIO + + def cleanup_gpio(event): + """Stuff to do before stopping.""" + GPIO.cleanup() + + def prepare_gpio(event): + """Stuff to do when home assistant starts.""" + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) + return True + + +def setup_mode(mode): + """Set GPIO pin mode.""" + from OPi import GPIO + + if mode == 'pc': + import orangepi.pc + GPIO.setmode(orangepi.pc.BOARD) + elif mode == 'zeroplus': + import orangepi.zeroplus + GPIO.setmode(orangepi.zeroplus.BOARD) + elif mode == 'zeroplus2': + import orangepi.zeroplus + GPIO.setmode(orangepi.zeroplus2.BOARD) + elif mode == 'duo': + import nanopi.duo + GPIO.setmode(nanopi.duo.BOARD) + elif mode == 'neocore2': + import nanopi.neocore2 + GPIO.setmode(nanopi.neocore2.BOARD) + + +def setup_output(port): + """Set up a GPIO as output.""" + from OPi import GPIO + GPIO.setup(port, GPIO.OUT) + + +def setup_input(port): + """Set up a GPIO as input.""" + from OPi import GPIO + GPIO.setup(port, GPIO.IN) + + +def write_output(port, value): + """Write a value to a GPIO.""" + from OPi import GPIO + GPIO.output(port, value) + + +def read_input(port): + """Read a value from a GPIO.""" + from OPi import GPIO + return GPIO.input(port) + + +def edge_detect(port, event_callback): + """Add detection for RISING and FALLING events.""" + from OPi import GPIO + GPIO.add_event_detect( + port, + GPIO.BOTH, + callback=event_callback) diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py new file mode 100644 index 00000000000..1c5a447b101 --- /dev/null +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -0,0 +1,68 @@ +"""Support for binary sensor using Orange Pi GPIO.""" +import logging + +from homeassistant.components import orangepi_gpio +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import DEVICE_DEFAULT_NAME + +from . import CONF_PINMODE +from .const import CONF_INVERT_LOGIC, CONF_PORTS, PORT_SCHEMA + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Orange Pi GPIO devices.""" + pinmode = config[CONF_PINMODE] + orangepi_gpio.setup_mode(pinmode) + + invert_logic = config[CONF_INVERT_LOGIC] + + binary_sensors = [] + ports = config[CONF_PORTS] + for port_num, port_name in ports.items(): + binary_sensors.append(OPiGPIOBinarySensor( + port_name, port_num, invert_logic)) + add_entities(binary_sensors, True) + + +class OPiGPIOBinarySensor(BinarySensorDevice): + """Represent a binary sensor that uses Orange Pi GPIO.""" + + def __init__(self, name, port, invert_logic): + """Initialize the Orange Pi binary sensor.""" + self._name = name or DEVICE_DEFAULT_NAME + self._port = port + self._invert_logic = invert_logic + self._state = None + + orangepi_gpio.setup_input(self._port) + + def read_gpio(port): + """Read state from GPIO.""" + self._state = orangepi_gpio.read_input(self._port) + self.schedule_update_ha_state() + + orangepi_gpio.edge_detect(self._port, read_gpio) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the entity.""" + return self._state != self._invert_logic + + def update(self): + """Update the GPIO state.""" + self._state = orangepi_gpio.read_input(self._port) diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py new file mode 100644 index 00000000000..422660f1f64 --- /dev/null +++ b/homeassistant/components/orangepi_gpio/const.py @@ -0,0 +1,21 @@ +"""Constants for Orange Pi GPIO.""" +import voluptuous as vol + +from homeassistant.helpers import config_validation as cv + +from . import CONF_PINMODE, PINMODES + +CONF_INVERT_LOGIC = 'invert_logic' +CONF_PORTS = 'ports' + +DEFAULT_INVERT_LOGIC = False + +_SENSORS_SCHEMA = vol.Schema({ + cv.positive_int: cv.string, +}) + +PORT_SCHEMA = { + vol.Required(CONF_PORTS): _SENSORS_SCHEMA, + vol.Required(CONF_PINMODE): vol.In(PINMODES), + vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, +} diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json new file mode 100644 index 00000000000..65fd0f7de50 --- /dev/null +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "orangepi_gpio", + "name": "Orangepi GPIO", + "documentation": "https://www.home-assistant.io/components/orangepi_gpio", + "requirements": [ + "OPi.GPIO==0.3.6" + ], + "dependencies": [], + "codeowners": [ + "@pascallj" + ] +} diff --git a/requirements_all.txt b/requirements_all.txt index c4411eb1789..316a21da33a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -38,6 +38,9 @@ HAP-python==2.5.0 # homeassistant.components.mastodon Mastodon.py==1.3.1 +# homeassistant.components.orangepi_gpio +OPi.GPIO==0.3.6 + # homeassistant.components.github PyGithub==1.43.5 From d9fb3c8c28cdd25f94d7246cb9e0fadf484078a6 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Thu, 18 Apr 2019 20:04:30 +0800 Subject: [PATCH 013/346] Potential None (#23187) --- homeassistant/components/automation/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index ed86d52584f..6371be28021 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -31,6 +31,6 @@ async def async_trigger(hass, config, action, automation_info): 'from_state': from_s, 'to_state': to_s, }, - }, context=to_s.context)) + }, context=(to_s.context if to_s else None))) return async_track_template(hass, value_template, template_listener) From 11fb4866a83164f1d7c5e1f745490919b3df470f Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Thu, 18 Apr 2019 13:37:52 +0100 Subject: [PATCH 014/346] Improve configuration schema for Geniushub integration (#23155) * configuration for hub tokens are now separate from host addresses/credentials * small change to docstring * use *args **kwargs --- .../components/geniushub/__init__.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 90e04db0111..aa57af55852 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,10 +1,10 @@ -"""This module connects to the Genius hub and shares the data.""" +"""This module connects to a Genius hub and shares the data.""" import logging import voluptuous as vol from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME) + CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform @@ -13,12 +13,19 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'geniushub' +_V1_API_SCHEMA = vol.Schema({ + vol.Required(CONF_TOKEN): cv.string, +}) +_V3_API_SCHEMA = vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +}) CONFIG_SCHEMA = vol.Schema({ - DOMAIN: vol.Schema({ - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Required(CONF_HOST): cv.string, - }), + DOMAIN: vol.Any( + _V3_API_SCHEMA, + _V1_API_SCHEMA, + ) }, extra=vol.ALLOW_EXTRA) @@ -26,16 +33,17 @@ async def async_setup(hass, hass_config): """Create a Genius Hub system.""" from geniushubclient import GeniusHubClient # noqa; pylint: disable=no-name-in-module - host = hass_config[DOMAIN].get(CONF_HOST) - username = hass_config[DOMAIN].get(CONF_USERNAME) - password = hass_config[DOMAIN].get(CONF_PASSWORD) - geniushub_data = hass.data[DOMAIN] = {} + kwargs = dict(hass_config[DOMAIN]) + if CONF_HOST in kwargs: + args = (kwargs.pop(CONF_HOST), ) + else: + args = (kwargs.pop(CONF_TOKEN), ) + try: client = geniushub_data['client'] = GeniusHubClient( - host, username, password, - session=async_get_clientsession(hass) + *args, **kwargs, session=async_get_clientsession(hass) ) await client.hub.update() From f57191e8dde2414b846dda2e734ac6bc54f1b9aa Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Thu, 18 Apr 2019 16:53:02 +0100 Subject: [PATCH 015/346] Hue motion senors are motion sensors, not presence sensors. (#23193) --- homeassistant/components/hue/binary_sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index d60750721ac..3286f185ea4 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,5 +1,6 @@ """Hue binary sensor entities.""" -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, DEVICE_CLASS_MOTION) from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, async_setup_entry as shared_async_setup_entry) @@ -16,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HuePresence(GenericZLLSensor, BinarySensorDevice): """The presence sensor entity for a Hue motion sensor device.""" - device_class = 'presence' + device_class = DEVICE_CLASS_MOTION async def _async_update_ha_state(self, *args, **kwargs): await self.async_update_ha_state(self, *args, **kwargs) From 4ac9a2e9debc11532b9b1472916ce4e12d4b7032 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 18 Apr 2019 16:55:34 +0100 Subject: [PATCH 016/346] Add storage for cacheable homekit entity maps. (#23191) --- .../components/homekit_controller/__init__.py | 28 +++-- .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/connection.py | 100 +++++++++++++--- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/storage.py | 80 +++++++++++++ tests/components/homekit_controller/common.py | 1 + .../specific_devices/test_ecobee3.py | 76 +++++++++++- .../homekit_controller/test_config_flow.py | 2 +- .../homekit_controller/test_storage.py | 112 ++++++++++++++++++ 9 files changed, 373 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/homekit_controller/storage.py create mode 100644 tests/components/homekit_controller/test_storage.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 11026d7e9ac..3fa4ade519e 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -8,9 +8,10 @@ from homeassistant.helpers.entity import Entity from .config_flow import load_old_pairings from .connection import get_accessory_information, HKDevice from .const import ( - CONTROLLER, KNOWN_DEVICES + CONTROLLER, ENTITY_MAP, KNOWN_DEVICES ) from .const import DOMAIN # noqa: pylint: disable=unused-import +from .storage import EntityMapStorage HOMEKIT_IGNORE = [ 'BSB002', @@ -44,7 +45,7 @@ class HomeKitEntity(Entity): # pylint: disable=import-error from homekit.model.characteristics import CharacteristicsTypes - pairing_data = self._accessory.pairing.pairing_data + accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid characteristic_types = [ @@ -55,7 +56,7 @@ class HomeKitEntity(Entity): self._chars = {} self._char_names = {} - for accessory in pairing_data.get('accessories', []): + for accessory in accessories: if accessory['aid'] != self._aid: continue self._accessory_info = get_accessory_information(accessory) @@ -149,12 +150,15 @@ class HomeKitEntity(Entity): raise NotImplementedError -def setup(hass, config): +async def async_setup(hass, config): """Set up for Homekit devices.""" # pylint: disable=import-error import homekit from homekit.controller.ip_implementation import IpPairing + map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) + await map_storage.async_initialize() + hass.data[CONTROLLER] = controller = homekit.Controller() for hkid, pairing_data in load_old_pairings(hass).items(): @@ -185,12 +189,22 @@ def setup(hass, config): device = hass.data[KNOWN_DEVICES][hkid] if config_num > device.config_num and \ device.pairing is not None: - device.accessory_setup() + device.refresh_entity_map(config_num) return _LOGGER.debug('Discovered unique device %s', hkid) - HKDevice(hass, host, port, model, hkid, config_num, config) + device = HKDevice(hass, host, port, model, hkid, config_num, config) + device.setup() hass.data[KNOWN_DEVICES] = {} - discovery.listen(hass, SERVICE_HOMEKIT, discovery_dispatch) + + await hass.async_add_executor_job( + discovery.listen, hass, SERVICE_HOMEKIT, discovery_dispatch) + return True + + +async def async_remove_entry(hass, entry): + """Cleanup caches before removing config entry.""" + hkid = entry.data['AccessoryPairingID'] + hass.data[ENTITY_MAP].async_delete_map(hkid) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 1cd66896fe2..a6c5ac8b36d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -152,7 +152,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): "HomeKit info %s: c# incremented, refreshing entities", hkid) self.hass.async_create_task( - conn.async_config_num_changed(config_num)) + conn.async_refresh_entity_map(config_num)) return self.async_abort(reason='already_configured') old_pairings = await self.hass.async_add_executor_job( diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 2ca568b547f..2b82370d187 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -4,11 +4,10 @@ import logging import os from homeassistant.helpers import discovery -from homeassistant.helpers.event import call_later from .const import ( CONTROLLER, DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, KNOWN_DEVICES, - PAIRING_FILE, HOMEKIT_DIR + PAIRING_FILE, HOMEKIT_DIR, ENTITY_MAP ) @@ -67,7 +66,7 @@ class HKDevice(): self.config_num = config_num self.config = config self.configurator = hass.components.configurator - self._connection_warning_logged = False + self.accessories = {} # This just tracks aid/iid pairs so we know if a HK service has been # mapped to a HA entity. @@ -79,27 +78,77 @@ class HKDevice(): hass.data[KNOWN_DEVICES][hkid] = self - if self.pairing is not None: - self.accessory_setup() - else: + def setup(self): + """Prepare to use a paired HomeKit device in homeassistant.""" + if self.pairing is None: self.configure() - - def accessory_setup(self): - """Handle setup of a HomeKit accessory.""" - # pylint: disable=import-error - from homekit.model.services import ServicesTypes - from homekit.exceptions import AccessoryDisconnectedError + return self.pairing.pairing_data['AccessoryIP'] = self.host self.pairing.pairing_data['AccessoryPort'] = self.port + cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) + if not cache or cache['config_num'] < self.config_num: + return self.refresh_entity_map(self.config_num) + + self.accessories = cache['accessories'] + + # Ensure the Pairing object has access to the latest version of the + # entity map. + self.pairing.pairing_data['accessories'] = self.accessories + + self.add_entities() + + return True + + async def async_refresh_entity_map(self, config_num): + """ + Handle setup of a HomeKit accessory. + + The sync version will be removed when homekit_controller migrates to + config flow. + """ + return await self.hass.async_add_executor_job( + self.refresh_entity_map, + config_num, + ) + + def refresh_entity_map(self, config_num): + """Handle setup of a HomeKit accessory.""" + # pylint: disable=import-error + from homekit.exceptions import AccessoryDisconnectedError + try: - data = self.pairing.list_accessories_and_characteristics() + accessories = self.pairing.list_accessories_and_characteristics() except AccessoryDisconnectedError: - call_later( - self.hass, RETRY_INTERVAL, lambda _: self.accessory_setup()) + # If we fail to refresh this data then we will naturally retry + # later when Bonjour spots c# is still not up to date. return - for accessory in data: + + self.hass.data[ENTITY_MAP].async_create_or_update_map( + self.unique_id, + config_num, + accessories, + ) + + self.accessories = accessories + self.config_num = config_num + + # For BLE, the Pairing instance relies on the entity map to map + # aid/iid to GATT characteristics. So push it to there as well. + self.pairing.pairing_data['accessories'] = accessories + + # Register add new entities that are available + self.add_entities() + + return True + + def add_entities(self): + """Process the entity map and create HA entities.""" + # pylint: disable=import-error + from homekit.model.services import ServicesTypes + + for accessory in self.accessories: aid = accessory['aid'] for service in accessory['services']: iid = service['iid'] @@ -118,6 +167,7 @@ class HKDevice(): if component is not None: discovery.load_platform(self.hass, component, DOMAIN, service_info, self.config) + self.entities.append((aid, iid)) def device_config_callback(self, callback_data): """Handle initial pairing.""" @@ -145,15 +195,20 @@ class HKDevice(): self.pairing = self.controller.pairings.get(self.hkid) if self.pairing is not None: - pairing_file = os.path.join( + pairing_dir = os.path.join( self.hass.config.path(), HOMEKIT_DIR, + ) + if not os.path.exists(pairing_dir): + os.makedirs(pairing_dir) + pairing_file = os.path.join( + pairing_dir, PAIRING_FILE, ) self.controller.save_data(pairing_file) _configurator = self.hass.data[DOMAIN+self.hkid] self.configurator.request_done(_configurator) - self.accessory_setup() + self.setup() else: error_msg = "Unable to pair, please try again" _configurator = self.hass.data[DOMAIN+self.hkid] @@ -197,3 +252,12 @@ class HKDevice(): self.pairing.put_characteristics, chars ) + + @property + def unique_id(self): + """ + Return a unique id for this accessory or bridge. + + This id is random and will change if a device undergoes a hard reset. + """ + return self.hkid diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index de9663f1202..f112737ca24 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -3,6 +3,7 @@ DOMAIN = 'homekit_controller' KNOWN_DEVICES = "{}-devices".format(DOMAIN) CONTROLLER = "{}-controller".format(DOMAIN) +ENTITY_MAP = '{}-entity-map'.format(DOMAIN) HOMEKIT_DIR = '.homekit' PAIRING_FILE = 'pairing.json' diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py new file mode 100644 index 00000000000..4a7c0a8057b --- /dev/null +++ b/homeassistant/components/homekit_controller/storage.py @@ -0,0 +1,80 @@ +"""Helpers for HomeKit data stored in HA storage.""" + +from homeassistant.helpers.storage import Store +from homeassistant.core import callback + +from .const import DOMAIN + +ENTITY_MAP_STORAGE_KEY = '{}-entity-map'.format(DOMAIN) +ENTITY_MAP_STORAGE_VERSION = 1 +ENTITY_MAP_SAVE_DELAY = 10 + + +class EntityMapStorage: + """ + Holds a cache of entity structure data from a paired HomeKit device. + + HomeKit has a cacheable entity map that describes how an IP or BLE + endpoint is structured. This object holds the latest copy of that data. + + An endpoint is made of accessories, services and characteristics. It is + safe to cache this data until the c# discovery data changes. + + Caching this data means we can add HomeKit devices to HA immediately at + start even if discovery hasn't seen them yet or they are out of range. It + is also important for BLE devices - accessing the entity structure is + very slow for these devices. + """ + + def __init__(self, hass): + """Create a new entity map store.""" + self.hass = hass + self.store = Store( + hass, + ENTITY_MAP_STORAGE_VERSION, + ENTITY_MAP_STORAGE_KEY + ) + self.storage_data = {} + + async def async_initialize(self): + """Get the pairing cache data.""" + raw_storage = await self.store.async_load() + if not raw_storage: + # There is no cached data about HomeKit devices yet + return + + self.storage_data = raw_storage.get('pairings', {}) + + def get_map(self, homekit_id): + """Get a pairing cache item.""" + return self.storage_data.get(homekit_id) + + def async_create_or_update_map(self, homekit_id, config_num, accessories): + """Create a new pairing cache.""" + data = { + 'config_num': config_num, + 'accessories': accessories, + } + self.storage_data[homekit_id] = data + self._async_schedule_save() + return data + + def async_delete_map(self, homekit_id): + """Delete pairing cache.""" + if homekit_id not in self.storage_data: + return + + self.storage_data.pop(homekit_id) + self._async_schedule_save() + + @callback + def _async_schedule_save(self): + """Schedule saving the entity map cache.""" + self.store.async_delay_save(self._data_to_save, ENTITY_MAP_SAVE_DELAY) + + @callback + def _data_to_save(self): + """Return data of entity map to store in a file.""" + return { + 'pairings': self.storage_data, + } diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 5ad197f8294..5d85fba6ae3 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -249,6 +249,7 @@ async def device_config_changed(hass, accessories): # Wait for services to reconfigure await hass.async_block_till_done() + await hass.async_block_till_done() async def setup_test_component(hass, services, capitalize=False, suffix=None): diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 0831cd5b780..a7e449ddbe4 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -4,12 +4,16 @@ Regression tests for Ecobee 3. https://github.com/home-assistant/home-assistant/issues/15336 """ +from unittest import mock + +from homekit import AccessoryDisconnectedError + from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, SUPPORT_OPERATION_MODE) from tests.components.homekit_controller.common import ( - device_config_changed, setup_accessories_from_file, setup_test_accessories, - Helper + FakePairing, device_config_changed, setup_accessories_from_file, + setup_test_accessories, Helper ) @@ -46,6 +50,74 @@ async def test_ecobee3_setup(hass): assert occ3.unique_id == 'homekit-AB3C-56' +async def test_ecobee3_setup_from_cache(hass, hass_storage): + """Test that Ecbobee can be correctly setup from its cached entity map.""" + accessories = await setup_accessories_from_file(hass, 'ecobee3.json') + + hass_storage['homekit_controller-entity-map'] = { + 'version': 1, + 'data': { + 'pairings': { + '00:00:00:00:00:00': { + 'config_num': 1, + 'accessories': [ + a.to_accessory_and_service_list() for a in accessories + ], + } + } + } + } + + await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + climate = entity_registry.async_get('climate.homew') + assert climate.unique_id == 'homekit-123456789012-16' + + occ1 = entity_registry.async_get('binary_sensor.kitchen') + assert occ1.unique_id == 'homekit-AB1C-56' + + occ2 = entity_registry.async_get('binary_sensor.porch') + assert occ2.unique_id == 'homekit-AB2C-56' + + occ3 = entity_registry.async_get('binary_sensor.basement') + assert occ3.unique_id == 'homekit-AB3C-56' + + +async def test_ecobee3_setup_connection_failure(hass): + """Test that Ecbobee can be correctly setup from its cached entity map.""" + accessories = await setup_accessories_from_file(hass, 'ecobee3.json') + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Test that the connection fails during initial setup. + # No entities should be created. + list_accessories = 'list_accessories_and_characteristics' + with mock.patch.object(FakePairing, list_accessories) as laac: + laac.side_effect = AccessoryDisconnectedError('Connection failed') + await setup_test_accessories(hass, accessories) + + climate = entity_registry.async_get('climate.homew') + assert climate is None + + # When a regular discovery event happens it should trigger another scan + # which should cause our entities to be added. + await device_config_changed(hass, accessories) + + climate = entity_registry.async_get('climate.homew') + assert climate.unique_id == 'homekit-123456789012-16' + + occ1 = entity_registry.async_get('binary_sensor.kitchen') + assert occ1.unique_id == 'homekit-AB1C-56' + + occ2 = entity_registry.async_get('binary_sensor.porch') + assert occ2.unique_id == 'homekit-AB2C-56' + + occ3 = entity_registry.async_get('binary_sensor.basement') + assert occ3.unique_id == 'homekit-AB3C-56' + + async def test_ecobee3_add_sensors_at_runtime(hass): """Test that new sensors are automatically added.""" entity_registry = await hass.helpers.entity_registry.async_get_registry() diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 62c741b4eaa..da4176e1edc 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -295,7 +295,7 @@ async def test_discovery_already_configured_config_change(hass): assert result['type'] == 'abort' assert result['reason'] == 'already_configured' - assert conn.async_config_num_changed.call_args == mock.call(2) + assert conn.async_refresh_entity_map.call_args == mock.call(2) async def test_pair_unable_to_pair(hass): diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py new file mode 100644 index 00000000000..43b8cba885a --- /dev/null +++ b/tests/components/homekit_controller/test_storage.py @@ -0,0 +1,112 @@ +"""Basic checks for entity map storage.""" +from tests.common import flush_store +from tests.components.homekit_controller.common import ( + FakeService, setup_test_component, setup_platform) + +from homeassistant import config_entries +from homeassistant.components.homekit_controller import async_remove_entry +from homeassistant.components.homekit_controller.const import ENTITY_MAP + + +async def test_load_from_storage(hass, hass_storage): + """Test that entity map can be correctly loaded from cache.""" + hkid = '00:00:00:00:00:00' + + hass_storage['homekit_controller-entity-map'] = { + 'version': 1, + 'data': { + 'pairings': { + hkid: { + 'c#': 1, + 'accessories': [], + } + } + } + } + + await setup_platform(hass) + assert hkid in hass.data[ENTITY_MAP].storage_data + + +async def test_storage_is_removed(hass, hass_storage): + """Test entity map storage removal is idempotent.""" + await setup_platform(hass) + + entity_map = hass.data[ENTITY_MAP] + hkid = '00:00:00:00:00:01' + + entity_map.async_create_or_update_map( + hkid, + 1, + [], + ) + assert hkid in entity_map.storage_data + await flush_store(entity_map.store) + assert hkid in hass_storage[ENTITY_MAP]['data']['pairings'] + + entity_map.async_delete_map(hkid) + assert hkid not in hass.data[ENTITY_MAP].storage_data + await flush_store(entity_map.store) + + assert hass_storage[ENTITY_MAP]['data']['pairings'] == {} + + +async def test_storage_is_removed_idempotent(hass): + """Test entity map storage removal is idempotent.""" + await setup_platform(hass) + + entity_map = hass.data[ENTITY_MAP] + hkid = '00:00:00:00:00:01' + + assert hkid not in entity_map.storage_data + + entity_map.async_delete_map(hkid) + + assert hkid not in entity_map.storage_data + + +def create_lightbulb_service(): + """Define lightbulb characteristics.""" + service = FakeService('public.hap.service.lightbulb') + on_char = service.add_characteristic('on') + on_char.value = 0 + return service + + +async def test_storage_is_updated_on_add(hass, hass_storage, utcnow): + """Test entity map storage is cleaned up on adding an accessory.""" + bulb = create_lightbulb_service() + await setup_test_component(hass, [bulb]) + + entity_map = hass.data[ENTITY_MAP] + hkid = '00:00:00:00:00:00' + + # Is in memory store updated? + assert hkid in entity_map.storage_data + + # Is saved out to store? + await flush_store(entity_map.store) + assert hkid in hass_storage[ENTITY_MAP]['data']['pairings'] + + +async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): + """Test entity map storage is cleaned up on config entry removal.""" + bulb = create_lightbulb_service() + await setup_test_component(hass, [bulb]) + + hkid = '00:00:00:00:00:00' + + pairing_data = { + 'AccessoryPairingID': hkid, + } + + entry = config_entries.ConfigEntry( + 1, 'homekit_controller', 'TestData', pairing_data, + 'test', config_entries.CONN_CLASS_LOCAL_PUSH + ) + + assert hkid in hass.data[ENTITY_MAP].storage_data + + await async_remove_entry(hass, entry) + + assert hkid not in hass.data[ENTITY_MAP].storage_data From 5e1338a9e41e8824f1a833775434c929ea36bc2c Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 18 Apr 2019 09:03:25 -0700 Subject: [PATCH 017/346] Further improve IndieAuth redirect_uri lookup failure logs (#23183) --- homeassistant/components/auth/indieauth.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 1437685692b..a56671c9dcd 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -6,7 +6,6 @@ from html.parser import HTMLParser from urllib.parse import urlparse, urljoin import aiohttp -from aiohttp.client_exceptions import ClientError from homeassistant.util.network import is_local @@ -81,8 +80,22 @@ async def fetch_redirect_uris(hass, url): if chunks == 10: break - except (asyncio.TimeoutError, ClientError) as ex: - _LOGGER.error("Error while looking up redirect_uri %s: %s", url, ex) + except asyncio.TimeoutError: + _LOGGER.error("Timeout while looking up redirect_uri %s", url) + pass + except aiohttp.client_exceptions.ClientSSLError: + _LOGGER.error("SSL error while looking up redirect_uri %s", url) + pass + except aiohttp.client_exceptions.ClientOSError as ex: + _LOGGER.error("OS error while looking up redirect_uri %s: %s", url, + ex.strerror) + pass + except aiohttp.client_exceptions.ClientConnectionError: + _LOGGER.error(("Low level connection error while looking up " + "redirect_uri %s"), url) + pass + except aiohttp.client_exceptions.ClientError: + _LOGGER.error("Unknown error while looking up redirect_uri %s", url) pass # Authorization endpoints verifying that a redirect_uri is allowed for use From 38d23ba0af5fbbe3be8d3c488afe69a8e7f8d39c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 18 Apr 2019 12:24:02 -0400 Subject: [PATCH 018/346] Misc. ZHA changes (#23190) * handle the off part of on with timed off command * use correct var * only bind / configure cluster once * clean up channel configuration * additional debug logging * add guard * prevent multiple discoveries for a device * cleanup and still configure on rejoin --- .../components/zha/core/channels/__init__.py | 5 ++ .../components/zha/core/channels/general.py | 20 ++++++- homeassistant/components/zha/core/device.py | 54 ++++++++++++++++--- homeassistant/components/zha/core/gateway.py | 45 +++++++++------- 4 files changed, 99 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 10370c42c66..1845ae8e999 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -167,6 +167,11 @@ class ZigbeeChannel: async def async_initialize(self, from_cache): """Initialize channel.""" + _LOGGER.debug( + 'initializing channel: %s from_cache: %s', + self._channel_name, + from_cache + ) self._status = ChannelStatus.INITIALIZED @callback diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 061541d4dae..b5509b1d559 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -7,6 +7,7 @@ https://home-assistant.io/components/zha/ import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later from . import ZigbeeChannel, parse_and_log_command, MAINS_POWERED from ..helpers import get_attr_id_by_name from ..const import ( @@ -40,11 +41,28 @@ class OnOffChannel(ZigbeeChannel): if cmd in ('off', 'off_with_effect'): self.attribute_updated(self.ON_OFF, False) - elif cmd in ('on', 'on_with_recall_global_scene', 'on_with_timed_off'): + elif cmd in ('on', 'on_with_recall_global_scene'): self.attribute_updated(self.ON_OFF, True) + elif cmd == 'on_with_timed_off': + should_accept = args[0] + on_time = args[1] + # 0 is always accept 1 is only accept when already on + if should_accept == 0 or (should_accept == 1 and self._state): + self.attribute_updated(self.ON_OFF, True) + if on_time > 0: + async_call_later( + self.device.hass, + (on_time / 10), # value is in 10ths of a second + self.set_to_off + ) elif cmd == 'toggle': self.attribute_updated(self.ON_OFF, not bool(self._state)) + @callback + def set_to_off(self, *_): + """Set the state to off.""" + self.attribute_updated(self.ON_OFF, False) + @callback def attribute_updated(self, attrid, value): """Handle attribute updates on this cluster.""" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 435ab25acc6..74e3c7bcc46 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -200,10 +200,50 @@ class ZHADevice: self.cluster_channels[cluster_channel.name] = cluster_channel self._all_channels.append(cluster_channel) + def get_channels_to_configure(self): + """Get a deduped list of channels for configuration. + + This goes through all channels and gets a unique list of channels to + configure. It first assembles a unique list of channels that are part + of entities while stashing relay channels off to the side. It then + takse the stashed relay channels and adds them to the list of channels + that will be returned if there isn't a channel in the list for that + cluster already. This is done to ensure each cluster is only configured + once. + """ + channel_keys = [] + channels = [] + relay_channels = self._relay_channels.values() + + def get_key(channel): + channel_key = "ZDO" + if hasattr(channel.cluster, 'cluster_id'): + channel_key = "{}_{}".format( + channel.cluster.endpoint.endpoint_id, + channel.cluster.cluster_id + ) + return channel_key + + # first we get all unique non event channels + for channel in self.all_channels: + c_key = get_key(channel) + if c_key not in channel_keys and channel not in relay_channels: + channel_keys.append(c_key) + channels.append(channel) + + # now we get event channels that still need their cluster configured + for channel in relay_channels: + channel_key = get_key(channel) + if channel_key not in channel_keys: + channel_keys.append(channel_key) + channels.append(channel) + return channels + async def async_configure(self): """Configure the device.""" _LOGGER.debug('%s: started configuration', self.name) - await self._execute_channel_tasks('async_configure') + await self._execute_channel_tasks( + self.get_channels_to_configure(), 'async_configure') _LOGGER.debug('%s: completed configuration', self.name) entry = self.gateway.zha_storage.async_create_or_update(self) _LOGGER.debug('%s: stored in registry: %s', self.name, entry) @@ -211,7 +251,8 @@ class ZHADevice: async def async_initialize(self, from_cache=False): """Initialize channels.""" _LOGGER.debug('%s: started initialization', self.name) - await self._execute_channel_tasks('async_initialize', from_cache) + await self._execute_channel_tasks( + self.all_channels, 'async_initialize', from_cache) _LOGGER.debug( '%s: power source: %s', self.name, @@ -220,16 +261,17 @@ class ZHADevice: self.status = DeviceStatus.INITIALIZED _LOGGER.debug('%s: completed initialization', self.name) - async def _execute_channel_tasks(self, task_name, *args): + async def _execute_channel_tasks(self, channels, task_name, *args): """Gather and execute a set of CHANNEL tasks.""" channel_tasks = [] semaphore = asyncio.Semaphore(3) zdo_task = None - for channel in self.all_channels: + for channel in channels: if channel.name == ZDO_CHANNEL: # pylint: disable=E1111 - zdo_task = self._async_create_task( - semaphore, channel, task_name, *args) + if zdo_task is None: # We only want to do this once + zdo_task = self._async_create_task( + semaphore, channel, task_name, *args) else: channel_tasks.append( self._async_create_task( diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 17c7c6f878f..4a16bfe5004 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -259,17 +259,25 @@ class ZHAGateway: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device, is_new_join) - discovery_infos = [] - for endpoint_id, endpoint in device.endpoints.items(): - async_process_endpoint( - self._hass, self._config, endpoint_id, endpoint, - discovery_infos, device, zha_device, is_new_join + is_rejoin = False + if zha_device.status is not DeviceStatus.INITIALIZED: + discovery_infos = [] + for endpoint_id, endpoint in device.endpoints.items(): + async_process_endpoint( + self._hass, self._config, endpoint_id, endpoint, + discovery_infos, device, zha_device, is_new_join + ) + if endpoint_id != 0: + for cluster in endpoint.in_clusters.values(): + cluster.bind_only = False + for cluster in endpoint.out_clusters.values(): + cluster.bind_only = True + else: + is_rejoin = is_new_join is True + _LOGGER.debug( + 'skipping discovery for previously discovered device: %s', + "{} - is rejoin: {}".format(zha_device.ieee, is_rejoin) ) - if endpoint_id != 0: - for cluster in endpoint.in_clusters.values(): - cluster.bind_only = False - for cluster in endpoint.out_clusters.values(): - cluster.bind_only = True if is_new_join: # configure the device @@ -290,15 +298,16 @@ class ZHAGateway: else: await zha_device.async_initialize(from_cache=True) - for discovery_info in discovery_infos: - async_dispatch_discovery_info( - self._hass, - is_new_join, - discovery_info - ) + if not is_rejoin: + for discovery_info in discovery_infos: + async_dispatch_discovery_info( + self._hass, + is_new_join, + discovery_info + ) - device_entity = async_create_device_entity(zha_device) - await self._component.async_add_entities([device_entity]) + device_entity = async_create_device_entity(zha_device) + await self._component.async_add_entities([device_entity]) if is_new_join: device_info = async_get_device_info(self._hass, zha_device) From c2cce13e2a6d56369d3ddb8efe6739ce9acaedd7 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 18 Apr 2019 11:11:26 -0700 Subject: [PATCH 019/346] Migrating codeowners-mention to Heroku --- .github/main.workflow | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .github/main.workflow diff --git a/.github/main.workflow b/.github/main.workflow deleted file mode 100644 index 62336ebf126..00000000000 --- a/.github/main.workflow +++ /dev/null @@ -1,14 +0,0 @@ -workflow "Mention CODEOWNERS of integrations when integration label is added to an issue" { - on = "issues" - resolves = "codeowners-mention" -} - -workflow "Mention CODEOWNERS of integrations when integration label is added to an PRs" { - on = "pull_request" - resolves = "codeowners-mention" -} - -action "codeowners-mention" { - uses = "home-assistant/codeowners-mention@master" - secrets = ["GITHUB_TOKEN"] -} From fda483f4824cd0d72c1b1ecce594eb6abdff260b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Apr 2019 11:11:43 -0700 Subject: [PATCH 020/346] Don't load component when fetching translations (#23196) --- homeassistant/helpers/translation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 24e6f4f390d..4f655e692f7 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -52,11 +52,9 @@ async def component_translation_file(hass: HomeAssistantType, component: str, filename = "{}.{}.json".format(parts[0], language) return str(integration.file_path / '.translations' / filename) - module = integration.get_component() - # If it's a component that is just one file, we don't support translations # Example custom_components/my_component.py - if module.__name__ != module.__package__: + if integration.file_path.name != domain: return None filename = '{}.json'.format(language) From daf2f30822d643fcd6684ce225fb84dc3c4d05f6 Mon Sep 17 00:00:00 2001 From: Florian Klien Date: Thu, 18 Apr 2019 21:26:02 +0200 Subject: [PATCH 021/346] set myself as codeowner of xmpp, removed me from notify/* (#23207) * set myself as codeowner of xmpp, removed me from notify/* * changed the manifests as well --- CODEOWNERS | 4 ++-- homeassistant/components/notify/manifest.json | 2 +- homeassistant/components/xmpp/manifest.json | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index aa4b5547d2c..e5d009cc7a5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -154,7 +154,7 @@ homeassistant/components/netdata/* @fabaff homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff -homeassistant/components/notify/* @flowolf +homeassistant/components/notify/* @home-assistant/core homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nuki/* @pschmitt homeassistant/components/ohmconnect/* @robbiet480 @@ -247,7 +247,7 @@ homeassistant/components/xfinity/* @cisasteelersfan homeassistant/components/xiaomi_aqara/* @danielhiversen @syssi homeassistant/components/xiaomi_miio/* @rytilahti @syssi homeassistant/components/xiaomi_tv/* @fattdev -homeassistant/components/xmpp/* @fabaff +homeassistant/components/xmpp/* @fabaff @flowolf homeassistant/components/yamaha_musiccast/* @jalmeroth homeassistant/components/yeelight/* @rytilahti @zewelor homeassistant/components/yeelightsunflower/* @lindsaymarkward diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index 22c85723cb8..bad39a1cb97 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -5,6 +5,6 @@ "requirements": [], "dependencies": [], "codeowners": [ - "@flowolf" + "@home-assistant/core" ] } diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index d8e4e5c4da6..3d2c3a5e911 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -7,6 +7,7 @@ ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@flowolf" ] } From 70c5bd4316c04e5573963f5b1018db340bbc5b9b Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 18 Apr 2019 16:09:42 -0400 Subject: [PATCH 022/346] Create services.yaml for Tuya (#23209) --- homeassistant/components/tuya/services.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 homeassistant/components/tuya/services.yaml diff --git a/homeassistant/components/tuya/services.yaml b/homeassistant/components/tuya/services.yaml new file mode 100644 index 00000000000..c96ea3fd09f --- /dev/null +++ b/homeassistant/components/tuya/services.yaml @@ -0,0 +1,7 @@ +# Describes the format for available Tuya services + +pull_devices: + description: Pull device list from Tuya server. + +force_update: + description: Force all Tuya devices to pull data. From 4be30f7c88684b1f109319ba95fbf7c42e87aacd Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 18 Apr 2019 16:10:10 -0400 Subject: [PATCH 023/346] create services.yaml for shell_command (#23210) --- homeassistant/components/shell_command/services.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 homeassistant/components/shell_command/services.yaml diff --git a/homeassistant/components/shell_command/services.yaml b/homeassistant/components/shell_command/services.yaml new file mode 100644 index 00000000000..df056f94e85 --- /dev/null +++ b/homeassistant/components/shell_command/services.yaml @@ -0,0 +1 @@ +# Empty file, shell_command services are dynamically created From 0eb8c77889f065ad5ea3f046265c1f87fd5eada9 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Thu, 18 Apr 2019 16:10:25 -0400 Subject: [PATCH 024/346] Create services.yaml for python_script and script (#23201) * Create services.yaml for python_script * Create services.yaml for script --- .../components/python_script/services.yaml | 4 +++ homeassistant/components/script/services.yaml | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 homeassistant/components/python_script/services.yaml create mode 100644 homeassistant/components/script/services.yaml diff --git a/homeassistant/components/python_script/services.yaml b/homeassistant/components/python_script/services.yaml new file mode 100644 index 00000000000..835f6402481 --- /dev/null +++ b/homeassistant/components/python_script/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available python_script services + +reload: + description: Reload all available python_scripts diff --git a/homeassistant/components/script/services.yaml b/homeassistant/components/script/services.yaml new file mode 100644 index 00000000000..736b0ec71c3 --- /dev/null +++ b/homeassistant/components/script/services.yaml @@ -0,0 +1,25 @@ +# Describes the format for available python_script services + +reload: + description: Reload all the available scripts + +turn_on: + description: Turn on script + fields: + entity_id: + description: Name(s) of script to be turned on. + example: 'script.arrive_home' + +turn_off: + description: Turn off script + fields: + entity_id: + description: Name(s) of script to be turned off. + example: 'script.arrive_home' + +toggle: + description: Toggle script + fields: + entity_id: + description: Name(s) of script to be toggled. + example: 'script.arrive_home' From 37cd711c9688dbd7752d217726d94d77011f566e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 18 Apr 2019 22:10:36 +0200 Subject: [PATCH 025/346] Create empty services.yaml for esphome (#23200) --- homeassistant/components/esphome/services.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 homeassistant/components/esphome/services.yaml diff --git a/homeassistant/components/esphome/services.yaml b/homeassistant/components/esphome/services.yaml new file mode 100644 index 00000000000..f4c31420f9a --- /dev/null +++ b/homeassistant/components/esphome/services.yaml @@ -0,0 +1 @@ +# Empty file, ESPHome services are dynamically created (user-defined services) From 33b8241d2668b663f18cbae90e9733360c1a0644 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Apr 2019 13:40:46 -0700 Subject: [PATCH 026/346] Add services.yaml validator (#23205) * Add services.yaml validator * Fix path --- .../components/climate/services.yaml | 1 - homeassistant/components/deconz/services.yaml | 7 +- .../components/device_tracker/services.yaml | 73 ++++++------ .../components/hdmi_cec/services.yaml | 2 +- .../components/system_log/services.yaml | 30 ++--- homeassistant/components/zwave/services.yaml | 10 +- script/hassfest/__main__.py | 4 +- script/hassfest/model.py | 12 +- script/hassfest/services.py | 104 ++++++++++++++++++ 9 files changed, 172 insertions(+), 71 deletions(-) create mode 100644 script/hassfest/services.py diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 1460181ddc2..c0dd231ef95 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -80,7 +80,6 @@ set_swing_mode: example: 'climate.nest' swing_mode: description: New value of swing mode. - example: turn_on: description: Turn climate device on. diff --git a/homeassistant/components/deconz/services.yaml b/homeassistant/components/deconz/services.yaml index a39bbc01ea1..4d77101cf0d 100644 --- a/homeassistant/components/deconz/services.yaml +++ b/homeassistant/components/deconz/services.yaml @@ -19,6 +19,7 @@ configure: device_refresh: description: Refresh device lists from deCONZ. - bridgeid: - description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. - example: '00212EFFFF012345' \ No newline at end of file + fields: + bridgeid: + description: (Optional) Bridgeid is a string unique for each deCONZ hardware. It can be found as part of the integration name. + example: '00212EFFFF012345' diff --git a/homeassistant/components/device_tracker/services.yaml b/homeassistant/components/device_tracker/services.yaml index 7436bbd6ea4..938e9c8e324 100644 --- a/homeassistant/components/device_tracker/services.yaml +++ b/homeassistant/components/device_tracker/services.yaml @@ -25,40 +25,39 @@ see: description: Battery level of device. example: '100' -icloud: - icloud_lost_iphone: - description: Service to play the lost iphone sound on an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' - icloud_set_interval: - description: Service to set the interval of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 - icloud_update: - description: Service to ask for an update of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' - icloud_reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. - fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' +icloud_lost_iphone: + description: Service to play the lost iphone sound on an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. + example: 'iphonebart' +icloud_set_interval: + description: Service to set the interval of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. + example: 'iphonebart' + interval: + description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. + example: 1 +icloud_update: + description: Service to ask for an update of an iDevice. + fields: + account_name: + description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. + example: 'bart' + device_name: + description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. + example: 'iphonebart' +icloud_reset_account: + description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. + fields: + account_name: + description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. + example: 'bart' diff --git a/homeassistant/components/hdmi_cec/services.yaml b/homeassistant/components/hdmi_cec/services.yaml index bb0f5f932ae..f2e5f0b837a 100644 --- a/homeassistant/components/hdmi_cec/services.yaml +++ b/homeassistant/components/hdmi_cec/services.yaml @@ -19,7 +19,7 @@ send_command: are source and destination, second byte is command and optional other bytes are command parameters. If raw command specified, other params are ignored.', example: '"10:36"'} - src: {desctiption: 'Source of command. Could be decimal number or string with + src: {description: 'Source of command. Could be decimal number or string with hexadeximal notation: "0x10".', example: 12 or "0xc"} standby: {description: Standby all devices which supports it.} update: {description: Update devices state from network.} diff --git a/homeassistant/components/system_log/services.yaml b/homeassistant/components/system_log/services.yaml index c168185c9b3..2545d47c825 100644 --- a/homeassistant/components/system_log/services.yaml +++ b/homeassistant/components/system_log/services.yaml @@ -1,15 +1,15 @@ -system_log: - clear: - description: Clear all log entries. - write: - description: Write log entry. - fields: - message: - description: Message to log. [Required] - example: Something went wrong - level: - description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." - example: debug - logger: - description: Logger name under which to log the message. Defaults to 'system_log.external'. - example: mycomponent.myplatform +clear: + description: Clear all log entries. + +write: + description: Write log entry. + fields: + message: + description: Message to log. [Required] + example: Something went wrong + level: + description: "Log level: debug, info, warning, error, critical. Defaults to 'error'." + example: debug + logger: + description: Logger name under which to log the message. Defaults to 'system_log.external'. + example: mycomponent.myplatform diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 7c926a5a879..83e6ea2533b 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -30,15 +30,15 @@ heal_network: description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. - example: True + description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. + example: True heal_node: description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the node to the controller. Defaults to False. - example: True + description: Whether or not to update the return routes from the node to the controller. Defaults to False. + example: True remove_node: description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. @@ -160,7 +160,7 @@ test_node: example: 10 messages: description: Optional. Amount of test messages to send. - example: 3 + example: 3 rename_node: description: Set the name of a node. This will also affect the IDs of all entities in the node. diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 2514db6314d..b555f98d883 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -3,12 +3,13 @@ import pathlib import sys from .model import Integration, Config -from . import dependencies, manifest, codeowners +from . import dependencies, manifest, codeowners, services PLUGINS = [ manifest, dependencies, codeowners, + services, ] @@ -37,6 +38,7 @@ def main(): manifest.validate(integrations, config) dependencies.validate(integrations, config) codeowners.validate(integrations, config) + services.validate(integrations, config) # When we generate, all errors that are fixable will be ignored, # as generating them will be fixed. diff --git a/script/hassfest/model.py b/script/hassfest/model.py index c2a72ebd509..059231cf954 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -61,26 +61,22 @@ class Integration: """Integration domain.""" return self.path.name - @property - def manifest_path(self) -> pathlib.Path: - """Integration manifest path.""" - return self.path / 'manifest.json' - def add_error(self, *args, **kwargs): """Add an error.""" self.errors.append(Error(*args, **kwargs)) def load_manifest(self) -> None: """Load manifest.""" - if not self.manifest_path.is_file(): + manifest_path = self.path / 'manifest.json' + if not manifest_path.is_file(): self.add_error( 'model', - "Manifest file {} not found".format(self.manifest_path) + "Manifest file {} not found".format(manifest_path) ) return try: - manifest = json.loads(self.manifest_path.read_text()) + manifest = json.loads(manifest_path.read_text()) except ValueError as err: self.add_error( 'model', diff --git a/script/hassfest/services.py b/script/hassfest/services.py new file mode 100644 index 00000000000..9765eff1d36 --- /dev/null +++ b/script/hassfest/services.py @@ -0,0 +1,104 @@ +"""Validate dependencies.""" +import pathlib +from typing import Dict + +import re +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv +from homeassistant.util.yaml import load_yaml + +from .model import Integration + + +def exists(value): + """Check if value exists.""" + if value is None: + raise vol.Invalid("Value cannot be None") + return value + + +FIELD_SCHEMA = vol.Schema({ + vol.Required('description'): str, + vol.Optional('example'): exists, + vol.Optional('default'): exists, + vol.Optional('values'): exists, + vol.Optional('required'): bool, +}) + +SERVICE_SCHEMA = vol.Schema({ + vol.Required('description'): str, + vol.Optional('fields'): vol.Schema({ + str: FIELD_SCHEMA + }) +}) + +SERVICES_SCHEMA = vol.Schema({ + cv.slug: SERVICE_SCHEMA +}) + + +def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) \ + -> bool: + """Recursively go through a dir and it's children and find the regex.""" + pattern = re.compile(search_pattern) + + for fil in path.glob(glob_pattern): + if not fil.is_file(): + continue + + if pattern.search(fil.read_text()): + return True + + return False + + +def validate_services(integration: Integration): + """Validate services.""" + # Find if integration uses services + has_services = grep_dir(integration.path, "**/*.py", + r"hass\.(services|async_register)") + + if not has_services: + return + + try: + data = load_yaml(str(integration.path / 'services.yaml')) + except FileNotFoundError: + print( + "Warning: {} registeres services but has no services.yaml".format( + integration.domain)) + # integration.add_error( + # 'services', 'Registers services but has no services.yaml') + return + except HomeAssistantError: + integration.add_error( + 'services', 'Registers services but unable to load services.yaml') + return + + try: + SERVICES_SCHEMA(data) + except vol.Invalid as err: + integration.add_error( + 'services', + "Invalid services.yaml: {}".format(humanize_error(data, err))) + + +def validate(integrations: Dict[str, Integration], config): + """Handle dependencies for integrations.""" + # check services.yaml is cool + for integration in integrations.values(): + if not integration.manifest: + continue + + validate_services(integration) + + # check that all referenced dependencies exist + for dep in integration.manifest['dependencies']: + if dep not in integrations: + integration.add_error( + 'dependencies', + "Dependency {} does not exist" + ) From 66b2ed930cc6208fcd93af6919dce7bf7d100207 Mon Sep 17 00:00:00 2001 From: Robbie Trencheny Date: Thu, 18 Apr 2019 13:46:49 -0700 Subject: [PATCH 027/346] Set encoding before connecting (#23204) --- homeassistant/components/mikrotik/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 0c3b6b313f1..3709bc476f5 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -55,13 +55,13 @@ class MikrotikScanner(DeviceScanner): self.username = config[CONF_USERNAME] self.password = config[CONF_PASSWORD] self.method = config.get(CONF_METHOD) + self.encoding = config[CONF_ENCODING] self.connected = False self.success_init = False self.client = None self.wireless_exist = None self.success_init = self.connect_to_device() - self.encoding = config[CONF_ENCODING] if self.success_init: _LOGGER.info("Start polling Mikrotik (%s) router...", self.host) From e1d1f21a74e682f380b72b8c39721c89bc95bed1 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 18 Apr 2019 22:47:17 +0200 Subject: [PATCH 028/346] Don't create connections between sensors. Fixes #22787 (#23202) --- homeassistant/components/upnp/sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 411d529b33f..0527904a083 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -3,7 +3,6 @@ from datetime import datetime import logging from homeassistant.core import callback -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -108,9 +107,6 @@ class UpnpSensor(Entity): 'identifiers': { (DOMAIN_UPNP, self.unique_id) }, - 'connections': { - (dr.CONNECTION_UPNP, self._device.udn) - }, 'name': self.name, 'manufacturer': self._device.manufacturer, } From 620c6a22ac2fd16cef4e7391d9d13dd3cf6ba138 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 18 Apr 2019 16:48:05 -0400 Subject: [PATCH 029/346] Update vizio component to support latest pyvizio with soundbar support (#22294) * update vizio component to support latest pyvizio with soundbar support * Resolved Hound issues * Additional Hound issue * Updated based on feedback * Style updates * Additional code styling changes * Added check for auth token not being set for tv device_class * Limited lines to 80 characters * moved MAX_VOLUME into base package * fixed supported commands * styling changes * fix styling yet again * remove unnecessary elif * removed play/pause since I can't get current state * changed value access method from config dict * fixed flake failures * try to fix docstring * try to fix docstring * fixed auth token validation * rebase and regenerate requirements_all.txt * updated log text * line length fix * added config validation to handle conditionally optional parameter * updated validate setup log message and string formatting based on review * fix pylint error * less ugly --- CODEOWNERS | 1 + homeassistant/components/vizio/manifest.json | 4 +- .../components/vizio/media_player.py | 155 ++++++++++++------ requirements_all.txt | 2 +- 4 files changed, 108 insertions(+), 54 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e5d009cc7a5..a6dd61e4ffb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -237,6 +237,7 @@ homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/utility_meter/* @dgomes homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff +homeassistant/components/vizio/* @raman325 homeassistant/components/waqi/* @andrey-git homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index ac589de841a..c65204d78e8 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -3,8 +3,8 @@ "name": "Vizio", "documentation": "https://www.home-assistant.io/components/vizio", "requirements": [ - "pyvizio==0.0.4" + "pyvizio==0.0.7" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@raman325"] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 7b47a388325..68374ed59b9 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,18 +1,30 @@ -"""Vizio SmartCast TV support.""" +"""Vizio SmartCast Device support.""" from datetime import timedelta import logging - import voluptuous as vol - from homeassistant import util from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) + MediaPlayerDevice, + PLATFORM_SCHEMA +) from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, - SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + SUPPORT_NEXT_TRACK, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP +) from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON +) from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -22,6 +34,7 @@ CONF_VOLUME_STEP = 'volume_step' DEFAULT_NAME = 'Vizio SmartCast' DEFAULT_VOLUME_STEP = 1 +DEFAULT_DEVICE_CLASS = 'tv' DEVICE_ID = 'pyvizio' DEVICE_NAME = 'Python Vizio' @@ -30,36 +43,71 @@ ICON = 'mdi:television' MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) -SUPPORTED_COMMANDS = SUPPORT_TURN_ON | SUPPORT_TURN_OFF \ - | SUPPORT_SELECT_SOURCE \ - | SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK \ - | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP \ - | SUPPORT_VOLUME_SET +COMMON_SUPPORTED_COMMANDS = ( + SUPPORT_SELECT_SOURCE | + SUPPORT_TURN_ON | + SUPPORT_TURN_OFF | + SUPPORT_VOLUME_MUTE | + SUPPORT_VOLUME_SET | + SUPPORT_VOLUME_STEP +) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SUPPRESS_WARNING, default=False): cv.boolean, - vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): - vol.All(vol.Coerce(int), vol.Range(min=1, max=10)), -}) +SUPPORTED_COMMANDS = { + 'soundbar': COMMON_SUPPORTED_COMMANDS, + 'tv': ( + COMMON_SUPPORTED_COMMANDS | + SUPPORT_NEXT_TRACK | + SUPPORT_PREVIOUS_TRACK + ) +} + + +def validate_auth(config): + """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS=tv.""" + token = config.get(CONF_ACCESS_TOKEN) + if config[CONF_DEVICE_CLASS] == 'tv' and (token is None or token == ''): + raise vol.Invalid( + "When '{}' is 'tv' then '{}' is required.".format( + CONF_DEVICE_CLASS, + CONF_ACCESS_TOKEN, + ), + path=[CONF_ACCESS_TOKEN], + ) + return config + + +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SUPPRESS_WARNING, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): + vol.All(cv.string, vol.Lower, vol.In(['tv', 'soundbar'])), + vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): + vol.All(vol.Coerce(int), vol.Range(min=1, max=10)), + }), + validate_auth, +) def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the VizioTV media player platform.""" - host = config.get(CONF_HOST) + """Set up the Vizio media player platform.""" + host = config[CONF_HOST] token = config.get(CONF_ACCESS_TOKEN) - name = config.get(CONF_NAME) - volume_step = config.get(CONF_VOLUME_STEP) - - device = VizioDevice(host, token, name, volume_step) + name = config[CONF_NAME] + volume_step = config[CONF_VOLUME_STEP] + device_type = config[CONF_DEVICE_CLASS] + device = VizioDevice(host, token, name, volume_step, device_type) if device.validate_setup() is False: - _LOGGER.error("Failed to set up Vizio TV platform, " - "please check if host and API key are correct") + fail_auth_msg = "" + if token is not None and token != '': + fail_auth_msg = " and auth token is correct" + _LOGGER.error("Failed to set up Vizio platform, please check if host " + "is valid and available%s", fail_auth_msg) return - if config.get(CONF_SUPPRESS_WARNING): + if config[CONF_SUPPRESS_WARNING]: from requests.packages import urllib3 _LOGGER.warning("InsecureRequestWarning is disabled " "because of Vizio platform configuration") @@ -68,22 +116,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class VizioDevice(MediaPlayerDevice): - """Media Player implementation which performs REST requests to TV.""" + """Media Player implementation which performs REST requests to device.""" - def __init__(self, host, token, name, volume_step): + def __init__(self, host, token, name, volume_step, device_type): """Initialize Vizio device.""" import pyvizio - self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token) + self._name = name self._state = None self._volume_level = None self._volume_step = volume_step self._current_input = None self._available_inputs = None + self._device_type = device_type + self._supported_commands = SUPPORTED_COMMANDS[device_type] + self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token, + device_type) + self._max_volume = float(self._device.get_max_volume()) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): - """Retrieve latest state of the TV.""" + """Retrieve latest state of the device.""" is_on = self._device.get_power_state() if is_on: @@ -91,7 +144,7 @@ class VizioDevice(MediaPlayerDevice): volume = self._device.get_current_volume() if volume is not None: - self._volume_level = float(volume) / 100. + self._volume_level = float(volume) / self._max_volume input_ = self._device.get_current_input() if input_ is not None: @@ -113,40 +166,40 @@ class VizioDevice(MediaPlayerDevice): @property def state(self): - """Return the state of the TV.""" + """Return the state of the device.""" return self._state @property def name(self): - """Return the name of the TV.""" + """Return the name of the device.""" return self._name @property def volume_level(self): - """Return the volume level of the TV.""" + """Return the volume level of the device.""" return self._volume_level @property def source(self): - """Return current input of the TV.""" + """Return current input of the device.""" return self._current_input @property def source_list(self): - """Return list of available inputs of the TV.""" + """Return list of available inputs of the device.""" return self._available_inputs @property def supported_features(self): - """Flag TV features that are supported.""" - return SUPPORTED_COMMANDS + """Flag device features that are supported.""" + return self._supported_commands def turn_on(self): - """Turn the TV player on.""" + """Turn the device on.""" self._device.pow_on() def turn_off(self): - """Turn the TV player off.""" + """Turn the device off.""" self._device.pow_off() def mute_volume(self, mute): @@ -169,27 +222,27 @@ class VizioDevice(MediaPlayerDevice): self._device.input_switch(source) def volume_up(self): - """Increasing volume of the TV.""" - self._volume_level += self._volume_step / 100. + """Increasing volume of the device.""" + self._volume_level += self._volume_step / self._max_volume self._device.vol_up(num=self._volume_step) def volume_down(self): - """Decreasing volume of the TV.""" - self._volume_level -= self._volume_step / 100. + """Decreasing volume of the device.""" + self._volume_level -= self._volume_step / self._max_volume self._device.vol_down(num=self._volume_step) def validate_setup(self): - """Validate if host is available and key is correct.""" + """Validate if host is available and auth token is correct.""" return self._device.get_current_volume() is not None def set_volume_level(self, volume): """Set volume level.""" if self._volume_level is not None: if volume > self._volume_level: - num = int(100*(volume - self._volume_level)) + num = int(self._max_volume * (volume - self._volume_level)) self._volume_level = volume self._device.vol_up(num=num) elif volume < self._volume_level: - num = int(100*(self._volume_level - volume)) + num = int(self._max_volume * (self._volume_level - volume)) self._volume_level = volume self._device.vol_down(num=num) diff --git a/requirements_all.txt b/requirements_all.txt index 316a21da33a..2fb55c9ddd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1459,7 +1459,7 @@ pyvera==0.2.45 pyvesync_v2==0.9.6 # homeassistant.components.vizio -pyvizio==0.0.4 +pyvizio==0.0.7 # homeassistant.components.velux pyvlx==0.2.10 From a52f96b23abcdcdec12608834698af425e2ff43a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Apr 2019 15:13:35 -0700 Subject: [PATCH 030/346] Add stub services.yaml and make validation mandatory (#23213) --- homeassistant/components/alarmdecoder/services.yaml | 0 homeassistant/components/alexa/services.yaml | 0 homeassistant/components/api/services.yaml | 0 homeassistant/components/apns/services.yaml | 0 homeassistant/components/arlo/services.yaml | 0 homeassistant/components/blackbird/services.yaml | 0 homeassistant/components/bluesound/services.yaml | 0 homeassistant/components/bluetooth_tracker/services.yaml | 0 homeassistant/components/browser/services.yaml | 0 homeassistant/components/channels/services.yaml | 0 homeassistant/components/cloudflare/services.yaml | 0 homeassistant/components/config/services.yaml | 0 homeassistant/components/configurator/services.yaml | 0 homeassistant/components/demo/services.yaml | 0 .../components/device_sun_light_trigger/services.yaml | 0 homeassistant/components/dominos/services.yaml | 0 homeassistant/components/downloader/services.yaml | 0 homeassistant/components/duckdns/services.yaml | 0 homeassistant/components/ecobee/services.yaml | 0 homeassistant/components/econet/services.yaml | 0 homeassistant/components/emulated_hue/services.yaml | 0 homeassistant/components/epson/services.yaml | 0 homeassistant/components/facebox/services.yaml | 0 homeassistant/components/flux/services.yaml | 0 homeassistant/components/generic_thermostat/services.yaml | 0 homeassistant/components/harmony/services.yaml | 0 homeassistant/components/html5/services.yaml | 0 homeassistant/components/hue/services.yaml | 0 homeassistant/components/icloud/services.yaml | 0 homeassistant/components/ifttt/services.yaml | 0 homeassistant/components/joaoapps_join/services.yaml | 0 homeassistant/components/keyboard/services.yaml | 0 homeassistant/components/kodi/services.yaml | 0 homeassistant/components/lifx/services.yaml | 0 homeassistant/components/local_file/services.yaml | 0 homeassistant/components/logbook/services.yaml | 0 homeassistant/components/matrix/services.yaml | 0 homeassistant/components/media_extractor/services.yaml | 0 homeassistant/components/mill/services.yaml | 0 homeassistant/components/mobile_app/services.yaml | 0 homeassistant/components/monoprice/services.yaml | 0 homeassistant/components/mysensors/services.yaml | 0 homeassistant/components/neato/services.yaml | 0 homeassistant/components/ness_alarm/services.yaml | 0 homeassistant/components/nuheat/services.yaml | 0 homeassistant/components/nuimo_controller/services.yaml | 0 homeassistant/components/nuki/services.yaml | 0 homeassistant/components/onkyo/services.yaml | 0 homeassistant/components/onvif/services.yaml | 0 homeassistant/components/pilight/services.yaml | 0 homeassistant/components/rest_command/services.yaml | 0 homeassistant/components/roku/services.yaml | 0 homeassistant/components/route53/services.yaml | 0 homeassistant/components/sabnzbd/services.yaml | 0 homeassistant/components/sensibo/services.yaml | 0 homeassistant/components/snapcast/services.yaml | 0 homeassistant/components/songpal/services.yaml | 0 homeassistant/components/sonos/services.yaml | 0 homeassistant/components/soundtouch/services.yaml | 0 homeassistant/components/squeezebox/services.yaml | 0 homeassistant/components/stream/services.yaml | 0 homeassistant/components/telegram/services.yaml | 0 homeassistant/components/todoist/services.yaml | 0 homeassistant/components/universal/services.yaml | 0 homeassistant/components/websocket_api/services.yaml | 0 homeassistant/components/wemo/services.yaml | 0 homeassistant/components/xiaomi_miio/services.yaml | 0 homeassistant/components/yamaha/services.yaml | 0 script/hassfest/services.py | 7 ++----- 69 files changed, 2 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/alarmdecoder/services.yaml create mode 100644 homeassistant/components/alexa/services.yaml create mode 100644 homeassistant/components/api/services.yaml create mode 100644 homeassistant/components/apns/services.yaml create mode 100644 homeassistant/components/arlo/services.yaml create mode 100644 homeassistant/components/blackbird/services.yaml create mode 100644 homeassistant/components/bluesound/services.yaml create mode 100644 homeassistant/components/bluetooth_tracker/services.yaml create mode 100644 homeassistant/components/browser/services.yaml create mode 100644 homeassistant/components/channels/services.yaml create mode 100644 homeassistant/components/cloudflare/services.yaml create mode 100644 homeassistant/components/config/services.yaml create mode 100644 homeassistant/components/configurator/services.yaml create mode 100644 homeassistant/components/demo/services.yaml create mode 100644 homeassistant/components/device_sun_light_trigger/services.yaml create mode 100644 homeassistant/components/dominos/services.yaml create mode 100644 homeassistant/components/downloader/services.yaml create mode 100644 homeassistant/components/duckdns/services.yaml create mode 100644 homeassistant/components/ecobee/services.yaml create mode 100644 homeassistant/components/econet/services.yaml create mode 100644 homeassistant/components/emulated_hue/services.yaml create mode 100644 homeassistant/components/epson/services.yaml create mode 100644 homeassistant/components/facebox/services.yaml create mode 100644 homeassistant/components/flux/services.yaml create mode 100644 homeassistant/components/generic_thermostat/services.yaml create mode 100644 homeassistant/components/harmony/services.yaml create mode 100644 homeassistant/components/html5/services.yaml create mode 100644 homeassistant/components/hue/services.yaml create mode 100644 homeassistant/components/icloud/services.yaml create mode 100644 homeassistant/components/ifttt/services.yaml create mode 100644 homeassistant/components/joaoapps_join/services.yaml create mode 100644 homeassistant/components/keyboard/services.yaml create mode 100644 homeassistant/components/kodi/services.yaml create mode 100644 homeassistant/components/lifx/services.yaml create mode 100644 homeassistant/components/local_file/services.yaml create mode 100644 homeassistant/components/logbook/services.yaml create mode 100644 homeassistant/components/matrix/services.yaml create mode 100644 homeassistant/components/media_extractor/services.yaml create mode 100644 homeassistant/components/mill/services.yaml create mode 100644 homeassistant/components/mobile_app/services.yaml create mode 100644 homeassistant/components/monoprice/services.yaml create mode 100644 homeassistant/components/mysensors/services.yaml create mode 100644 homeassistant/components/neato/services.yaml create mode 100644 homeassistant/components/ness_alarm/services.yaml create mode 100644 homeassistant/components/nuheat/services.yaml create mode 100644 homeassistant/components/nuimo_controller/services.yaml create mode 100644 homeassistant/components/nuki/services.yaml create mode 100644 homeassistant/components/onkyo/services.yaml create mode 100644 homeassistant/components/onvif/services.yaml create mode 100644 homeassistant/components/pilight/services.yaml create mode 100644 homeassistant/components/rest_command/services.yaml create mode 100644 homeassistant/components/roku/services.yaml create mode 100644 homeassistant/components/route53/services.yaml create mode 100644 homeassistant/components/sabnzbd/services.yaml create mode 100644 homeassistant/components/sensibo/services.yaml create mode 100644 homeassistant/components/snapcast/services.yaml create mode 100644 homeassistant/components/songpal/services.yaml create mode 100644 homeassistant/components/sonos/services.yaml create mode 100644 homeassistant/components/soundtouch/services.yaml create mode 100644 homeassistant/components/squeezebox/services.yaml create mode 100644 homeassistant/components/stream/services.yaml create mode 100644 homeassistant/components/telegram/services.yaml create mode 100644 homeassistant/components/todoist/services.yaml create mode 100644 homeassistant/components/universal/services.yaml create mode 100644 homeassistant/components/websocket_api/services.yaml create mode 100644 homeassistant/components/wemo/services.yaml create mode 100644 homeassistant/components/xiaomi_miio/services.yaml create mode 100644 homeassistant/components/yamaha/services.yaml diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/alexa/services.yaml b/homeassistant/components/alexa/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/api/services.yaml b/homeassistant/components/api/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/apns/services.yaml b/homeassistant/components/apns/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/arlo/services.yaml b/homeassistant/components/arlo/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/config/services.yaml b/homeassistant/components/config/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/configurator/services.yaml b/homeassistant/components/configurator/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/device_sun_light_trigger/services.yaml b/homeassistant/components/device_sun_light_trigger/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/duckdns/services.yaml b/homeassistant/components/duckdns/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/econet/services.yaml b/homeassistant/components/econet/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/emulated_hue/services.yaml b/homeassistant/components/emulated_hue/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/flux/services.yaml b/homeassistant/components/flux/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/generic_thermostat/services.yaml b/homeassistant/components/generic_thermostat/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/joaoapps_join/services.yaml b/homeassistant/components/joaoapps_join/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/kodi/services.yaml b/homeassistant/components/kodi/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/mobile_app/services.yaml b/homeassistant/components/mobile_app/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/nuheat/services.yaml b/homeassistant/components/nuheat/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/onkyo/services.yaml b/homeassistant/components/onkyo/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/rest_command/services.yaml b/homeassistant/components/rest_command/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/snapcast/services.yaml b/homeassistant/components/snapcast/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/telegram/services.yaml b/homeassistant/components/telegram/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/websocket_api/services.yaml b/homeassistant/components/websocket_api/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 9765eff1d36..bb04d2fc13f 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -67,11 +67,8 @@ def validate_services(integration: Integration): try: data = load_yaml(str(integration.path / 'services.yaml')) except FileNotFoundError: - print( - "Warning: {} registeres services but has no services.yaml".format( - integration.domain)) - # integration.add_error( - # 'services', 'Registers services but has no services.yaml') + integration.add_error( + 'services', 'Registers services but has no services.yaml') return except HomeAssistantError: integration.add_error( From 5e363d124ec945857830626d761c8ea625a347fd Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 18 Apr 2019 20:21:30 -0400 Subject: [PATCH 031/346] fix bindable devices (#23216) --- homeassistant/components/zha/api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index aacb0a711a5..0604c2fada4 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -351,10 +351,11 @@ async def websocket_get_bindable_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_IEEE] source_device = zha_gateway.get_device(source_ieee) + ha_device_registry = await async_get_registry(hass) devices = [ - { - **device.device_info - } for device in zha_gateway.devices.values() if + async_get_device_info( + hass, device, ha_device_registry=ha_device_registry + ) for device in zha_gateway.devices.values() if async_is_bindable_target(source_device, device) ] From c2b4e243728e72e0f49412d9bd13f35e99036e96 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 18 Apr 2019 20:23:48 -0400 Subject: [PATCH 032/346] update zha-quirks (#23215) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e2b2c54fd93..9fd0629fcb2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.7.2", - "zha-quirks==0.0.7", + "zha-quirks==0.0.8", "zigpy-deconz==0.1.3", "zigpy-homeassistant==0.3.1", "zigpy-xbee-homeassistant==0.1.3" diff --git a/requirements_all.txt b/requirements_all.txt index 2fb55c9ddd5..57f8d813af1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1836,7 +1836,7 @@ zengge==0.2 zeroconf==0.21.3 # homeassistant.components.zha -zha-quirks==0.0.7 +zha-quirks==0.0.8 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From 1761b25879a179d3a297c5153c6dd4a64dc8644b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Apr 2019 20:31:53 -0700 Subject: [PATCH 033/346] Remove copy paste error --- script/hassfest/services.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index bb04d2fc13f..4be366b3d55 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -91,11 +91,3 @@ def validate(integrations: Dict[str, Integration], config): continue validate_services(integration) - - # check that all referenced dependencies exist - for dep in integration.manifest['dependencies']: - if dep not in integrations: - integration.add_error( - 'dependencies', - "Dependency {} does not exist" - ) From 70ba5eb0ef9b9f879138104d3e522515d0e67321 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 05:55:10 +0200 Subject: [PATCH 034/346] Add json_attributes_template (#22981) --- homeassistant/components/mqtt/__init__.py | 14 ++++++++++++-- tests/components/mqtt/test_sensor.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e226e966b09..3de53145cfc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -69,6 +69,7 @@ CONF_AVAILABILITY_TOPIC = 'availability_topic' CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' CONF_JSON_ATTRS_TOPIC = 'json_attributes_topic' +CONF_JSON_ATTRS_TEMPLATE = 'json_attributes_template' CONF_QOS = 'qos' CONF_RETAIN = 'retain' @@ -242,6 +243,7 @@ MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All(vol.Schema({ MQTT_JSON_ATTRS_SCHEMA = vol.Schema({ vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, }) MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) @@ -908,10 +910,18 @@ class MqttAttributes(Entity): """(Re)Subscribe to topics.""" from .subscription import async_subscribe_topics + attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) + if attr_tpl is not None: + attr_tpl.hass = self.hass + @callback def attributes_message_received(msg: Message) -> None: try: - json_dict = json.loads(msg.payload) + payload = msg.payload + if attr_tpl is not None: + payload = attr_tpl.async_render_with_possible_json_value( + payload) + json_dict = json.loads(payload) if isinstance(json_dict, dict): self._attributes = json_dict self.async_write_ha_state() @@ -919,7 +929,7 @@ class MqttAttributes(Entity): _LOGGER.warning("JSON result was not a dictionary") self._attributes = None except ValueError: - _LOGGER.warning("Erroneous JSON: %s", msg.payload) + _LOGGER.warning("Erroneous JSON: %s", payload) self._attributes = None self._attributes_sub_state = await async_subscribe_topics( diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 027135e8a7a..45267484211 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -384,6 +384,27 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): assert '100' == state.attributes.get('val') +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic', + 'json_attributes_template': "{{ value_json['Timer1'] | tojson }}" + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', json.dumps( + {"Timer1": {"Arm": 0, "Time": "22:18"}})) + await hass.async_block_till_done() + state = hass.states.get('sensor.test') + + assert 0 == state.attributes.get('Arm') + assert '22:18' == state.attributes.get('Time') + + async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" assert await async_setup_component(hass, sensor.DOMAIN, { From b0ce3dc683559bbe66411729c6469fc4e508a4eb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Apr 2019 20:58:35 -0700 Subject: [PATCH 035/346] Only comment with changed coverage on release PRs [skip-ci] (#23224) --- .codecov.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.codecov.yml b/.codecov.yml index 9ad9083506d..be739b61809 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -13,3 +13,4 @@ coverage: url: "secret:TgWDUM4Jw0w7wMJxuxNF/yhSOHglIo1fGwInJnRLEVPy2P2aLimkoK1mtKCowH5TFw+baUXVXT3eAqefbdvIuM8BjRR4aRji95C6CYyD0QHy4N8i7nn1SQkWDPpS8IthYTg07rUDF7s5guurkKv2RrgoCdnnqjAMSzHoExMOF7xUmblMdhBTWJgBpWEhASJy85w/xxjlsE1xoTkzeJu9Q67pTXtRcn+5kb5/vIzPSYg=" comment: require_changes: yes + branches: master From 7a84cfb0be49f4905a977c0bca9bc8e72eae2836 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 05:59:41 +0200 Subject: [PATCH 036/346] Fix optimistic mode + other bugs, tests (#22976) --- homeassistant/components/mqtt/fan.py | 61 +++-- tests/components/mqtt/test_fan.py | 347 ++++++++++++++++++++++++++- 2 files changed, 383 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 99aa68d1975..8b116210a10 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -9,7 +9,7 @@ from homeassistant.components.fan import ( SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, - CONF_STATE, STATE_OFF, STATE_ON) + CONF_STATE) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -32,6 +32,7 @@ CONF_OSCILLATION_COMMAND_TOPIC = 'oscillation_command_topic' CONF_OSCILLATION_VALUE_TEMPLATE = 'oscillation_value_template' CONF_PAYLOAD_OSCILLATION_ON = 'payload_oscillation_on' CONF_PAYLOAD_OSCILLATION_OFF = 'payload_oscillation_off' +CONF_PAYLOAD_OFF_SPEED = 'payload_off_speed' CONF_PAYLOAD_LOW_SPEED = 'payload_low_speed' CONF_PAYLOAD_MEDIUM_SPEED = 'payload_medium_speed' CONF_PAYLOAD_HIGH_SPEED = 'payload_high_speed' @@ -57,12 +58,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PAYLOAD_HIGH_SPEED, default=SPEED_HIGH): cv.string, vol.Optional(CONF_PAYLOAD_LOW_SPEED, default=SPEED_LOW): cv.string, vol.Optional(CONF_PAYLOAD_MEDIUM_SPEED, default=SPEED_MEDIUM): cv.string, + vol.Optional(CONF_PAYLOAD_OFF_SPEED, default=SPEED_OFF): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OSCILLATION_OFF, - default=DEFAULT_PAYLOAD_OFF): cv.string, + default=OSCILLATE_OFF_PAYLOAD): cv.string, vol.Optional(CONF_PAYLOAD_OSCILLATION_ON, - default=DEFAULT_PAYLOAD_ON): cv.string, + default=OSCILLATE_ON_PAYLOAD): cv.string, vol.Optional(CONF_SPEED_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_SPEED_LIST, default=[SPEED_OFF, SPEED_LOW, @@ -172,13 +174,14 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, OSCILLATION: config.get(CONF_OSCILLATION_VALUE_TEMPLATE) } self._payload = { - STATE_ON: config[CONF_PAYLOAD_ON], - STATE_OFF: config[CONF_PAYLOAD_OFF], - OSCILLATE_ON_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_ON], - OSCILLATE_OFF_PAYLOAD: config[CONF_PAYLOAD_OSCILLATION_OFF], - SPEED_LOW: config[CONF_PAYLOAD_LOW_SPEED], - SPEED_MEDIUM: config[CONF_PAYLOAD_MEDIUM_SPEED], - SPEED_HIGH: config[CONF_PAYLOAD_HIGH_SPEED], + 'STATE_ON': config[CONF_PAYLOAD_ON], + 'STATE_OFF': config[CONF_PAYLOAD_OFF], + 'OSCILLATE_ON_PAYLOAD': config[CONF_PAYLOAD_OSCILLATION_ON], + 'OSCILLATE_OFF_PAYLOAD': config[CONF_PAYLOAD_OSCILLATION_OFF], + 'SPEED_LOW': config[CONF_PAYLOAD_LOW_SPEED], + 'SPEED_MEDIUM': config[CONF_PAYLOAD_MEDIUM_SPEED], + 'SPEED_HIGH': config[CONF_PAYLOAD_HIGH_SPEED], + 'SPEED_OFF': config[CONF_PAYLOAD_OFF_SPEED], } optimistic = config[CONF_OPTIMISTIC] self._optimistic = optimistic or self._topic[CONF_STATE_TOPIC] is None @@ -208,9 +211,9 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def state_received(msg): """Handle new received MQTT message.""" payload = templates[CONF_STATE](msg.payload) - if payload == self._payload[STATE_ON]: + if payload == self._payload['STATE_ON']: self._state = True - elif payload == self._payload[STATE_OFF]: + elif payload == self._payload['STATE_OFF']: self._state = False self.async_write_ha_state() @@ -224,12 +227,14 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def speed_received(msg): """Handle new received MQTT message for the speed.""" payload = templates[ATTR_SPEED](msg.payload) - if payload == self._payload[SPEED_LOW]: + if payload == self._payload['SPEED_LOW']: self._speed = SPEED_LOW - elif payload == self._payload[SPEED_MEDIUM]: + elif payload == self._payload['SPEED_MEDIUM']: self._speed = SPEED_MEDIUM - elif payload == self._payload[SPEED_HIGH]: + elif payload == self._payload['SPEED_HIGH']: self._speed = SPEED_HIGH + elif payload == self._payload['SPEED_OFF']: + self._speed = SPEED_OFF self.async_write_ha_state() if self._topic[CONF_SPEED_STATE_TOPIC] is not None: @@ -243,9 +248,9 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def oscillation_received(msg): """Handle new received MQTT message for the oscillation.""" payload = templates[OSCILLATION](msg.payload) - if payload == self._payload[OSCILLATE_ON_PAYLOAD]: + if payload == self._payload['OSCILLATE_ON_PAYLOAD']: self._oscillation = True - elif payload == self._payload[OSCILLATE_OFF_PAYLOAD]: + elif payload == self._payload['OSCILLATE_OFF_PAYLOAD']: self._oscillation = False self.async_write_ha_state() @@ -314,10 +319,13 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """ mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], - self._payload[STATE_ON], self._config[CONF_QOS], + self._payload['STATE_ON'], self._config[CONF_QOS], self._config[CONF_RETAIN]) if speed: await self.async_set_speed(speed) + if self._optimistic: + self._state = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn off the entity. @@ -326,8 +334,11 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """ mqtt.async_publish( self.hass, self._topic[CONF_COMMAND_TOPIC], - self._payload[STATE_OFF], self._config[CONF_QOS], + self._payload['STATE_OFF'], self._config[CONF_QOS], self._config[CONF_RETAIN]) + if self._optimistic: + self._state = False + self.async_write_ha_state() async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan. @@ -338,11 +349,13 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, return if speed == SPEED_LOW: - mqtt_payload = self._payload[SPEED_LOW] + mqtt_payload = self._payload['SPEED_LOW'] elif speed == SPEED_MEDIUM: - mqtt_payload = self._payload[SPEED_MEDIUM] + mqtt_payload = self._payload['SPEED_MEDIUM'] elif speed == SPEED_HIGH: - mqtt_payload = self._payload[SPEED_HIGH] + mqtt_payload = self._payload['SPEED_HIGH'] + elif speed == SPEED_OFF: + mqtt_payload = self._payload['SPEED_OFF'] else: mqtt_payload = speed @@ -364,9 +377,9 @@ class MqttFan(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, return if oscillating is False: - payload = self._payload[OSCILLATE_OFF_PAYLOAD] + payload = self._payload['OSCILLATE_OFF_PAYLOAD'] else: - payload = self._payload[OSCILLATE_ON_PAYLOAD] + payload = self._payload['OSCILLATE_ON_PAYLOAD'] mqtt.async_publish( self.hass, self._topic[CONF_OSCILLATION_COMMAND_TOPIC], diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index b7f8b8338a0..c00de8522b9 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -4,12 +4,14 @@ from unittest.mock import ANY from homeassistant.components import fan, mqtt from homeassistant.components.mqtt.discovery import async_start -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, mock_registry) +from tests.components.fan import common async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): @@ -23,6 +25,349 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get('fan.test') is None +async def test_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'payload_off': 'StAtE_OfF', + 'payload_on': 'StAtE_On', + 'oscillation_state_topic': 'oscillation-state-topic', + 'oscillation_command_topic': 'oscillation-command-topic', + 'payload_oscillation_off': 'OsC_OfF', + 'payload_oscillation_on': 'OsC_On', + 'speed_state_topic': 'speed-state-topic', + 'speed_command_topic': 'speed-command-topic', + 'payload_off_speed': 'speed_OfF', + 'payload_low_speed': 'speed_lOw', + 'payload_medium_speed': 'speed_mEdium', + 'payload_high_speed': 'speed_High', + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, 'state-topic', 'StAtE_On') + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + + async_fire_mqtt_message(hass, 'state-topic', 'StAtE_OfF') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get('oscillating') is False + + async_fire_mqtt_message(hass, 'oscillation-state-topic', 'OsC_On') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is True + + async_fire_mqtt_message(hass, 'oscillation-state-topic', 'OsC_OfF') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is False + + assert fan.SPEED_OFF == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_lOw') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_LOW == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_mEdium') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_MEDIUM == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_High') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_HIGH == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_OfF') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_OFF == state.attributes.get('speed') + + +async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): + """Test the controlling state via topic and JSON message.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'oscillation_state_topic': 'oscillation-state-topic', + 'oscillation_command_topic': 'oscillation-command-topic', + 'speed_state_topic': 'speed-state-topic', + 'speed_command_topic': 'speed-command-topic', + 'state_value_template': '{{ value_json.val }}', + 'oscillation_value_template': '{{ value_json.val }}', + 'speed_value_template': '{{ value_json.val }}', + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, 'state-topic', '{"val":"ON"}') + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + + async_fire_mqtt_message(hass, 'state-topic', '{"val":"OFF"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get('oscillating') is False + + async_fire_mqtt_message( + hass, 'oscillation-state-topic', '{"val":"oscillate_on"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is True + + async_fire_mqtt_message( + hass, 'oscillation-state-topic', '{"val":"oscillate_off"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert state.attributes.get('oscillating') is False + + assert fan.SPEED_OFF == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"low"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_LOW == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"medium"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_MEDIUM == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"high"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_HIGH == state.attributes.get('speed') + + async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"off"}') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('fan.test') + assert fan.SPEED_OFF == state.attributes.get('speed') + + +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test optimistic mode without state topic.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'command-topic', + 'payload_off': 'StAtE_OfF', + 'payload_on': 'StAtE_On', + 'oscillation_command_topic': 'oscillation-command-topic', + 'payload_oscillation_off': 'OsC_OfF', + 'payload_oscillation_on': 'OsC_On', + 'speed_command_topic': 'speed-command-topic', + 'payload_off_speed': 'speed_OfF', + 'payload_low_speed': 'speed_lOw', + 'payload_medium_speed': 'speed_mEdium', + 'payload_high_speed': 'speed_High', + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_on(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'StAtE_On', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_off(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'StAtE_OfF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', True) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'OsC_On', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', False) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'OsC_OfF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_lOw', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_mEdium', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_High', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'speed_OfF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + +async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): + """Test optimistic mode with state topic.""" + assert await async_setup_component(hass, fan.DOMAIN, { + fan.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'oscillation_state_topic': 'oscillation-state-topic', + 'oscillation_command_topic': 'oscillation-command-topic', + 'speed_state_topic': 'speed-state-topic', + 'speed_command_topic': 'speed-command-topic', + 'optimistic': True + } + }) + + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_on(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'ON', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_ON + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_turn_off(hass, 'fan.test') + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'OFF', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', True) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'oscillate_on', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_oscillate(hass, 'fan.test', False) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'oscillation-command-topic', 'oscillate_off', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'low', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'medium', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'high', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'speed-command-topic', 'off', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('fan.test') + assert state.state is STATE_OFF + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_default_availability_payload(hass, mqtt_mock): """Test the availability payload.""" assert await async_setup_component(hass, fan.DOMAIN, { From eac2388d4984f256eefe0dfd3b52b835fc4d6880 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 19 Apr 2019 04:00:35 +0000 Subject: [PATCH 037/346] Set default value for input_datetime (#21919) * Set default value for input_datetime If no initial value is set and no value is available to be restored, set the default value as specified in the docs to 1970-01-01 00:00. * Use regular if statement Ternary statements can be tricky if you try to keep the value the same if not something * Add test for default values Check that if no initial value is set, state returns 1970-01-01 at 00:00 * Fix tests - was passing wrong args to time/date * Verify we get a timestamp attribute for input_datetime This adds a check that when using the default timestamp of 1970-1-1 00:00:00, we get a timestamp attribute. This is waht prompted this PR in the first place, as when specifying an automation trying to access the timestamp attribute for a non- initialized input_datetime HASS wouldn't start. * Simplify the change for a default value Based on @balloob comment. Simplifying the code * Revert "Simplify the change for a default value" This reverts commit c2d67f19a686b141672d619be62e3f53890f1328. --- .../components/input_datetime/__init__.py | 21 ++++++++---- tests/components/input_datetime/test_init.py | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 34faffd2028..af0a28aa34a 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -20,6 +20,8 @@ CONF_HAS_DATE = 'has_date' CONF_HAS_TIME = 'has_time' CONF_INITIAL = 'initial' +DEFAULT_VALUE = '1970-01-01 00:00:00' + ATTR_DATE = 'date' ATTR_TIME = 'time' @@ -120,13 +122,18 @@ class InputDatetime(RestoreEntity): if old_state is not None: restore_val = old_state.state - if restore_val is not None: - if not self.has_date: - self._current_datetime = dt_util.parse_time(restore_val) - elif not self.has_time: - self._current_datetime = dt_util.parse_date(restore_val) - else: - self._current_datetime = dt_util.parse_datetime(restore_val) + if not self.has_date: + if not restore_val: + restore_val = DEFAULT_VALUE.split()[1] + self._current_datetime = dt_util.parse_time(restore_val) + elif not self.has_time: + if not restore_val: + restore_val = DEFAULT_VALUE.split()[0] + self._current_datetime = dt_util.parse_date(restore_val) + else: + if not restore_val: + restore_val = DEFAULT_VALUE + self._current_datetime = dt_util.parse_datetime(restore_val) @property def should_poll(self): diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 2a4d0fef09d..03ad27e6048 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -199,6 +199,39 @@ def test_restore_state(hass): assert state_bogus.state == str(initial) +@asyncio.coroutine +def test_default_value(hass): + """Test default value if none has been set via inital or restore state.""" + yield from async_setup_component(hass, DOMAIN, { + DOMAIN: { + 'test_time': { + 'has_time': True, + 'has_date': False + }, + 'test_date': { + 'has_time': False, + 'has_date': True + }, + 'test_datetime': { + 'has_time': True, + 'has_date': True + }, + }}) + + dt_obj = datetime.datetime(1970, 1, 1, 0, 0) + state_time = hass.states.get('input_datetime.test_time') + assert state_time.state == str(dt_obj.time()) + assert state_time.attributes.get('timestamp') is not None + + state_date = hass.states.get('input_datetime.test_date') + assert state_date.state == str(dt_obj.date()) + assert state_date.attributes.get('timestamp') is not None + + state_datetime = hass.states.get('input_datetime.test_datetime') + assert state_datetime.state == str(dt_obj) + assert state_datetime.attributes.get('timestamp') is not None + + async def test_input_datetime_context(hass, hass_admin_user): """Test that input_datetime context works.""" assert await async_setup_component(hass, 'input_datetime', { From bea7e2a7facd698fa47d7f2906ed65e8ec97c244 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 06:01:19 +0200 Subject: [PATCH 038/346] Fix clearing error message for MQTT vacuum (#23206) * Fix clearing error message * Remove redundant hass.async_block_till_done --- homeassistant/components/mqtt/vacuum.py | 2 +- tests/components/mqtt/test_vacuum.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum.py index ae4b3322b8e..5895d52e9dc 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -313,7 +313,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, error = self._templates[CONF_ERROR_TEMPLATE]\ .async_render_with_possible_json_value( msg.payload, error_value=None) - if error: + if error is not None: self._error = cv.string(error) if self._docked: diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_vacuum.py index 4140177a929..78ca45a792f 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_vacuum.py @@ -298,10 +298,17 @@ async def test_status_error(hass, mock_publish): }""" async_fire_mqtt_message(hass, 'vacuum/state', message) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert 'Error: Error1' == state.attributes.get(ATTR_STATUS) + message = """{ + "error": "" + }""" + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert 'Stopped' == state.attributes.get(ATTR_STATUS) + async def test_battery_template(hass, mock_publish): """Test that you can use non-default templates for battery_level.""" From dbe0ba87a371a4e2acf3065d7d299855fa42f677 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Apr 2019 23:56:24 -0700 Subject: [PATCH 039/346] Async fix for bluetooth stopping (#23225) --- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f24b943f188..d256f56e7fe 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -24,7 +24,7 @@ def setup_scanner(hass, config, see, discovery_info=None): new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) - async def async_stop(event): + def handle_stop(event): """Try to shut down the bluetooth child process nicely.""" # These should never be unset at the point this runs, but just for # safety's sake, use `get`. @@ -32,7 +32,7 @@ def setup_scanner(hass, config, see, discovery_info=None): if adapter is not None: adapter.kill() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) def see_device(address, name, new_device=False): """Mark a device as seen.""" From 6a7bd19a5aa097384722cb6b485653807c905a99 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Apr 2019 01:14:14 -0600 Subject: [PATCH 040/346] Remove archived 17track packages from the entity registry (#23049) * Remove archived 17track packages from the entity registry * Fix incorrect __init__.py * Member comments * Member comments * Fix too many params * Member comments * Member comments --- .../components/seventeentrack/sensor.py | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index f9bae50698b..b8df1bbaaf1 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -33,6 +33,8 @@ DATA_SUMMARY = 'summary_data' DEFAULT_ATTRIBUTION = 'Data provided by 17track.net' DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) +ENTITY_ID_TEMPLATE = 'package_{0}_{1}' + NOTIFICATION_DELIVERED_ID_SCAFFOLD = 'package_delivered_{0}' NOTIFICATION_DELIVERED_TITLE = 'Package Delivered' NOTIFICATION_DELIVERED_URL_SCAFFOLD = 'https://t.17track.net/track#nums={0}' @@ -71,8 +73,8 @@ async def async_setup_platform( scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) data = SeventeenTrackData( - client, async_add_entities, scan_interval, config[CONF_SHOW_ARCHIVED], - config[CONF_SHOW_DELIVERED]) + hass, client, async_add_entities, scan_interval, + config[CONF_SHOW_ARCHIVED], config[CONF_SHOW_DELIVERED]) await data.async_update() sensors = [] @@ -208,7 +210,7 @@ class SeventeenTrackPackageSensor(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return 'package_{0}_{1}'.format( + return ENTITY_ID_TEMPLATE.format( self._data.account_id, self._tracking_number) async def async_update(self): @@ -227,12 +229,13 @@ class SeventeenTrackPackageSensor(Entity): # delete this entity: _LOGGER.info( 'Deleting entity for stale package: %s', self._tracking_number) + reg = await self.hass.helpers.entity_registry.async_get_registry() + self.hass.async_create_task(reg.async_remove(self.entity_id)) self.hass.async_create_task(self.async_remove()) return # If the user has elected to not see delivered packages and one gets - # delivered, post a notification, remove the entity from the UI, and - # delete it from the entity registry: + # delivered, post a notification: if package.status == VALUE_DELIVERED and not self._data.show_delivered: _LOGGER.info('Package delivered: %s', self._tracking_number) self.hass.components.persistent_notification.create( @@ -245,10 +248,6 @@ class SeventeenTrackPackageSensor(Entity): title=NOTIFICATION_DELIVERED_TITLE, notification_id=NOTIFICATION_DELIVERED_ID_SCAFFOLD.format( self._tracking_number)) - - reg = self.hass.helpers.entity_registry.async_get_registry() - self.hass.async_create_task(reg.async_remove(self.entity_id)) - self.hass.async_create_task(self.async_remove()) return self._attrs.update({ @@ -262,11 +261,12 @@ class SeventeenTrackData: """Define a data handler for 17track.net.""" def __init__( - self, client, async_add_entities, scan_interval, show_archived, - show_delivered): + self, hass, client, async_add_entities, scan_interval, + show_archived, show_delivered): """Initialize.""" self._async_add_entities = async_add_entities self._client = client + self._hass = hass self._scan_interval = scan_interval self._show_archived = show_archived self.account_id = client.profile.account_id @@ -296,6 +296,18 @@ class SeventeenTrackData: for package in to_add ], True) + # Remove archived packages from the entity registry: + to_remove = set(self.packages) - set(packages) + reg = await self._hass.helpers.entity_registry.async_get_registry() + for package in to_remove: + entity_id = reg.async_get_entity_id( + 'sensor', 'seventeentrack', + ENTITY_ID_TEMPLATE.format( + self.account_id, package.tracking_number)) + if not entity_id: + continue + self._hass.async_create_task(reg.async_remove(entity_id)) + self.packages = packages except SeventeenTrackError as err: _LOGGER.error('There was an error retrieving packages: %s', err) From 3e443d253c622ac06b14bce4f4c9d9e7188bd098 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 19 Apr 2019 09:43:47 +0200 Subject: [PATCH 041/346] Hass.io Add-on panel support for Ingress (#23185) * Hass.io Add-on panel support for Ingress * Revert part of discovery startup handling * Add type * Fix tests * Add tests * Fix lint * Fix lint on test --- homeassistant/components/hassio/__init__.py | 16 ++- .../components/hassio/addon_panel.py | 93 +++++++++++++ homeassistant/components/hassio/auth.py | 9 +- homeassistant/components/hassio/const.py | 6 + homeassistant/components/hassio/discovery.py | 23 ++-- homeassistant/components/hassio/handler.py | 8 ++ homeassistant/components/hassio/ingress.py | 2 +- homeassistant/components/hassio/manifest.json | 1 + tests/components/hassio/test_addon_panel.py | 128 ++++++++++++++++++ tests/components/hassio/test_handler.py | 20 +++ tests/components/hassio/test_init.py | 31 +++-- 11 files changed, 298 insertions(+), 39 deletions(-) create mode 100644 homeassistant/components/hassio/addon_panel.py create mode 100644 tests/components/hassio/test_addon_panel.py diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 2fdb859c320..c8c0f6c9f19 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -16,11 +16,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from .auth import async_setup_auth -from .discovery import async_setup_discovery +from .auth import async_setup_auth_view +from .addon_panel import async_setup_addon_panel +from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError from .http import HassIOView -from .ingress import async_setup_ingress +from .ingress import async_setup_ingress_view _LOGGER = logging.getLogger(__name__) @@ -265,12 +266,15 @@ async def async_setup(hass, config): HASS_DOMAIN, service, async_handle_core_service) # Init discovery Hass.io feature - async_setup_discovery(hass, hassio, config) + async_setup_discovery_view(hass, hassio) # Init auth Hass.io feature - async_setup_auth(hass) + async_setup_auth_view(hass) # Init ingress Hass.io feature - async_setup_ingress(hass, host) + async_setup_ingress_view(hass, host) + + # Init add-on ingress panels + await async_setup_addon_panel(hass, hassio) return True diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py new file mode 100644 index 00000000000..d19ca23799a --- /dev/null +++ b/homeassistant/components/hassio/addon_panel.py @@ -0,0 +1,93 @@ +"""Implement the Ingress Panel feature for Hass.io Add-ons.""" +import asyncio +import logging + +from aiohttp import web + +from homeassistant.components.http import HomeAssistantView +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ATTR_PANELS, ATTR_TITLE, ATTR_ICON, ATTR_ADMIN, ATTR_ENABLE +from .handler import HassioAPIError + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_addon_panel(hass: HomeAssistantType, hassio): + """Add-on Ingress Panel setup.""" + hassio_addon_panel = HassIOAddonPanel(hass, hassio) + hass.http.register_view(hassio_addon_panel) + + # If panels are exists + panels = await hassio_addon_panel.get_panels() + if not panels: + return + + # Register available panels + jobs = [] + for addon, data in panels.items(): + if not data[ATTR_ENABLE]: + continue + jobs.append(_register_panel(hass, addon, data)) + + if jobs: + await asyncio.wait(jobs) + + +class HassIOAddonPanel(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio_push:panel" + url = "/api/hassio_push/panel/{addon}" + + def __init__(self, hass, hassio): + """Initialize WebView.""" + self.hass = hass + self.hassio = hassio + + async def post(self, request, addon): + """Handle new add-on panel requests.""" + panels = await self.get_panels() + + # Panel exists for add-on slug + if addon not in panels or not panels[addon][ATTR_ENABLE]: + _LOGGER.error("Panel is not enable for %s", addon) + return web.Response(status=400) + data = panels[addon] + + # Register panel + await _register_panel(self.hass, addon, data) + return web.Response() + + async def delete(self, request, addon): + """Handle remove add-on panel requests.""" + # Currently not supported by backend / frontend + return web.Response() + + async def get_panels(self): + """Return panels add-on info data.""" + try: + data = await self.hassio.get_ingress_panels() + return data[ATTR_PANELS] + except HassioAPIError as err: + _LOGGER.error("Can't read panel info: %s", err) + return {} + + +def _register_panel(hass, addon, data): + """Init coroutine to register the panel. + + Return coroutine. + """ + return hass.components.frontend.async_register_built_in_panel( + frontend_url_path=addon, + webcomponent_name='hassio-main', + sidebar_title=data[ATTR_TITLE], + sidebar_icon=data[ATTR_ICON], + js_url='/api/hassio/app/entrypoint.js', + embed_iframe=True, + require_admin=data[ATTR_ADMIN], + config={ + "ingress": addon + } + ) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 05c183ccd60..85ae6473562 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,18 +1,19 @@ """Implement the auth feature from Hass.io for Add-ons.""" -from ipaddress import ip_address import logging import os +from ipaddress import ip_address +import voluptuous as vol from aiohttp import web from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound -import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME @@ -27,7 +28,7 @@ SCHEMA_API_AUTH = vol.Schema({ @callback -def async_setup_auth(hass): +def async_setup_auth_view(hass: HomeAssistantType): """Auth setup.""" hassio_auth = HassIOAuth(hass) hass.http.register_view(hassio_auth) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index e4132562c31..9656346cd2c 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -1,5 +1,6 @@ """Hass.io const variables.""" +ATTR_ADDONS = 'addons' ATTR_DISCOVERY = 'discovery' ATTR_ADDON = 'addon' ATTR_NAME = 'name' @@ -8,6 +9,11 @@ ATTR_CONFIG = 'config' ATTR_UUID = 'uuid' ATTR_USERNAME = 'username' ATTR_PASSWORD = 'password' +ATTR_PANELS = 'panels' +ATTR_ENABLE = 'enable' +ATTR_TITLE = 'title' +ATTR_ICON = 'icon' +ATTR_ADMIN = 'admin' X_HASSIO = 'X-Hassio-Key' X_INGRESS_PATH = "X-Ingress-Path" diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 09a98edc148..90953d634c3 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,9 +5,9 @@ import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable -from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CoreState, callback +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView from .const import ( ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_NAME, ATTR_SERVICE, @@ -18,12 +18,13 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_discovery(hass, hassio, config): +def async_setup_discovery_view(hass: HomeAssistantView, hassio): """Discovery setup.""" - hassio_discovery = HassIODiscovery(hass, hassio, config) + hassio_discovery = HassIODiscovery(hass, hassio) + hass.http.register_view(hassio_discovery) # Handle exists discovery messages - async def async_discovery_start_handler(event): + async def _async_discovery_start_handler(event): """Process all exists discovery on startup.""" try: data = await hassio.retrieve_discovery_messages() @@ -36,13 +37,8 @@ def async_setup_discovery(hass, hassio, config): if jobs: await asyncio.wait(jobs) - if hass.state == CoreState.running: - hass.async_create_task(async_discovery_start_handler(None)) - else: - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, async_discovery_start_handler) - - hass.http.register_view(hassio_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, _async_discovery_start_handler) class HassIODiscovery(HomeAssistantView): @@ -51,11 +47,10 @@ class HassIODiscovery(HomeAssistantView): name = "api:hassio_push:discovery" url = "/api/hassio_push/discovery/{uuid}" - def __init__(self, hass, hassio, config): + def __init__(self, hass: HomeAssistantView, hassio): """Initialize WebView.""" self.hass = hass self.hassio = hassio - self.config = config async def post(self, request, uuid): """Handle new discovery requests.""" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 7eddc639690..aae1f31d486 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -81,6 +81,14 @@ class HassIO: return self.send_command( "/addons/{}/info".format(addon), method="get") + @_api_data + def get_ingress_panels(self): + """Return data for Add-on ingress panels. + + This method return a coroutine. + """ + return self.send_command("/ingress/panels", method="get") + @_api_bool def restart_homeassistant(self): """Restart Home-Assistant container. diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 0ba83f1ca1b..824dee86fad 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) @callback -def async_setup_ingress(hass: HomeAssistantType, host: str): +def async_setup_ingress_view(hass: HomeAssistantType, host: str): """Auth setup.""" websession = hass.helpers.aiohttp_client.async_get_clientsession() diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 23095064d55..24782e45799 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -5,6 +5,7 @@ "requirements": [], "dependencies": [ "http", + "frontend", "panel_custom" ], "codeowners": [ diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py new file mode 100644 index 00000000000..05915218659 --- /dev/null +++ b/tests/components/hassio/test_addon_panel.py @@ -0,0 +1,128 @@ +"""Test add-on panel.""" +from unittest.mock import patch, Mock + +import pytest + +from homeassistant.setup import async_setup_component +from homeassistant.const import HTTP_HEADER_HA_AUTH + +from tests.common import mock_coro +from . import API_PASSWORD + + +@pytest.fixture(autouse=True) +def mock_all(aioclient_mock): + """Mock all setup requests.""" + aioclient_mock.post( + "http://127.0.0.1/homeassistant/options", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/supervisor/ping", json={'result': 'ok'}) + aioclient_mock.post( + "http://127.0.0.1/supervisor/options", json={'result': 'ok'}) + aioclient_mock.get( + "http://127.0.0.1/homeassistant/info", json={ + 'result': 'ok', 'data': {'last_version': '10.0'}}) + + +async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env): + """Test startup and panel setup after event.""" + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={ + 'result': 'ok', 'data': {'panels': { + "test1": { + "enable": True, + "title": "Test", + "icon": "mdi:test", + "admin": False + }, + "test2": { + "enable": False, + "title": "Test 2", + "icon": "mdi:test2", + "admin": True + }, + }}}) + + assert aioclient_mock.call_count == 0 + + with patch( + 'homeassistant.components.hassio.addon_panel._register_panel', + Mock(return_value=mock_coro()) + ) as mock_panel: + await async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': API_PASSWORD + } + }) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_panel.called + mock_panel.assert_called_with( + hass, 'test1', { + 'enable': True, 'title': 'Test', + 'icon': 'mdi:test', 'admin': False + }) + + +async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, + hass_client): + """Test panel api after event.""" + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={ + 'result': 'ok', 'data': {'panels': { + "test1": { + "enable": True, + "title": "Test", + "icon": "mdi:test", + "admin": False + }, + "test2": { + "enable": False, + "title": "Test 2", + "icon": "mdi:test2", + "admin": True + }, + }}}) + + assert aioclient_mock.call_count == 0 + + with patch( + 'homeassistant.components.hassio.addon_panel._register_panel', + Mock(return_value=mock_coro()) + ) as mock_panel: + await async_setup_component(hass, 'hassio', { + 'http': { + 'api_password': API_PASSWORD + } + }) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 2 + assert mock_panel.called + mock_panel.assert_called_with( + hass, 'test1', { + 'enable': True, 'title': 'Test', + 'icon': 'mdi:test', 'admin': False + }) + + hass_client = await hass_client() + + resp = await hass_client.post( + '/api/hassio_push/panel/test2', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) + assert resp.status == 400 + + resp = await hass_client.post( + '/api/hassio_push/panel/test1', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) + assert resp.status == 200 + assert mock_panel.call_count == 2 + + mock_panel.assert_called_with( + hass, 'test1', { + 'enable': True, 'title': 'Test', + 'icon': 'mdi:test', 'admin': False + }) diff --git a/tests/components/hassio/test_handler.py b/tests/components/hassio/test_handler.py index 3e7b9e95d92..372d567c021 100644 --- a/tests/components/hassio/test_handler.py +++ b/tests/components/hassio/test_handler.py @@ -105,3 +105,23 @@ async def test_api_retrieve_discovery(hassio_handler, aioclient_mock): data = await hassio_handler.retrieve_discovery_messages() assert data['discovery'][-1]['service'] == "mqtt" assert aioclient_mock.call_count == 1 + + +async def test_api_ingress_panels(hassio_handler, aioclient_mock): + """Test setup with API Ingress panels.""" + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={'result': 'ok', 'data': { + "panels": { + "slug": { + "enable": True, + "title": "Test", + "icon": "mdi:test", + "admin": False + } + } + }}) + + data = await hassio_handler.get_ingress_panels() + assert aioclient_mock.call_count == 1 + assert data['panels'] + assert "slug" in data['panels'] diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index f1f148f8495..7b8fad3ec09 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -31,6 +31,9 @@ def mock_all(aioclient_mock): aioclient_mock.get( "http://127.0.0.1/homeassistant/info", json={ 'result': 'ok', 'data': {'last_version': '10.0'}}) + aioclient_mock.get( + "http://127.0.0.1/ingress/panels", json={ + 'result': 'ok', 'data': {'panels': {}}}) @asyncio.coroutine @@ -40,7 +43,7 @@ def test_setup_api_ping(hass, aioclient_mock): result = yield from async_setup_component(hass, 'hassio', {}) assert result - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 assert hass.components.hassio.get_homeassistant_version() == "10.0" assert hass.components.hassio.is_hassio() @@ -79,7 +82,7 @@ def test_setup_api_push_api_data(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 assert not aioclient_mock.mock_calls[1][2]['ssl'] assert aioclient_mock.mock_calls[1][2]['port'] == 9999 assert aioclient_mock.mock_calls[1][2]['watchdog'] @@ -98,7 +101,7 @@ def test_setup_api_push_api_data_server_host(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 assert not aioclient_mock.mock_calls[1][2]['ssl'] assert aioclient_mock.mock_calls[1][2]['port'] == 9999 assert not aioclient_mock.mock_calls[1][2]['watchdog'] @@ -114,7 +117,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, }) assert result - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 assert not aioclient_mock.mock_calls[1][2]['ssl'] assert aioclient_mock.mock_calls[1][2]['port'] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]['refresh_token'] @@ -174,7 +177,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, }) assert result - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 assert not aioclient_mock.mock_calls[1][2]['ssl'] assert aioclient_mock.mock_calls[1][2]['port'] == 8123 assert aioclient_mock.mock_calls[1][2]['refresh_token'] == token.token @@ -192,7 +195,7 @@ def test_setup_core_push_timezone(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 4 + assert aioclient_mock.call_count == 5 assert aioclient_mock.mock_calls[2][2]['timezone'] == "testzone" @@ -206,7 +209,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): }) assert result - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456" @@ -285,14 +288,14 @@ def test_service_calls(hassio_env, hass, aioclient_mock): 'hassio', 'addon_stdin', {'addon': 'test', 'input': 'test'}) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 5 + assert aioclient_mock.call_count == 6 assert aioclient_mock.mock_calls[-1][2] == 'test' yield from hass.services.async_call('hassio', 'host_shutdown', {}) yield from hass.services.async_call('hassio', 'host_reboot', {}) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 7 + assert aioclient_mock.call_count == 8 yield from hass.services.async_call('hassio', 'snapshot_full', {}) yield from hass.services.async_call('hassio', 'snapshot_partial', { @@ -302,7 +305,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): }) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][2] == { 'addons': ['test'], 'folders': ['ssl'], 'password': "123456"} @@ -318,7 +321,7 @@ def test_service_calls(hassio_env, hass, aioclient_mock): }) yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 11 + assert aioclient_mock.call_count == 12 assert aioclient_mock.mock_calls[-1][2] == { 'addons': ['test'], 'folders': ['ssl'], 'homeassistant': False, 'password': "123456" @@ -338,12 +341,12 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock): yield from hass.services.async_call('homeassistant', 'stop') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 yield from hass.services.async_call('homeassistant', 'check_config') yield from hass.async_block_till_done() - assert aioclient_mock.call_count == 2 + assert aioclient_mock.call_count == 3 with patch( 'homeassistant.config.async_check_ha_config_file', @@ -353,4 +356,4 @@ def test_service_calls_core(hassio_env, hass, aioclient_mock): yield from hass.async_block_till_done() assert mock_check_config.called - assert aioclient_mock.call_count == 3 + assert aioclient_mock.call_count == 4 From b2a7699cdfc4292609f1eec96e7c16eae21e1c58 Mon Sep 17 00:00:00 2001 From: Pascal Roeleven Date: Fri, 19 Apr 2019 13:26:53 +0200 Subject: [PATCH 042/346] Change configuration for orangepi (#23231) --- homeassistant/components/orangepi_gpio/__init__.py | 4 ++-- homeassistant/components/orangepi_gpio/binary_sensor.py | 6 +++--- homeassistant/components/orangepi_gpio/const.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 072a05e0dd7..79ebf01ed61 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -6,9 +6,9 @@ from homeassistant.const import ( _LOGGER = logging.getLogger(__name__) -CONF_PINMODE = 'pinmode' +CONF_PIN_MODE = 'pin_mode' DOMAIN = 'orangepi_gpio' -PINMODES = ['pc', 'zeroplus', 'zeroplus2', 'deo', 'neocore2'] +PIN_MODES = ['pc', 'zeroplus', 'zeroplus2', 'deo', 'neocore2'] def setup(hass, config): diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py index 1c5a447b101..10eddb1e041 100644 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -6,7 +6,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, PLATFORM_SCHEMA) from homeassistant.const import DEVICE_DEFAULT_NAME -from . import CONF_PINMODE +from . import CONF_PIN_MODE from .const import CONF_INVERT_LOGIC, CONF_PORTS, PORT_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -16,8 +16,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Orange Pi GPIO devices.""" - pinmode = config[CONF_PINMODE] - orangepi_gpio.setup_mode(pinmode) + pin_mode = config[CONF_PIN_MODE] + orangepi_gpio.setup_mode(pin_mode) invert_logic = config[CONF_INVERT_LOGIC] diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py index 422660f1f64..373df656b25 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -3,7 +3,7 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv -from . import CONF_PINMODE, PINMODES +from . import CONF_PIN_MODE, PIN_MODES CONF_INVERT_LOGIC = 'invert_logic' CONF_PORTS = 'ports' @@ -16,6 +16,6 @@ _SENSORS_SCHEMA = vol.Schema({ PORT_SCHEMA = { vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Required(CONF_PINMODE): vol.In(PINMODES), + vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES), vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, } From b3a8b0056b2765f0fcd4f4d2c3a41db7a18dff4f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 19 Apr 2019 13:38:50 +0100 Subject: [PATCH 043/346] Add and use an async_fire_service_discovered helper (#23232) --- tests/common.py | 9 +++++++++ tests/components/homekit_controller/common.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/common.py b/tests/common.py index 2467dae04b9..46e30187d45 100644 --- a/tests/common.py +++ b/tests/common.py @@ -269,6 +269,15 @@ def fire_service_discovered(hass, service, info): }) +@ha.callback +def async_fire_service_discovered(hass, service, info): + """Fire the MQTT message.""" + hass.bus.async_fire(EVENT_PLATFORM_DISCOVERED, { + ATTR_SERVICE: service, + ATTR_DISCOVERED: info + }) + + def load_fixture(filename): """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), 'fixtures', filename) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 5d85fba6ae3..43003251218 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -15,7 +15,7 @@ from homeassistant.components.homekit_controller.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - async_fire_time_changed, fire_service_discovered, load_fixture) + async_fire_time_changed, async_fire_service_discovered, load_fixture) class FakePairing: @@ -221,7 +221,7 @@ async def setup_test_accessories(hass, accessories, capitalize=False): } } - fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) + async_fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) await hass.async_block_till_done() return pairing @@ -245,7 +245,7 @@ async def device_config_changed(hass, accessories): } } - fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) + async_fire_service_discovered(hass, SERVICE_HOMEKIT, discovery_info) # Wait for services to reconfigure await hass.async_block_till_done() From 21a194f9d8b64f5c3da655bf45982446e7affeda Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 19 Apr 2019 13:39:06 +0100 Subject: [PATCH 044/346] Review feedback from #23191 (#23233) --- homeassistant/components/homekit_controller/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 3fa4ade519e..1b1c7b96b58 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -161,7 +161,11 @@ async def async_setup(hass, config): hass.data[CONTROLLER] = controller = homekit.Controller() - for hkid, pairing_data in load_old_pairings(hass).items(): + old_pairings = await hass.async_add_executor_job( + load_old_pairings, + hass + ) + for hkid, pairing_data in old_pairings.items(): controller.pairings[hkid] = IpPairing(pairing_data) def discovery_dispatch(service, discovery_info): From 6e300bd438464975d3da345dd04522bfa14430d3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 19 Apr 2019 15:14:48 +0200 Subject: [PATCH 045/346] Add missing service for persistent_notification (#23230) --- .../components/persistent_notification/services.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index ca73c6d56bb..496ab9199c3 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -17,3 +17,10 @@ dismiss: notification_id: description: Target ID of the notification, which should be removed. [Required] example: 1234 + +mark_read: + description: Mark a notification read. + fields: + notification_id: + description: Target ID of the notification, which should be mark read. [Required] + example: 1234 From b1b269b302efff3565719e3ce1666438ad8b65a8 Mon Sep 17 00:00:00 2001 From: Christopher Viel Date: Fri, 19 Apr 2019 09:21:16 -0400 Subject: [PATCH 046/346] Add more CPU temp. labels to Glances (#23179) --- homeassistant/components/glances/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 2a883e33da6..9e31887cebb 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -175,8 +175,9 @@ class GlancesSensor(Entity): self._state = value['quicklook']['cpu'] elif self.type == 'cpu_temp': for sensor in value['sensors']: - if sensor['label'] in ['CPU', "Package id 0", - "Physical id 0", "cpu-thermal 1", + if sensor['label'] in ['CPU', "CPU Temperature", + "Package id 0", "Physical id 0", + "cpu_thermal 1", "cpu-thermal 1", "exynos-therm 1", "soc_thermal 1"]: self._state = sensor['value'] elif self.type == 'docker_active': From 9cf9be88507ad9c975f2d2ca5e12ce48328728ba Mon Sep 17 00:00:00 2001 From: GoNzCiD Date: Fri, 19 Apr 2019 18:42:27 +0200 Subject: [PATCH 047/346] Add accuracy and status for Traccar (#23180) * Fix read gps position accuracy & read device status * Fix: W291 trailing whitespace & E501 line too long (80 > 79 characters) * Upgrade pytraccar dependency to 0.7.0 * met snake case --- .../components/traccar/device_tracker.py | 50 +++++++++++-------- .../components/traccar/manifest.json | 4 +- requirements_all.txt | 2 +- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 1600227bfe2..39d1c2dd370 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -25,6 +25,7 @@ ATTR_MOTION = 'motion' ATTR_SPEED = 'speed' ATTR_TRACKER = 'tracker' ATTR_TRACCAR_ID = 'traccar_id' +ATTR_STATUS = 'status' EVENT_DEVICE_MOVING = 'device_moving' EVENT_COMMAND_RESULT = 'command_result' @@ -131,30 +132,39 @@ class TraccarScanner: async def import_device_data(self): """Import device data from Traccar.""" - for devicename in self._api.device_info: - device = self._api.device_info[devicename] + for device_unique_id in self._api.device_info: + device_info = self._api.device_info[device_unique_id] + device = None attr = {} attr[ATTR_TRACKER] = 'traccar' - if device.get('address') is not None: - attr[ATTR_ADDRESS] = device['address'] - if device.get('geofence') is not None: - attr[ATTR_GEOFENCE] = device['geofence'] - if device.get('category') is not None: - attr[ATTR_CATEGORY] = device['category'] - if device.get('speed') is not None: - attr[ATTR_SPEED] = device['speed'] - if device.get('battery') is not None: - attr[ATTR_BATTERY_LEVEL] = device['battery'] - if device.get('motion') is not None: - attr[ATTR_MOTION] = device['motion'] - if device.get('traccar_id') is not None: - attr[ATTR_TRACCAR_ID] = device['traccar_id'] + if device_info.get('address') is not None: + attr[ATTR_ADDRESS] = device_info['address'] + if device_info.get('geofence') is not None: + attr[ATTR_GEOFENCE] = device_info['geofence'] + if device_info.get('category') is not None: + attr[ATTR_CATEGORY] = device_info['category'] + if device_info.get('speed') is not None: + attr[ATTR_SPEED] = device_info['speed'] + if device_info.get('battery') is not None: + attr[ATTR_BATTERY_LEVEL] = device_info['battery'] + if device_info.get('motion') is not None: + attr[ATTR_MOTION] = device_info['motion'] + if device_info.get('traccar_id') is not None: + attr[ATTR_TRACCAR_ID] = device_info['traccar_id'] + for dev in self._api.devices: + if dev['id'] == device_info['traccar_id']: + device = dev + break + if device is not None and device.get('status') is not None: + attr[ATTR_STATUS] = device['status'] for custom_attr in self._custom_attributes: - if device.get(custom_attr) is not None: - attr[custom_attr] = device[custom_attr] + if device_info.get(custom_attr) is not None: + attr[custom_attr] = device_info[custom_attr] await self._async_see( - dev_id=slugify(device['device_id']), - gps=(device.get('latitude'), device.get('longitude')), + dev_id=slugify(device_info['device_id']), + gps=(device_info.get('latitude'), + device_info.get('longitude')), + gps_accuracy=(device_info.get('accuracy')), attributes=attr) async def import_events(self): diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 57bd1383363..5c859fefb71 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,11 +3,11 @@ "name": "Traccar", "documentation": "https://www.home-assistant.io/components/traccar", "requirements": [ - "pytraccar==0.5.0", + "pytraccar==0.7.0", "stringcase==1.2.0" ], "dependencies": [], "codeowners": [ "@ludeeus" ] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 57f8d813af1..c1e381e760b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1429,7 +1429,7 @@ pytile==2.0.6 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.5.0 +pytraccar==0.7.0 # homeassistant.components.trackr pytrackr==0.0.5 From e7054e0fd27b5d86b4d1b2d2d597b21fee450af8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 19 Apr 2019 18:59:54 +0100 Subject: [PATCH 048/346] Avoid calling async code in sync context (#23235) --- .../homekit_controller/connection.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 2b82370d187..af438c68164 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -101,25 +101,27 @@ class HKDevice(): return True - async def async_refresh_entity_map(self, config_num): + def refresh_entity_map(self, config_num): """ Handle setup of a HomeKit accessory. The sync version will be removed when homekit_controller migrates to config flow. """ - return await self.hass.async_add_executor_job( - self.refresh_entity_map, + self.hass.add_job( + self.async_refresh_entity_map, config_num, ) - def refresh_entity_map(self, config_num): + async def async_refresh_entity_map(self, config_num): """Handle setup of a HomeKit accessory.""" # pylint: disable=import-error from homekit.exceptions import AccessoryDisconnectedError try: - accessories = self.pairing.list_accessories_and_characteristics() + self.accessories = await self.hass.async_add_executor_job( + self.pairing.list_accessories_and_characteristics, + ) except AccessoryDisconnectedError: # If we fail to refresh this data then we will naturally retry # later when Bonjour spots c# is still not up to date. @@ -128,18 +130,17 @@ class HKDevice(): self.hass.data[ENTITY_MAP].async_create_or_update_map( self.unique_id, config_num, - accessories, + self.accessories, ) - self.accessories = accessories self.config_num = config_num # For BLE, the Pairing instance relies on the entity map to map # aid/iid to GATT characteristics. So push it to there as well. - self.pairing.pairing_data['accessories'] = accessories + self.pairing.pairing_data['accessories'] = self.accessories # Register add new entities that are available - self.add_entities() + await self.hass.async_add_executor_job(self.add_entities) return True From c899e2a662efdf3589e0c5482fec3a58e333a2fc Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Fri, 19 Apr 2019 19:01:54 +0100 Subject: [PATCH 049/346] Name sensors correctly (#23208) * Hue motion senors are motion sensors, not presence sensors. * Name the sensors 'motion' instead of 'presence' - match the HA paradigm. --- homeassistant/components/hue/binary_sensor.py | 2 +- tests/components/hue/test_sensor_base.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 3286f185ea4..b9921a9a01f 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, async_setup_entry as shared_async_setup_entry) -PRESENCE_NAME_FORMAT = "{} presence" +PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 99829c59666..38eb3d8c55b 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -391,7 +391,7 @@ async def test_sensors(hass, mock_bridge): assert len(hass.states.async_all()) == 6 presence_sensor_1 = hass.states.get( - 'binary_sensor.living_room_sensor_presence') + 'binary_sensor.living_room_sensor_motion') light_level_sensor_1 = hass.states.get( 'sensor.living_room_sensor_light_level') temperature_sensor_1 = hass.states.get( @@ -406,7 +406,7 @@ async def test_sensors(hass, mock_bridge): assert temperature_sensor_1.name == 'Living room sensor temperature' presence_sensor_2 = hass.states.get( - 'binary_sensor.kitchen_sensor_presence') + 'binary_sensor.kitchen_sensor_motion') light_level_sensor_2 = hass.states.get( 'sensor.kitchen_sensor_light_level') temperature_sensor_2 = hass.states.get( @@ -459,7 +459,7 @@ async def test_new_sensor_discovered(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 2 assert len(hass.states.async_all()) == 9 - presence = hass.states.get('binary_sensor.bedroom_sensor_presence') + presence = hass.states.get('binary_sensor.bedroom_sensor_motion') assert presence is not None assert presence.state == 'on' temperature = hass.states.get('sensor.bedroom_sensor_temperature') From 887e1cd8e32037a5f95cedd6ba1988ad79e22740 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 21:19:46 +0200 Subject: [PATCH 050/346] Drop unnecessary block_till_done, improve tests (#23246) --- .../mqtt/test_alarm_control_panel.py | 688 +++++++++--------- 1 file changed, 339 insertions(+), 349 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 6efaedd270b..882f748fe4c 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1,6 +1,5 @@ """The tests the MQTT alarm control panel component.""" import json -import unittest from unittest.mock import ANY from homeassistant.components import alarm_control_panel, mqtt @@ -9,398 +8,408 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.setup import setup_component from tests.common import ( MockConfigEntry, assert_setup_component, async_fire_mqtt_message, - async_mock_mqtt_component, async_setup_component, fire_mqtt_message, - get_test_home_assistant, mock_mqtt_component, mock_registry) + async_mock_mqtt_component, async_setup_component, mock_registry) from tests.components.alarm_control_panel import common CODE = 'HELLO_CODE' -class TestAlarmControlPanelMQTT(unittest.TestCase): - """Test the manual alarm module.""" - - # pylint: disable=invalid-name - - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down stuff we started.""" - self.hass.stop() - - def test_fail_setup_without_state_topic(self): - """Test for failing with no state topic.""" - with assert_setup_component(0) as config: - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'command_topic': 'alarm/command' - } - }) - assert not config[alarm_control_panel.DOMAIN] - - def test_fail_setup_without_command_topic(self): - """Test failing with no command topic.""" - with assert_setup_component(0): - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'state_topic': 'alarm/state' - } - }) - - def test_update_state_via_state_topic(self): - """Test updating with via state topic.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { +async def test_fail_setup_without_state_topic(hass, mqtt_mock): + """Test for failing with no state topic.""" + with assert_setup_component(0) as config: + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { alarm_control_panel.DOMAIN: { 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', + 'command_topic': 'alarm/command' + } + }) + assert not config[alarm_control_panel.DOMAIN] + + +async def test_fail_setup_without_command_topic(hass, mqtt_mock): + """Test failing with no command topic.""" + with assert_setup_component(0): + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'state_topic': 'alarm/state' } }) - entity_id = 'alarm_control_panel.test' - assert STATE_UNKNOWN == \ - self.hass.states.get(entity_id).state +async def test_update_state_via_state_topic(hass, mqtt_mock): + """Test updating with via state topic.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) - for state in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED): - fire_mqtt_message(self.hass, 'alarm/state', state) - self.hass.block_till_done() - assert state == self.hass.states.get(entity_id).state + entity_id = 'alarm_control_panel.test' - def test_ignore_update_state_if_unknown_via_state_topic(self): - """Test ignoring updates via state topic.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - } - }) + assert STATE_UNKNOWN == \ + hass.states.get(entity_id).state - entity_id = 'alarm_control_panel.test' + for state in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED): + async_fire_mqtt_message(hass, 'alarm/state', state) + assert state == hass.states.get(entity_id).state - assert STATE_UNKNOWN == \ - self.hass.states.get(entity_id).state - fire_mqtt_message(self.hass, 'alarm/state', 'unsupported state') - self.hass.block_till_done() - assert STATE_UNKNOWN == self.hass.states.get(entity_id).state +async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): + """Test ignoring updates via state topic.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) - def test_arm_home_publishes_mqtt(self): - """Test publishing of MQTT messages while armed.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - } - }) + entity_id = 'alarm_control_panel.test' - common.alarm_arm_home(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'ARM_HOME', 0, False) + assert STATE_UNKNOWN == \ + hass.states.get(entity_id).state - def test_arm_home_not_publishes_mqtt_with_invalid_code_when_req(self): - """Test not publishing of MQTT messages with invalid. + async_fire_mqtt_message(hass, 'alarm/state', 'unsupported state') + assert STATE_UNKNOWN == hass.states.get(entity_id).state - When code_arm_required = True - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_arm_required': True - } - }) - call_count = self.mock_publish.call_count - common.alarm_arm_home(self.hass, 'abcd') - self.hass.block_till_done() - assert call_count == self.mock_publish.call_count +async def test_arm_home_publishes_mqtt(hass, mqtt_mock): + """Test publishing of MQTT messages while armed.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) - def test_arm_home_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages. + common.async_alarm_arm_home(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_HOME', 0, False) - When code_arm_required = False - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_arm_required': False - } - }) - common.alarm_arm_home(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'ARM_HOME', 0, False) +async def test_arm_home_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid. - def test_arm_away_publishes_mqtt(self): - """Test publishing of MQTT messages while armed.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - } - }) + When code_arm_required = True + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_arm_required': True + } + }) - common.alarm_arm_away(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'ARM_AWAY', 0, False) + call_count = mqtt_mock.async_publish.call_count + common.async_alarm_arm_home(hass, 'abcd') + await hass.async_block_till_done() + assert call_count == mqtt_mock.async_publish.call_count - def test_arm_away_not_publishes_mqtt_with_invalid_code_when_req(self): - """Test not publishing of MQTT messages with invalid code. - When code_arm_required = True - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_arm_required': True - } - }) +async def test_arm_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages. - call_count = self.mock_publish.call_count - common.alarm_arm_away(self.hass, 'abcd') - self.hass.block_till_done() - assert call_count == self.mock_publish.call_count + When code_arm_required = False + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_arm_required': False + } + }) - def test_arm_away_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages. + common.async_alarm_arm_home(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_HOME', 0, False) - When code_arm_required = False - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_arm_required': False - } - }) - common.alarm_arm_away(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'ARM_AWAY', 0, False) +async def test_arm_away_publishes_mqtt(hass, mqtt_mock): + """Test publishing of MQTT messages while armed.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) - def test_arm_night_publishes_mqtt(self): - """Test publishing of MQTT messages while armed.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - } - }) + common.async_alarm_arm_away(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_AWAY', 0, False) - common.alarm_arm_night(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'ARM_NIGHT', 0, False) - def test_arm_night_not_publishes_mqtt_with_invalid_code_when_req(self): - """Test not publishing of MQTT messages with invalid code. +async def test_arm_away_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid code. - When code_arm_required = True - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_arm_required': True - } - }) + When code_arm_required = True + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_arm_required': True + } + }) - call_count = self.mock_publish.call_count - common.alarm_arm_night(self.hass, 'abcd') - self.hass.block_till_done() - assert call_count == self.mock_publish.call_count + call_count = mqtt_mock.async_publish.call_count + common.async_alarm_arm_away(hass, 'abcd') + await hass.async_block_till_done() + assert call_count == mqtt_mock.async_publish.call_count - def test_arm_night_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages. - When code_arm_required = False - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_arm_required': False - } - }) +async def test_arm_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages. - common.alarm_arm_night(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'ARM_NIGHT', 0, False) + When code_arm_required = False + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_arm_required': False + } + }) - def test_disarm_publishes_mqtt(self): - """Test publishing of MQTT messages while disarmed.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - } - }) + common.async_alarm_arm_away(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_AWAY', 0, False) - common.alarm_disarm(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'DISARM', 0, False) - def test_disarm_publishes_mqtt_with_template(self): - """Test publishing of MQTT messages while disarmed. +async def test_arm_night_publishes_mqtt(hass, mqtt_mock): + """Test publishing of MQTT messages while armed.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) - When command_template set to output json - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'command_template': '{\"action\":\"{{ action }}\",' - '\"code\":\"{{ code }}\"}', - } - }) + common.async_alarm_arm_night(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_NIGHT', 0, False) - common.alarm_disarm(self.hass, 1234) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', '{\"action\":\"DISARM\",\"code\":\"1234\"}', - 0, - False) - def test_disarm_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages while disarmed. +async def test_arm_night_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid code. - When code_disarm_required = False - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_disarm_required': False - } - }) + When code_arm_required = True + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_arm_required': True + } + }) - common.alarm_disarm(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'DISARM', 0, False) + call_count = mqtt_mock.async_publish.call_count + common.async_alarm_arm_night(hass, 'abcd') + await hass.async_block_till_done() + assert call_count == mqtt_mock.async_publish.call_count - def test_disarm_not_publishes_mqtt_with_invalid_code_when_req(self): - """Test not publishing of MQTT messages with invalid code. - When code_disarm_required = True - """ - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'code_disarm_required': True - } - }) +async def test_arm_night_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages. - call_count = self.mock_publish.call_count - common.alarm_disarm(self.hass, 'abcd') - self.hass.block_till_done() - assert call_count == self.mock_publish.call_count + When code_arm_required = False + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_arm_required': False + } + }) - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'availability_topic': 'availability-topic' - } - }) + common.async_alarm_arm_night(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'ARM_NIGHT', 0, False) - state = self.hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() +async def test_disarm_publishes_mqtt(hass, mqtt_mock): + """Test publishing of MQTT messages while disarmed.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + } + }) - state = self.hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE != state.state + common.async_alarm_disarm(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'DISARM', 0, False) - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - state = self.hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state +async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): + """Test publishing of MQTT messages while disarmed. - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - assert setup_component(self.hass, alarm_control_panel.DOMAIN, { - alarm_control_panel.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', - 'code': '1234', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - }) + When command_template set to output json + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'command_template': '{\"action\":\"{{ action }}\",' + '\"code\":\"{{ code }}\"}', + } + }) - state = self.hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state + common.async_alarm_disarm(hass, 1234) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', '{\"action\":\"DISARM\",\"code\":\"1234\"}', + 0, + False) - fire_mqtt_message(self.hass, 'availability-topic', 'good') + +async def test_disarm_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages while disarmed. + + When code_disarm_required = False + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_disarm_required': False + } + }) + + common.async_alarm_disarm(hass) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'alarm/command', 'DISARM', 0, False) + + +async def test_disarm_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid code. + + When code_disarm_required = True + """ + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'code_disarm_required': True + } + }) + + call_count = mqtt_mock.async_publish.call_count + common.async_alarm_disarm(hass, 'abcd') + await hass.async_block_till_done() + assert call_count == mqtt_mock.async_publish.call_count + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'availability_topic': 'availability-topic' + } + }) + + state = hass.states.get('alarm_control_panel.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'online') + + state = hass.states.get('alarm_control_panel.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + + state = hass.states.get('alarm_control_panel.test') + assert STATE_UNAVAILABLE == state.state + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + assert await async_setup_component(hass, alarm_control_panel.DOMAIN, { + alarm_control_panel.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'alarm/state', + 'command_topic': 'alarm/command', + 'code': '1234', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) + + state = hass.states.get('alarm_control_panel.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + + state = hass.states.get('alarm_control_panel.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + + state = hass.states.get('alarm_control_panel.test') + assert STATE_UNAVAILABLE == state.state async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -416,7 +425,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.test') assert '100' == state.attributes.get('val') @@ -443,7 +451,6 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): assert STATE_UNKNOWN == state.state async_fire_mqtt_message(hass, 'test-topic', '100') - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.test') assert STATE_ALARM_ARMED_AWAY == state.state @@ -462,7 +469,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.test') assert state.attributes.get('val') is None @@ -482,7 +488,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.test') assert state.attributes.get('val') is None @@ -507,8 +512,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): hass, 'homeassistant/alarm_control_panel/bla/config', data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.beer') assert '100' == state.attributes.get('val') @@ -516,19 +519,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message( hass, 'homeassistant/alarm_control_panel/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.beer') assert '75' == state.attributes.get('val') @@ -552,7 +550,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(alarm_control_panel.DOMAIN)) == 1 @@ -580,7 +577,6 @@ async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): 'homeassistant/alarm_control_panel/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.beer') assert state is None @@ -615,7 +611,6 @@ async def test_discovery_update_alarm(hass, mqtt_mock, caplog): 'homeassistant/alarm_control_panel/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.beer') assert state is not None @@ -651,7 +646,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): 'homeassistant/alarm_control_panel/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.milk') assert state is not None @@ -687,7 +681,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message( hass, 'homeassistant/alarm_control_panel/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -728,7 +721,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message( hass, 'homeassistant/alarm_control_panel/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -739,7 +731,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message( hass, 'homeassistant/alarm_control_panel/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -771,7 +762,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity( 'alarm_control_panel.beer', new_entity_id='alarm_control_panel.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.beer') assert state is None From 0e429cca33c951dd899881c529735767cf1c024f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 21:26:56 +0200 Subject: [PATCH 051/346] Drop unnecessary block_till_done, improve tests (#23247) --- tests/components/mqtt/test_binary_sensor.py | 444 +++++++++----------- 1 file changed, 205 insertions(+), 239 deletions(-) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 3e6e36cd050..2c8faf66549 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1,285 +1,269 @@ """The tests for the MQTT binary sensor platform.""" from datetime import timedelta import json -import unittest -from unittest.mock import ANY, Mock +from unittest.mock import ANY from homeassistant.components import binary_sensor, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, STATE_UNAVAILABLE) import homeassistant.core as ha -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, - fire_mqtt_message, fire_time_changed, get_test_home_assistant, - mock_component, mock_mqtt_component, mock_registry) + MockConfigEntry, async_fire_mqtt_message, async_fire_time_changed, + async_mock_mqtt_component, mock_registry) -class TestSensorMQTT(unittest.TestCase): - """Test the MQTT sensor.""" +async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): + """Test the setting of the value via MQTT.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + } + }) - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config_entries._async_schedule_save = Mock() - mock_mqtt_component(self.hass) + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() + async_fire_mqtt_message(hass, 'test-topic', 'ON') + state = hass.states.get('binary_sensor.test') + assert STATE_ON == state.state - def test_setting_sensor_value_via_mqtt_message(self): - """Test the setting of the value via MQTT.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'payload_on': 'ON', - 'payload_off': 'OFF', - } - }) + async_fire_mqtt_message(hass, 'test-topic', 'OFF') + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state - state = self.hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_ON == state.state +async def test_setting_sensor_value_via_mqtt_message_and_template( + hass, mqtt_mock): + """Test the setting of the value via MQTT.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'value_template': '{%if is_state(entity_id,\"on\")-%}OFF' + '{%-else-%}ON{%-endif%}' + } + }) - fire_mqtt_message(self.hass, 'test-topic', 'OFF') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state - def test_setting_sensor_value_via_mqtt_message_and_template(self): - """Test the setting of the value via MQTT.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'payload_on': 'ON', - 'payload_off': 'OFF', - 'value_template': '{%if is_state(entity_id,\"on\")-%}OFF' - '{%-else-%}ON{%-endif%}' - } - }) + async_fire_mqtt_message(hass, 'test-topic', '') + state = hass.states.get('binary_sensor.test') + assert STATE_ON == state.state - state = self.hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state + async_fire_mqtt_message(hass, 'test-topic', '') + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state - fire_mqtt_message(self.hass, 'test-topic', '') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_ON == state.state - fire_mqtt_message(self.hass, 'test-topic', '') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state +async def test_valid_device_class(hass, mqtt_mock): + """Test the setting of a valid sensor class.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'motion', + 'state_topic': 'test-topic', + } + }) - def test_valid_device_class(self): - """Test the setting of a valid sensor class.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'device_class': 'motion', - 'state_topic': 'test-topic', - } - }) + state = hass.states.get('binary_sensor.test') + assert 'motion' == state.attributes.get('device_class') - state = self.hass.states.get('binary_sensor.test') - assert 'motion' == state.attributes.get('device_class') - def test_invalid_device_class(self): - """Test the setting of an invalid sensor class.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'device_class': 'abc123', - 'state_topic': 'test-topic', - } - }) +async def test_invalid_device_class(hass, mqtt_mock): + """Test the setting of an invalid sensor class.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'abc123', + 'state_topic': 'test-topic', + } + }) - state = self.hass.states.get('binary_sensor.test') - assert state is None + state = hass.states.get('binary_sensor.test') + assert state is None - def test_availability_without_topic(self): - """Test availability without defined availability topic.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - } - }) - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + } + }) - def test_availability_by_defaults(self): - """Test availability by defaults with defined topic.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'availability_topic': 'availability-topic' - } - }) + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE != state.state - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() +async def test_availability_by_defaults(hass, mqtt_mock): + """Test availability by defaults with defined topic.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'availability_topic': 'availability-topic' + } + }) - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'availability-topic', 'online') - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE != state.state - def test_availability_by_custom_payload(self): - """Test availability by custom payload with defined topic.""" - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - }) + async_fire_mqtt_message(hass, 'availability-topic', 'offline') - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state +async def test_availability_by_custom_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + async_fire_mqtt_message(hass, 'availability-topic', 'good') - def test_force_update_disabled(self): - """Test force update option.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'payload_on': 'ON', - 'payload_off': 'OFF' - } - }) + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE != state.state - events = [] + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - assert 1 == len(events) +async def test_force_update_disabled(hass, mqtt_mock): + """Test force update option.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF' + } + }) - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - assert 1 == len(events) + events = [] - def test_force_update_enabled(self): - """Test force update option.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'payload_on': 'ON', - 'payload_off': 'OFF', - 'force_update': True - } - }) + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) - events = [] + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + assert 1 == len(events) - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + assert 1 == len(events) - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - assert 1 == len(events) - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - assert 2 == len(events) +async def test_force_update_enabled(hass, mqtt_mock): + """Test force update option.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'force_update': True + } + }) - def test_off_delay(self): - """Test off_delay option.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'payload_on': 'ON', - 'payload_off': 'OFF', - 'off_delay': 30, - 'force_update': True - } - }) + events = [] - events = [] + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + assert 1 == len(events) - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_ON == state.state - assert 1 == len(events) + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + assert 2 == len(events) - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_ON == state.state - assert 2 == len(events) - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=30)) - self.hass.block_till_done() - state = self.hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state - assert 3 == len(events) +async def test_off_delay(hass, mqtt_mock): + """Test off_delay option.""" + assert await async_setup_component(hass, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'payload_on': 'ON', + 'payload_off': 'OFF', + 'off_delay': 30, + 'force_update': True + } + }) + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test') + assert STATE_ON == state.state + assert 1 == len(events) + + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test') + assert STATE_ON == state.state + assert 2 == len(events) + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state + assert 3 == len(events) async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -294,7 +278,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert '100' == state.attributes.get('val') @@ -312,7 +295,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.attributes.get('val') is None @@ -331,7 +313,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') assert state.attributes.get('val') is None @@ -356,8 +337,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert '100' == state.attributes.get('val') @@ -365,19 +344,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert '75' == state.attributes.get('val') @@ -399,7 +373,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -421,7 +394,6 @@ async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert state is None @@ -449,7 +421,6 @@ async def test_discovery_update_binary_sensor(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert state is not None assert state.name == 'Milk' @@ -482,7 +453,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.milk') assert state is not None @@ -517,7 +487,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -557,7 +526,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -568,7 +536,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -599,7 +566,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity( 'binary_sensor.beer', new_entity_id='binary_sensor.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.beer') assert state is None From 13e0691c9075235b671ca3765514030f6ed98136 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:08:02 +0200 Subject: [PATCH 052/346] Drop unnecessary block_till_done, improve tests (#23248) --- tests/components/mqtt/test_camera.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 5726a64ba11..9774ba81b51 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,5 +1,4 @@ """The tests for mqtt camera component.""" -import asyncio from unittest.mock import ANY from homeassistant.components import camera, mqtt @@ -11,12 +10,11 @@ from tests.common import ( mock_registry) -@asyncio.coroutine -def test_run_camera_setup(hass, aiohttp_client): +async 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) - yield from async_setup_component(hass, 'camera', { + await async_mock_mqtt_component(hass) + await async_setup_component(hass, 'camera', { 'camera': { 'platform': 'mqtt', 'topic': topic, @@ -26,20 +24,18 @@ def test_run_camera_setup(hass, aiohttp_client): url = hass.states.get('camera.test_camera').attributes['entity_picture'] async_fire_mqtt_message(hass, topic, 'beer') - yield from hass.async_block_till_done() - client = yield from aiohttp_client(hass.http.app) - resp = yield from client.get(url) + client = await aiohttp_client(hass.http.app) + resp = await client.get(url) assert resp.status == 200 - body = yield from resp.text() + body = await resp.text() assert body == 'beer' -@asyncio.coroutine -def test_unique_id(hass): +async def test_unique_id(hass): """Test unique id option only creates one camera per unique_id.""" - yield from async_mock_mqtt_component(hass) - yield from async_setup_component(hass, 'camera', { + await async_mock_mqtt_component(hass) + await async_setup_component(hass, 'camera', { 'camera': [{ 'platform': 'mqtt', 'name': 'Test Camera 1', @@ -54,7 +50,6 @@ def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - yield from hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -71,7 +66,6 @@ async def test_discovery_removal_camera(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('camera.beer') assert state is not None @@ -80,7 +74,6 @@ async def test_discovery_removal_camera(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('camera.beer') assert state is None @@ -111,7 +104,6 @@ async def test_discovery_update_camera(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('camera.beer') assert state is not None @@ -143,7 +135,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/camera/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('camera.milk') assert state is not None @@ -173,7 +164,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('camera.beer', new_entity_id='camera.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('camera.beer') assert state is None From 557211240e577a5b8c15e1c3b865d4ba3ce232dc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:08:54 +0200 Subject: [PATCH 053/346] Drop unnecessary block_till_done, improve tests (#23249) --- tests/components/mqtt/test_climate.py | 1392 ++++++++++++------------- 1 file changed, 673 insertions(+), 719 deletions(-) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index a8e1ae6111e..15321301997 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -4,9 +4,6 @@ import json import unittest from unittest.mock import ANY -import pytest -import voluptuous as vol - from homeassistant.components import mqtt from homeassistant.components.climate import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) @@ -19,13 +16,10 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_LOW, SUPPORT_TARGET_TEMPERATURE_HIGH) from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE -from homeassistant.setup import setup_component -from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, - async_setup_component, fire_mqtt_message, get_test_home_assistant, - mock_component, mock_mqtt_component, mock_registry) + async_setup_component, mock_registry) from tests.components.climate import common ENTITY_CLIMATE = 'climate.test' @@ -46,700 +40,678 @@ DEFAULT_CONFIG = { }} -class TestMQTTClimate(unittest.TestCase): - """Test the mqtt climate hvac.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) - self.hass.config.units = METRIC_SYSTEM - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - def test_setup_params(self): - """Test the initial parameters.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert 21 == state.attributes.get('temperature') - assert "low" == state.attributes.get('fan_mode') - assert "off" == state.attributes.get('swing_mode') - assert "off" == state.attributes.get('operation_mode') - assert DEFAULT_MIN_TEMP == state.attributes.get('min_temp') - assert DEFAULT_MAX_TEMP == state.attributes.get('max_temp') - - def test_supported_features(self): - """Test the supported_features.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - support = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | - SUPPORT_SWING_MODE | SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | - SUPPORT_HOLD_MODE | SUPPORT_AUX_HEAT | - SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH) - - assert state.attributes.get("supported_features") == support - - def test_get_operation_modes(self): - """Test that the operation list returns the correct modes.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - modes = state.attributes.get('operation_list') - assert [ - STATE_AUTO, STATE_OFF, STATE_COOL, - STATE_HEAT, STATE_DRY, STATE_FAN_ONLY - ] == modes - - def test_set_operation_bad_attr_and_state(self): - """Test setting operation mode without required attribute. - - Also check the state. - """ - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - with pytest.raises(vol.Invalid): - common.set_operation_mode(self.hass, None, ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - - def test_set_operation(self): - """Test setting of new operation mode.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - assert "cool" == state.state - self.mock_publish.async_publish.assert_called_once_with( - 'mode-topic', 'cool', 0, False) - - def test_set_operation_pessimistic(self): - """Test setting operation mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['mode_state_topic'] = 'mode-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('operation_mode') is None - assert "unknown" == state.state - - common.set_operation_mode(self.hass, "cool", ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('operation_mode') is None - assert "unknown" == state.state - - fire_mqtt_message(self.hass, 'mode-state', 'cool') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - assert "cool" == state.state - - fire_mqtt_message(self.hass, 'mode-state', 'bogus mode') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - assert "cool" == state.state - - def test_set_operation_with_power_command(self): - """Test setting of new operation mode with power command enabled.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['power_command_topic'] = 'power-command' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - common.set_operation_mode(self.hass, "on", ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('operation_mode') - assert "on" == state.state - self.mock_publish.async_publish.assert_has_calls([ - unittest.mock.call('power-command', 'ON', 0, False), - unittest.mock.call('mode-topic', 'on', 0, False) - ]) - self.mock_publish.async_publish.reset_mock() - - common.set_operation_mode(self.hass, "off", ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - self.mock_publish.async_publish.assert_has_calls([ - unittest.mock.call('power-command', 'OFF', 0, False), - unittest.mock.call('mode-topic', 'off', 0, False) - ]) - self.mock_publish.async_publish.reset_mock() - - def test_set_fan_mode_bad_attr(self): - """Test setting fan mode without required attribute.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "low" == state.attributes.get('fan_mode') - with pytest.raises(vol.Invalid): - common.set_fan_mode(self.hass, None, ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "low" == state.attributes.get('fan_mode') - - def test_set_fan_mode_pessimistic(self): - """Test setting of new fan mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['fan_mode_state_topic'] = 'fan-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('fan_mode') is None - - common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('fan_mode') is None - - fire_mqtt_message(self.hass, 'fan-state', 'high') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') - - fire_mqtt_message(self.hass, 'fan-state', 'bogus mode') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') - - def test_set_fan_mode(self): - """Test setting of new fan mode.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "low" == state.attributes.get('fan_mode') - common.set_fan_mode(self.hass, 'high', ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'fan-mode-topic', 'high', 0, False) - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') - - def test_set_swing_mode_bad_attr(self): - """Test setting swing mode without required attribute.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('swing_mode') - with pytest.raises(vol.Invalid): - common.set_swing_mode(self.hass, None, ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('swing_mode') - - def test_set_swing_pessimistic(self): - """Test setting swing mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['swing_mode_state_topic'] = 'swing-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('swing_mode') is None - - common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('swing_mode') is None - - fire_mqtt_message(self.hass, 'swing-state', 'on') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') - - fire_mqtt_message(self.hass, 'swing-state', 'bogus state') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') - - def test_set_swing(self): - """Test setting of new swing mode.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('swing_mode') - common.set_swing_mode(self.hass, 'on', ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'swing-mode-topic', 'on', 0, False) - state = self.hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') - - def test_set_target_temperature(self): - """Test setting the target temperature.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert 21 == state.attributes.get('temperature') - common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'heat' == state.attributes.get('operation_mode') - self.mock_publish.async_publish.assert_called_once_with( - 'mode-topic', 'heat', 0, False) - self.mock_publish.async_publish.reset_mock() - common.set_temperature(self.hass, temperature=47, - entity_id=ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 47 == state.attributes.get('temperature') - self.mock_publish.async_publish.assert_called_once_with( - 'temperature-topic', 47, 0, False) - - # also test directly supplying the operation mode to set_temperature - self.mock_publish.async_publish.reset_mock() - common.set_temperature(self.hass, temperature=21, - operation_mode="cool", - entity_id=ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'cool' == state.attributes.get('operation_mode') - assert 21 == state.attributes.get('temperature') - self.mock_publish.async_publish.assert_has_calls([ - unittest.mock.call('mode-topic', 'cool', 0, False), - unittest.mock.call('temperature-topic', 21, 0, False) - ]) - self.mock_publish.async_publish.reset_mock() - - def test_set_target_temperature_pessimistic(self): - """Test setting the target temperature.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['temperature_state_topic'] = 'temperature-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('temperature') is None - common.set_operation_mode(self.hass, 'heat', ENTITY_CLIMATE) - self.hass.block_till_done() - common.set_temperature(self.hass, temperature=47, - entity_id=ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('temperature') is None - - fire_mqtt_message(self.hass, 'temperature-state', '1701') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('temperature') - - fire_mqtt_message(self.hass, 'temperature-state', 'not a number') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('temperature') - - def test_set_target_temperature_low_high(self): - """Test setting the low/high target temperature.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - common.set_temperature(self.hass, target_temp_low=20, - target_temp_high=23, - entity_id=ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - print(state.attributes) - assert 20 == state.attributes.get('target_temp_low') - assert 23 == state.attributes.get('target_temp_high') - self.mock_publish.async_publish.assert_any_call( - 'temperature-low-topic', 20, 0, False) - self.mock_publish.async_publish.assert_any_call( - 'temperature-high-topic', 23, 0, False) - - def test_set_target_temperature_low_highpessimistic(self): - """Test setting the low/high target temperature.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['temperature_low_state_topic'] = \ - 'temperature-low-state' - config['climate']['temperature_high_state_topic'] = \ - 'temperature-high-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('target_temp_low') is None - assert state.attributes.get('target_temp_high') is None - self.hass.block_till_done() - common.set_temperature(self.hass, target_temp_low=20, - target_temp_high=23, - entity_id=ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('target_temp_low') is None - assert state.attributes.get('target_temp_high') is None - - fire_mqtt_message(self.hass, 'temperature-low-state', '1701') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('target_temp_low') - assert state.attributes.get('target_temp_high') is None - - fire_mqtt_message(self.hass, 'temperature-high-state', '1703') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('target_temp_low') - assert 1703 == state.attributes.get('target_temp_high') - - fire_mqtt_message(self.hass, 'temperature-low-state', 'not a number') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('target_temp_low') - - fire_mqtt_message(self.hass, 'temperature-high-state', 'not a number') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1703 == state.attributes.get('target_temp_high') - - def test_receive_mqtt_temperature(self): - """Test getting the current temperature via MQTT.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['current_temperature_topic'] = 'current_temperature' - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - fire_mqtt_message(self.hass, 'current_temperature', '47') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 47 == state.attributes.get('current_temperature') - - def test_set_away_mode_pessimistic(self): - """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['away_mode_state_topic'] = 'away-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - - common.set_away_mode(self.hass, True, ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - - fire_mqtt_message(self.hass, 'away-state', 'ON') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') - - fire_mqtt_message(self.hass, 'away-state', 'OFF') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - - fire_mqtt_message(self.hass, 'away-state', 'nonsense') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - - def test_set_away_mode(self): - """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['payload_on'] = 'AN' - config['climate']['payload_off'] = 'AUS' - - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - common.set_away_mode(self.hass, True, ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'away-mode-topic', 'AN', 0, False) - self.mock_publish.async_publish.reset_mock() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') - - common.set_away_mode(self.hass, False, ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'away-mode-topic', 'AUS', 0, False) - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - - def test_set_hold_pessimistic(self): - """Test setting the hold mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['hold_state_topic'] = 'hold-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('hold_mode') is None - - common.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('hold_mode') is None - - fire_mqtt_message(self.hass, 'hold-state', 'on') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('hold_mode') - - fire_mqtt_message(self.hass, 'hold-state', 'off') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('hold_mode') - - def test_set_hold(self): - """Test setting the hold mode.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('hold_mode') is None - common.set_hold_mode(self.hass, 'on', ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'hold-topic', 'on', 0, False) - self.mock_publish.async_publish.reset_mock() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('hold_mode') - - common.set_hold_mode(self.hass, 'off', ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'hold-topic', 'off', 0, False) - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('hold_mode') - - def test_set_aux_pessimistic(self): - """Test setting of the aux heating in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['aux_state_topic'] = 'aux-state' - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - - common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - - fire_mqtt_message(self.hass, 'aux-state', 'ON') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('aux_heat') - - fire_mqtt_message(self.hass, 'aux-state', 'OFF') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - - fire_mqtt_message(self.hass, 'aux-state', 'nonsense') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - - def test_set_aux(self): - """Test setting of the aux heating.""" - assert setup_component(self.hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - common.set_aux_heat(self.hass, True, ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'aux-topic', 'ON', 0, False) - self.mock_publish.async_publish.reset_mock() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('aux_heat') - - common.set_aux_heat(self.hass, False, ENTITY_CLIMATE) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'aux-topic', 'OFF', 0, False) - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['availability_topic'] = 'availability-topic' - config['climate']['payload_available'] = 'good' - config['climate']['payload_not_available'] = 'nogood' - - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get('climate.test') - assert STATE_UNAVAILABLE == state.state - - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - - state = self.hass.states.get('climate.test') - assert STATE_UNAVAILABLE != state.state - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('climate.test') - assert STATE_UNAVAILABLE == state.state - - def test_set_with_templates(self): - """Test setting of new fan mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_CONFIG) - # By default, just unquote the JSON-strings - config['climate']['value_template'] = '{{ value_json }}' - # Something more complicated for hold mode - config['climate']['hold_state_template'] = \ - '{{ value_json.attribute }}' - # Rendering to a bool for aux heat - config['climate']['aux_state_template'] = \ - "{{ value == 'switchmeon' }}" - - config['climate']['mode_state_topic'] = 'mode-state' - config['climate']['fan_mode_state_topic'] = 'fan-state' - config['climate']['swing_mode_state_topic'] = 'swing-state' - config['climate']['temperature_state_topic'] = 'temperature-state' - config['climate']['away_mode_state_topic'] = 'away-state' - config['climate']['hold_state_topic'] = 'hold-state' - config['climate']['aux_state_topic'] = 'aux-state' - config['climate']['current_temperature_topic'] = 'current-temperature' - - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - # Operation Mode - state = self.hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get('operation_mode') is None - fire_mqtt_message(self.hass, 'mode-state', '"cool"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - - # Fan Mode - assert state.attributes.get('fan_mode') is None - fire_mqtt_message(self.hass, 'fan-state', '"high"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') - - # Swing Mode - assert state.attributes.get('swing_mode') is None - fire_mqtt_message(self.hass, 'swing-state', '"on"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') - - # Temperature - with valid value - assert state.attributes.get('temperature') is None - fire_mqtt_message(self.hass, 'temperature-state', '"1031"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 1031 == state.attributes.get('temperature') - - # Temperature - with invalid value - with self.assertLogs(level='ERROR') as log: - fire_mqtt_message(self.hass, 'temperature-state', '"-INVALID-"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - # make sure, the invalid value gets logged... - assert len(log.output) == 1 - assert len(log.records) == 1 - assert "Could not parse temperature from -INVALID-" in \ - log.output[0] - # ... but the actual value stays unchanged. - assert 1031 == state.attributes.get('temperature') - - # Away Mode - assert 'off' == state.attributes.get('away_mode') - fire_mqtt_message(self.hass, 'away-state', '"ON"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') - - # Away Mode with JSON values - fire_mqtt_message(self.hass, 'away-state', 'false') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') - - fire_mqtt_message(self.hass, 'away-state', 'true') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') - - # Hold Mode - assert state.attributes.get('hold_mode') is None - fire_mqtt_message(self.hass, 'hold-state', """ - { "attribute": "somemode" } - """) - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'somemode' == state.attributes.get('hold_mode') - - # Aux mode - assert 'off' == state.attributes.get('aux_heat') - fire_mqtt_message(self.hass, 'aux-state', 'switchmeon') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('aux_heat') - - # anything other than 'switchmeon' should turn Aux mode off - fire_mqtt_message(self.hass, 'aux-state', 'somerandomstring') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') - - # Current temperature - fire_mqtt_message(self.hass, 'current-temperature', '"74656"') - self.hass.block_till_done() - state = self.hass.states.get(ENTITY_CLIMATE) - assert 74656 == state.attributes.get('current_temperature') - - def test_min_temp_custom(self): - """Test a custom min temp.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['min_temp'] = 26 - - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - min_temp = state.attributes.get('min_temp') - - assert isinstance(min_temp, float) - assert 26 == state.attributes.get('min_temp') - - def test_max_temp_custom(self): - """Test a custom max temp.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['max_temp'] = 60 - - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - max_temp = state.attributes.get('max_temp') - - assert isinstance(max_temp, float) - assert 60 == max_temp - - def test_temp_step_custom(self): - """Test a custom temp step.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config['climate']['temp_step'] = 0.01 - - assert setup_component(self.hass, CLIMATE_DOMAIN, config) - - state = self.hass.states.get(ENTITY_CLIMATE) - temp_step = state.attributes.get('target_temp_step') - - assert isinstance(temp_step, float) - assert 0.01 == temp_step +async def test_setup_params(hass, mqtt_mock): + """Test the initial parameters.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert 21 == state.attributes.get('temperature') + assert "low" == state.attributes.get('fan_mode') + assert "off" == state.attributes.get('swing_mode') + assert "off" == state.attributes.get('operation_mode') + assert DEFAULT_MIN_TEMP == state.attributes.get('min_temp') + assert DEFAULT_MAX_TEMP == state.attributes.get('max_temp') + + +async def test_supported_features(hass, mqtt_mock): + """Test the supported_features.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + support = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | + SUPPORT_SWING_MODE | SUPPORT_FAN_MODE | SUPPORT_AWAY_MODE | + SUPPORT_HOLD_MODE | SUPPORT_AUX_HEAT | + SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH) + + assert state.attributes.get("supported_features") == support + + +async def test_get_operation_modes(hass, mqtt_mock): + """Test that the operation list returns the correct modes.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + modes = state.attributes.get('operation_list') + assert [ + STATE_AUTO, STATE_OFF, STATE_COOL, + STATE_HEAT, STATE_DRY, STATE_FAN_ONLY + ] == modes + + +async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): + """Test setting operation mode without required attribute. + + Also check the state. + """ + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('operation_mode') + assert "off" == state.state + common.async_set_operation_mode(hass, None, ENTITY_CLIMATE) + await hass.async_block_till_done() + assert ("string value is None for dictionary value @ " + "data['operation_mode']")\ + in caplog.text + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('operation_mode') + assert "off" == state.state + + +async def test_set_operation(hass, mqtt_mock): + """Test setting of new operation mode.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('operation_mode') + assert "off" == state.state + common.async_set_operation_mode(hass, "cool", ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert "cool" == state.attributes.get('operation_mode') + assert "cool" == state.state + mqtt_mock.async_publish.assert_called_once_with( + 'mode-topic', 'cool', 0, False) + + +async def test_set_operation_pessimistic(hass, mqtt_mock): + """Test setting operation mode in pessimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['mode_state_topic'] = 'mode-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('operation_mode') is None + assert "unknown" == state.state + + common.async_set_operation_mode(hass, "cool", ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('operation_mode') is None + assert "unknown" == state.state + + async_fire_mqtt_message(hass, 'mode-state', 'cool') + state = hass.states.get(ENTITY_CLIMATE) + assert "cool" == state.attributes.get('operation_mode') + assert "cool" == state.state + + async_fire_mqtt_message(hass, 'mode-state', 'bogus mode') + state = hass.states.get(ENTITY_CLIMATE) + assert "cool" == state.attributes.get('operation_mode') + assert "cool" == state.state + + +async def test_set_operation_with_power_command(hass, mqtt_mock): + """Test setting of new operation mode with power command enabled.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['power_command_topic'] = 'power-command' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('operation_mode') + assert "off" == state.state + common.async_set_operation_mode(hass, "on", ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert "on" == state.attributes.get('operation_mode') + assert "on" == state.state + mqtt_mock.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'ON', 0, False), + unittest.mock.call('mode-topic', 'on', 0, False) + ]) + mqtt_mock.async_publish.reset_mock() + + common.async_set_operation_mode(hass, "off", ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('operation_mode') + assert "off" == state.state + mqtt_mock.async_publish.assert_has_calls([ + unittest.mock.call('power-command', 'OFF', 0, False), + unittest.mock.call('mode-topic', 'off', 0, False) + ]) + mqtt_mock.async_publish.reset_mock() + + +async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): + """Test setting fan mode without required attribute.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert "low" == state.attributes.get('fan_mode') + common.async_set_fan_mode(hass, None, ENTITY_CLIMATE) + await hass.async_block_till_done() + assert "string value is None for dictionary value @ data['fan_mode']"\ + in caplog.text + state = hass.states.get(ENTITY_CLIMATE) + assert "low" == state.attributes.get('fan_mode') + + +async def test_set_fan_mode_pessimistic(hass, mqtt_mock): + """Test setting of new fan mode in pessimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['fan_mode_state_topic'] = 'fan-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('fan_mode') is None + + common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('fan_mode') is None + + async_fire_mqtt_message(hass, 'fan-state', 'high') + state = hass.states.get(ENTITY_CLIMATE) + assert 'high' == state.attributes.get('fan_mode') + + async_fire_mqtt_message(hass, 'fan-state', 'bogus mode') + state = hass.states.get(ENTITY_CLIMATE) + assert 'high' == state.attributes.get('fan_mode') + + +async def test_set_fan_mode(hass, mqtt_mock): + """Test setting of new fan mode.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert "low" == state.attributes.get('fan_mode') + common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'fan-mode-topic', 'high', 0, False) + state = hass.states.get(ENTITY_CLIMATE) + assert 'high' == state.attributes.get('fan_mode') + + +async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): + """Test setting swing mode without required attribute.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('swing_mode') + common.async_set_swing_mode(hass, None, ENTITY_CLIMATE) + await hass.async_block_till_done() + assert "string value is None for dictionary value @ data['swing_mode']"\ + in caplog.text + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('swing_mode') + + +async def test_set_swing_pessimistic(hass, mqtt_mock): + """Test setting swing mode in pessimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['swing_mode_state_topic'] = 'swing-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('swing_mode') is None + + common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('swing_mode') is None + + async_fire_mqtt_message(hass, 'swing-state', 'on') + state = hass.states.get(ENTITY_CLIMATE) + assert "on" == state.attributes.get('swing_mode') + + async_fire_mqtt_message(hass, 'swing-state', 'bogus state') + state = hass.states.get(ENTITY_CLIMATE) + assert "on" == state.attributes.get('swing_mode') + + +async def test_set_swing(hass, mqtt_mock): + """Test setting of new swing mode.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert "off" == state.attributes.get('swing_mode') + common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'swing-mode-topic', 'on', 0, False) + state = hass.states.get(ENTITY_CLIMATE) + assert "on" == state.attributes.get('swing_mode') + + +async def test_set_target_temperature(hass, mqtt_mock): + """Test setting the target temperature.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert 21 == state.attributes.get('temperature') + common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert 'heat' == state.attributes.get('operation_mode') + mqtt_mock.async_publish.assert_called_once_with( + 'mode-topic', 'heat', 0, False) + mqtt_mock.async_publish.reset_mock() + common.async_set_temperature(hass, temperature=47, + entity_id=ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert 47 == state.attributes.get('temperature') + mqtt_mock.async_publish.assert_called_once_with( + 'temperature-topic', 47, 0, False) + + # also test directly supplying the operation mode to set_temperature + mqtt_mock.async_publish.reset_mock() + common.async_set_temperature(hass, temperature=21, + operation_mode="cool", + entity_id=ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert 'cool' == state.attributes.get('operation_mode') + assert 21 == state.attributes.get('temperature') + mqtt_mock.async_publish.assert_has_calls([ + unittest.mock.call('mode-topic', 'cool', 0, False), + unittest.mock.call('temperature-topic', 21, 0, False) + ]) + mqtt_mock.async_publish.reset_mock() + + +async def test_set_target_temperature_pessimistic(hass, mqtt_mock): + """Test setting the target temperature.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['temperature_state_topic'] = 'temperature-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('temperature') is None + common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) + await hass.async_block_till_done() + common.async_set_temperature(hass, temperature=47, + entity_id=ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('temperature') is None + + async_fire_mqtt_message(hass, 'temperature-state', '1701') + state = hass.states.get(ENTITY_CLIMATE) + assert 1701 == state.attributes.get('temperature') + + async_fire_mqtt_message(hass, 'temperature-state', 'not a number') + state = hass.states.get(ENTITY_CLIMATE) + assert 1701 == state.attributes.get('temperature') + + +async def test_set_target_temperature_low_high(hass, mqtt_mock): + """Test setting the low/high target temperature.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + common.async_set_temperature(hass, target_temp_low=20, + target_temp_high=23, + entity_id=ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + print(state.attributes) + assert 20 == state.attributes.get('target_temp_low') + assert 23 == state.attributes.get('target_temp_high') + mqtt_mock.async_publish.assert_any_call( + 'temperature-low-topic', 20, 0, False) + mqtt_mock.async_publish.assert_any_call( + 'temperature-high-topic', 23, 0, False) + + +async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): + """Test setting the low/high target temperature.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['temperature_low_state_topic'] = \ + 'temperature-low-state' + config['climate']['temperature_high_state_topic'] = \ + 'temperature-high-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('target_temp_low') is None + assert state.attributes.get('target_temp_high') is None + common.async_set_temperature(hass, target_temp_low=20, + target_temp_high=23, + entity_id=ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('target_temp_low') is None + assert state.attributes.get('target_temp_high') is None + + async_fire_mqtt_message(hass, 'temperature-low-state', '1701') + state = hass.states.get(ENTITY_CLIMATE) + assert 1701 == state.attributes.get('target_temp_low') + assert state.attributes.get('target_temp_high') is None + + async_fire_mqtt_message(hass, 'temperature-high-state', '1703') + state = hass.states.get(ENTITY_CLIMATE) + assert 1701 == state.attributes.get('target_temp_low') + assert 1703 == state.attributes.get('target_temp_high') + + async_fire_mqtt_message(hass, 'temperature-low-state', 'not a number') + state = hass.states.get(ENTITY_CLIMATE) + assert 1701 == state.attributes.get('target_temp_low') + + async_fire_mqtt_message(hass, 'temperature-high-state', 'not a number') + state = hass.states.get(ENTITY_CLIMATE) + assert 1703 == state.attributes.get('target_temp_high') + + +async def test_receive_mqtt_temperature(hass, mqtt_mock): + """Test getting the current temperature via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['current_temperature_topic'] = 'current_temperature' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + async_fire_mqtt_message(hass, 'current_temperature', '47') + state = hass.states.get(ENTITY_CLIMATE) + assert 47 == state.attributes.get('current_temperature') + + +async def test_set_away_mode_pessimistic(hass, mqtt_mock): + """Test setting of the away mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['away_mode_state_topic'] = 'away-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + + common.async_set_away_mode(hass, True, ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + + async_fire_mqtt_message(hass, 'away-state', 'ON') + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('away_mode') + + async_fire_mqtt_message(hass, 'away-state', 'OFF') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + + async_fire_mqtt_message(hass, 'away-state', 'nonsense') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + + +async def test_set_away_mode(hass, mqtt_mock): + """Test setting of the away mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['payload_on'] = 'AN' + config['climate']['payload_off'] = 'AUS' + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + common.async_set_away_mode(hass, True, ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'away-mode-topic', 'AN', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('away_mode') + + common.async_set_away_mode(hass, False, ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'away-mode-topic', 'AUS', 0, False) + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + + +async def test_set_hold_pessimistic(hass, mqtt_mock): + """Test setting the hold mode in pessimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['hold_state_topic'] = 'hold-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('hold_mode') is None + + common.async_set_hold_mode(hass, 'on', ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('hold_mode') is None + + async_fire_mqtt_message(hass, 'hold-state', 'on') + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('hold_mode') + + async_fire_mqtt_message(hass, 'hold-state', 'off') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('hold_mode') + + +async def test_set_hold(hass, mqtt_mock): + """Test setting the hold mode.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('hold_mode') is None + common.async_set_hold_mode(hass, 'on', ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'hold-topic', 'on', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('hold_mode') + + common.async_set_hold_mode(hass, 'off', ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'hold-topic', 'off', 0, False) + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('hold_mode') + + +async def test_set_aux_pessimistic(hass, mqtt_mock): + """Test setting of the aux heating in pessimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['aux_state_topic'] = 'aux-state' + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + + common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) + await hass.async_block_till_done() + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + + async_fire_mqtt_message(hass, 'aux-state', 'ON') + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('aux_heat') + + async_fire_mqtt_message(hass, 'aux-state', 'OFF') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + + async_fire_mqtt_message(hass, 'aux-state', 'nonsense') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + + +async def test_set_aux(hass, mqtt_mock): + """Test setting of the aux heating.""" + assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) + + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'aux-topic', 'ON', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('aux_heat') + + common.async_set_aux_heat(hass, False, ENTITY_CLIMATE) + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'aux-topic', 'OFF', 0, False) + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['availability_topic'] = 'availability-topic' + config['climate']['payload_available'] = 'good' + config['climate']['payload_not_available'] = 'nogood' + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get('climate.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + + state = hass.states.get('climate.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + + state = hass.states.get('climate.test') + assert STATE_UNAVAILABLE == state.state + + +async def test_set_with_templates(hass, mqtt_mock, caplog): + """Test setting of new fan mode in pessimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG) + # By default, just unquote the JSON-strings + config['climate']['value_template'] = '{{ value_json }}' + # Something more complicated for hold mode + config['climate']['hold_state_template'] = \ + '{{ value_json.attribute }}' + # Rendering to a bool for aux heat + config['climate']['aux_state_template'] = \ + "{{ value == 'switchmeon' }}" + + config['climate']['mode_state_topic'] = 'mode-state' + config['climate']['fan_mode_state_topic'] = 'fan-state' + config['climate']['swing_mode_state_topic'] = 'swing-state' + config['climate']['temperature_state_topic'] = 'temperature-state' + config['climate']['away_mode_state_topic'] = 'away-state' + config['climate']['hold_state_topic'] = 'hold-state' + config['climate']['aux_state_topic'] = 'aux-state' + config['climate']['current_temperature_topic'] = 'current-temperature' + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + # Operation Mode + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get('operation_mode') is None + async_fire_mqtt_message(hass, 'mode-state', '"cool"') + state = hass.states.get(ENTITY_CLIMATE) + assert "cool" == state.attributes.get('operation_mode') + + # Fan Mode + assert state.attributes.get('fan_mode') is None + async_fire_mqtt_message(hass, 'fan-state', '"high"') + state = hass.states.get(ENTITY_CLIMATE) + assert 'high' == state.attributes.get('fan_mode') + + # Swing Mode + assert state.attributes.get('swing_mode') is None + async_fire_mqtt_message(hass, 'swing-state', '"on"') + state = hass.states.get(ENTITY_CLIMATE) + assert "on" == state.attributes.get('swing_mode') + + # Temperature - with valid value + assert state.attributes.get('temperature') is None + async_fire_mqtt_message(hass, 'temperature-state', '"1031"') + state = hass.states.get(ENTITY_CLIMATE) + assert 1031 == state.attributes.get('temperature') + + # Temperature - with invalid value + async_fire_mqtt_message(hass, 'temperature-state', '"-INVALID-"') + state = hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + assert "Could not parse temperature from -INVALID-" in caplog.text + # ... but the actual value stays unchanged. + assert 1031 == state.attributes.get('temperature') + + # Away Mode + assert 'off' == state.attributes.get('away_mode') + async_fire_mqtt_message(hass, 'away-state', '"ON"') + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('away_mode') + + # Away Mode with JSON values + async_fire_mqtt_message(hass, 'away-state', 'false') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('away_mode') + + async_fire_mqtt_message(hass, 'away-state', 'true') + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('away_mode') + + # Hold Mode + assert state.attributes.get('hold_mode') is None + async_fire_mqtt_message(hass, 'hold-state', """ + { "attribute": "somemode" } + """) + state = hass.states.get(ENTITY_CLIMATE) + assert 'somemode' == state.attributes.get('hold_mode') + + # Aux mode + assert 'off' == state.attributes.get('aux_heat') + async_fire_mqtt_message(hass, 'aux-state', 'switchmeon') + state = hass.states.get(ENTITY_CLIMATE) + assert 'on' == state.attributes.get('aux_heat') + + # anything other than 'switchmeon' should turn Aux mode off + async_fire_mqtt_message(hass, 'aux-state', 'somerandomstring') + state = hass.states.get(ENTITY_CLIMATE) + assert 'off' == state.attributes.get('aux_heat') + + # Current temperature + async_fire_mqtt_message(hass, 'current-temperature', '"74656"') + state = hass.states.get(ENTITY_CLIMATE) + assert 74656 == state.attributes.get('current_temperature') + + +async def test_min_temp_custom(hass, mqtt_mock): + """Test a custom min temp.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['min_temp'] = 26 + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + min_temp = state.attributes.get('min_temp') + + assert isinstance(min_temp, float) + assert 26 == state.attributes.get('min_temp') + + +async def test_max_temp_custom(hass, mqtt_mock): + """Test a custom max temp.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['max_temp'] = 60 + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + max_temp = state.attributes.get('max_temp') + + assert isinstance(max_temp, float) + assert 60 == max_temp + + +async def test_temp_step_custom(hass, mqtt_mock): + """Test a custom temp step.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config['climate']['temp_step'] = 0.01 + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + + state = hass.states.get(ENTITY_CLIMATE) + temp_step = state.attributes.get('target_temp_step') + + assert isinstance(temp_step, float) + assert 0.01 == temp_step async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -755,7 +727,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('climate.test') assert '100' == state.attributes.get('val') @@ -774,7 +745,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('climate.test') assert state.attributes.get('val') is None @@ -794,7 +764,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('climate.test') assert state.attributes.get('val') is None @@ -821,8 +790,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.beer') assert '100' == state.attributes.get('val') @@ -830,19 +797,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.beer') assert '75' == state.attributes.get('val') @@ -866,7 +828,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1 @@ -886,7 +847,6 @@ async def test_discovery_removal_climate(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.beer') assert state is None @@ -912,7 +872,6 @@ async def test_discovery_update_climate(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.beer') assert state is not None @@ -946,7 +905,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.milk') assert state is not None @@ -980,7 +938,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1021,7 +978,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1032,7 +988,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1062,7 +1017,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('climate.beer', new_entity_id='climate.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('climate.beer') assert state is None From 416af5cf57297b7fd5e327095870215ba0fd10e9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:10:48 +0200 Subject: [PATCH 054/346] Drop unnecessary block_till_done (#23250) --- tests/components/mqtt/test_light.py | 84 -------------------- tests/components/mqtt/test_light_json.py | 47 ----------- tests/components/mqtt/test_light_template.py | 45 ----------- 3 files changed, 176 deletions(-) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index cfb0d75d1c7..7b0157aeb7e 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -203,7 +203,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.attributes.get('xy_color') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -255,7 +254,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'test_light_rgb/status', '1') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -268,61 +266,37 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert (0.323, 0.329) == state.attributes.get('xy_color') async_fire_mqtt_message(hass, 'test_light_rgb/status', '0') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_OFF == state.state async_fire_mqtt_message(hass, 'test_light_rgb/status', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() async_fire_mqtt_message(hass, 'test_light_rgb/brightness/status', '100') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 100 == \ light_state.attributes['brightness'] async_fire_mqtt_message(hass, 'test_light_rgb/color_temp/status', '300') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 300 == light_state.attributes['color_temp'] async_fire_mqtt_message(hass, 'test_light_rgb/effect/status', 'rainbow') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 'rainbow' == light_state.attributes['effect'] async_fire_mqtt_message(hass, 'test_light_rgb/white_value/status', '100') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 100 == \ light_state.attributes['white_value'] async_fire_mqtt_message(hass, 'test_light_rgb/status', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() async_fire_mqtt_message(hass, 'test_light_rgb/rgb/status', '125,125,125') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (255, 255, 255) == \ @@ -330,8 +304,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb/hs/status', '200,50') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (200, 50) == \ @@ -339,8 +311,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb/xy/status', '0.675,0.322') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (0.672, 0.324) == \ @@ -371,31 +341,21 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'test_scale/status', 'on') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state assert 255 == state.attributes.get('brightness') async_fire_mqtt_message(hass, 'test_scale/status', 'off') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_OFF == state.state async_fire_mqtt_message(hass, 'test_scale/status', 'on') - await hass.async_block_till_done() - await hass.async_block_till_done() async_fire_mqtt_message(hass, 'test_scale/brightness/status', '99') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 255 == \ light_state.attributes['brightness'] @@ -424,14 +384,11 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_scale_rgb/status', 'on') async_fire_mqtt_message(hass, 'test_scale_rgb/rgb/status', '255,0,0') - await hass.async_block_till_done() state = hass.states.get('light.test') assert 255 == state.attributes.get('brightness') async_fire_mqtt_message(hass, 'test_scale_rgb/rgb/status', '127,0,0') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert 127 == state.attributes.get('brightness') @@ -461,27 +418,21 @@ async def test_white_value_controlling_scale(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'test_scale/status', 'on') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state assert 255 == state.attributes.get('white_value') async_fire_mqtt_message(hass, 'test_scale/status', 'off') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_OFF == state.state async_fire_mqtt_message(hass, 'test_scale/status', 'on') - await hass.async_block_till_done() async_fire_mqtt_message(hass, 'test_scale/white_value/status', '99') - await hass.async_block_till_done() light_state = hass.states.get('light.test') - await hass.async_block_till_done() assert 255 == \ light_state.attributes['white_value'] @@ -536,7 +487,6 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): '{"hello": "rainbow"}') async_fire_mqtt_message(hass, 'test_light_rgb/white_value/status', '{"hello": "75"}') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -548,16 +498,12 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb/hs/status', '{"hello": [100,50]}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert (100, 50) == state.attributes.get('hs_color') async_fire_mqtt_message(hass, 'test_light_rgb/xy/status', '{"hello": [0.123,0.123]}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert (0.14, 0.131) == state.attributes.get('xy_color') @@ -726,7 +672,6 @@ async def test_show_brightness_if_only_command_topic(hass, mqtt_mock): assert state.attributes.get('brightness') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -750,7 +695,6 @@ async def test_show_color_temp_only_if_command_topic(hass, mqtt_mock): assert state.attributes.get('color_temp') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -774,7 +718,6 @@ async def test_show_effect_only_if_command_topic(hass, mqtt_mock): assert state.attributes.get('effect') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -798,7 +741,6 @@ async def test_show_hs_if_only_command_topic(hass, mqtt_mock): assert state.attributes.get('hs_color') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -822,7 +764,6 @@ async def test_show_white_value_if_only_command_topic(hass, mqtt_mock): assert state.attributes.get('white_value') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -846,7 +787,6 @@ async def test_show_xy_if_only_command_topic(hass, mqtt_mock): assert state.attributes.get('xy_color') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -1025,14 +965,11 @@ async def test_default_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability-topic', 'online') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE == state.state @@ -1057,14 +994,11 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability-topic', 'good') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE == state.state @@ -1082,7 +1016,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('light.test') assert '100' == state.attributes.get('val') @@ -1100,7 +1033,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('light.test') assert state.attributes.get('val') is None @@ -1119,7 +1051,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert state.attributes.get('val') is None @@ -1144,8 +1075,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '100' == state.attributes.get('val') @@ -1153,19 +1082,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '75' == state.attributes.get('val') @@ -1189,7 +1113,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 @@ -1215,7 +1138,6 @@ async def test_discovery_removal_light(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is None @@ -1265,7 +1187,6 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is not None @@ -1298,7 +1219,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.milk') assert state is not None @@ -1334,7 +1254,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1375,7 +1294,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1386,7 +1304,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1417,7 +1334,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('light.beer', new_entity_id='light.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 1e325cec5ab..e4f2a3b7ef8 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -154,7 +154,6 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( assert state.attributes.get('hs_color') is None async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON"}') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -207,7 +206,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): '"color_temp":155,' '"effect":"colorloop",' '"white_value":150}') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -221,16 +219,12 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): # Turn the light off async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"OFF"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_OFF == state.state async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "brightness":100}') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') @@ -240,7 +234,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", ' '"color":{"r":125,"g":125,"b":125}}') - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (255, 255, 255) == \ @@ -248,8 +241,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "color":{"x":0.135,"y":0.135}}') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (0.141, 0.14) == \ @@ -257,8 +248,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "color":{"h":180,"s":50}}') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (180.0, 50.0) == \ @@ -266,24 +255,18 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "color_temp":155}') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 155 == light_state.attributes.get('color_temp') async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "effect":"colorloop"}') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 'colorloop' == light_state.attributes.get('effect') async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "white_value":155}') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 155 == light_state.attributes.get('white_value') @@ -665,7 +648,6 @@ async def test_brightness_scale(hass, mqtt_mock): # Turn on the light async_fire_mqtt_message(hass, 'test_light_bright_scale', '{"state":"ON"}') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -674,7 +656,6 @@ async def test_brightness_scale(hass, mqtt_mock): # Turn on the light with brightness async_fire_mqtt_message(hass, 'test_light_bright_scale', '{"state":"ON", "brightness": 99}') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -711,7 +692,6 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): '"color":{"r":255,"g":255,"b":255},' '"brightness": 255,' '"white_value": 255}') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -723,7 +703,6 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON",' '"color":{"r":"bad","g":"val","b":"test"}}') - await hass.async_block_till_done() # Color should not have changed state = hass.states.get('light.test') @@ -734,7 +713,6 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON",' '"brightness": "badValue"}') - await hass.async_block_till_done() # Brightness should not have changed state = hass.states.get('light.test') @@ -745,7 +723,6 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON",' '"white_value": "badValue"}') - await hass.async_block_till_done() # White value should not have changed state = hass.states.get('light.test') @@ -770,14 +747,11 @@ async def test_default_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability-topic', 'online') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE == state.state @@ -802,14 +776,11 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability-topic', 'good') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE == state.state @@ -828,7 +799,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('light.test') assert '100' == state.attributes.get('val') @@ -847,7 +817,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('light.test') assert state.attributes.get('val') is None @@ -867,7 +836,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert state.attributes.get('val') is None @@ -894,8 +862,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '100' == state.attributes.get('val') @@ -903,19 +869,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '75' == state.attributes.get('val') @@ -941,7 +902,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 @@ -963,7 +923,6 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is None @@ -1014,7 +973,6 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is not None @@ -1048,7 +1006,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.milk') assert state is not None @@ -1085,7 +1042,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1127,7 +1083,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1138,7 +1093,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1170,7 +1124,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('light.beer', new_entity_id='light.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is None diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 2db2bd06aa2..658357b8063 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -120,7 +120,6 @@ async def test_state_change_via_topic(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'test_light_rgb', 'on') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -177,7 +176,6 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( # turn on the light, full white async_fire_mqtt_message(hass, 'test_light_rgb', 'on,255,145,123,255-128-64,') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -189,32 +187,24 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( # turn the light off async_fire_mqtt_message(hass, 'test_light_rgb', 'off') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_OFF == state.state # lower the brightness async_fire_mqtt_message(hass, 'test_light_rgb', 'on,100') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 100 == light_state.attributes['brightness'] # change the color temp async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,195') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 195 == light_state.attributes['color_temp'] # change the color async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,,41-42-43') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert (243, 249, 255) == \ @@ -222,8 +212,6 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( # change the white value async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,134') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 134 == light_state.attributes['white_value'] @@ -231,8 +219,6 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( # change the effect async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,,41-42-43,rainbow') - await hass.async_block_till_done() - await hass.async_block_till_done() light_state = hass.states.get('light.test') assert 'rainbow' == light_state.attributes.get('effect') @@ -361,7 +347,6 @@ async def test_invalid_values(hass, mqtt_mock): # turn on the light, full white async_fire_mqtt_message(hass, 'test_light_rgb', 'on,255,215,222,255-255-255,rainbow') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_ON == state.state @@ -373,7 +358,6 @@ async def test_invalid_values(hass, mqtt_mock): # bad state value async_fire_mqtt_message(hass, 'test_light_rgb', 'offf') - await hass.async_block_till_done() # state should not have changed state = hass.states.get('light.test') @@ -381,7 +365,6 @@ async def test_invalid_values(hass, mqtt_mock): # bad brightness values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,off,255-255-255') - await hass.async_block_till_done() # brightness should not have changed state = hass.states.get('light.test') @@ -389,7 +372,6 @@ async def test_invalid_values(hass, mqtt_mock): # bad color temp values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,off,255-255-255') - await hass.async_block_till_done() # color temp should not have changed state = hass.states.get('light.test') @@ -397,7 +379,6 @@ async def test_invalid_values(hass, mqtt_mock): # bad color values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,255,a-b-c') - await hass.async_block_till_done() # color should not have changed state = hass.states.get('light.test') @@ -405,7 +386,6 @@ async def test_invalid_values(hass, mqtt_mock): # bad white value values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,off,255-255-255') - await hass.async_block_till_done() # white value should not have changed state = hass.states.get('light.test') @@ -413,7 +393,6 @@ async def test_invalid_values(hass, mqtt_mock): # bad effect value async_fire_mqtt_message(hass, 'test_light_rgb', 'on,255,a-b-c,white') - await hass.async_block_till_done() # effect should not have changed state = hass.states.get('light.test') @@ -438,14 +417,11 @@ async def test_default_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability-topic', 'online') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE == state.state @@ -471,14 +447,11 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability-topic', 'good') - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.test') assert STATE_UNAVAILABLE == state.state @@ -499,7 +472,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('light.test') assert '100' == state.attributes.get('val') @@ -520,7 +492,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('light.test') assert state.attributes.get('val') is None @@ -542,7 +513,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('light.test') assert state.attributes.get('val') is None @@ -573,8 +543,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '100' == state.attributes.get('val') @@ -582,19 +550,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert '75' == state.attributes.get('val') @@ -622,7 +585,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(light.DOMAIN)) == 1 @@ -646,7 +608,6 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is None @@ -703,7 +664,6 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is not None @@ -739,7 +699,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.milk') assert state is not None @@ -778,7 +737,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -822,7 +780,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -833,7 +790,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -867,7 +823,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('light.beer', new_entity_id='light.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('light.beer') assert state is None From 0533f56fe3a1175f073ea899f5be35d277161894 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 19 Apr 2019 14:50:21 -0700 Subject: [PATCH 055/346] Ask users for a pin when interacting with locks/garage doors (#23223) * Ask users for a pin when interacting with locks/garage doors * Deprecate allow_unlock option --- homeassistant/components/cloud/client.py | 2 +- homeassistant/components/cloud/const.py | 2 +- homeassistant/components/cloud/http_api.py | 21 +- homeassistant/components/cloud/prefs.py | 12 +- .../components/google_assistant/__init__.py | 27 +-- .../components/google_assistant/const.py | 15 +- .../components/google_assistant/error.py | 29 +++ .../components/google_assistant/helpers.py | 13 +- .../components/google_assistant/http.py | 21 +- .../components/google_assistant/smart_home.py | 6 +- .../components/google_assistant/trait.py | 66 ++++-- tests/components/cloud/__init__.py | 2 +- tests/components/cloud/test_http_api.py | 9 +- .../google_assistant/test_smart_home.py | 3 - .../components/google_assistant/test_trait.py | 200 +++++++++++++----- 15 files changed, 290 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 9e24b619460..aedd71bd9ac 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -102,7 +102,7 @@ class CloudClient(Interface): self._google_config = ga_h.Config( should_expose=should_expose, - allow_unlock=self._prefs.google_allow_unlock, + secure_devices_pin=self._prefs.google_secure_devices_pin, entity_config=google_conf.get(CONF_ENTITY_CONFIG), ) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 1286832c0c7..5002286edb9 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -5,7 +5,7 @@ REQUEST_TIMEOUT = 10 PREF_ENABLE_ALEXA = 'alexa_enabled' PREF_ENABLE_GOOGLE = 'google_enabled' PREF_ENABLE_REMOTE = 'remote_enabled' -PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock' +PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin' PREF_CLOUDHOOKS = 'cloudhooks' PREF_CLOUD_USER = 'cloud_user' diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 6ab7d911d47..bf9b7833527 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -19,7 +19,7 @@ from homeassistant.components.google_assistant import ( from .const import ( DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, - PREF_GOOGLE_ALLOW_UNLOCK, InvalidTrustedNetworks) + PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks) _LOGGER = logging.getLogger(__name__) @@ -30,15 +30,6 @@ SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ }) -WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs' -SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ - vol.Required('type'): WS_TYPE_UPDATE_PREFS, - vol.Optional(PREF_ENABLE_GOOGLE): bool, - vol.Optional(PREF_ENABLE_ALEXA): bool, - vol.Optional(PREF_GOOGLE_ALLOW_UNLOCK): bool, -}) - - WS_TYPE_SUBSCRIPTION = 'cloud/subscription' SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({ vol.Required('type'): WS_TYPE_SUBSCRIPTION, @@ -77,9 +68,7 @@ async def async_setup(hass): SCHEMA_WS_SUBSCRIPTION ) hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE_PREFS, websocket_update_prefs, - SCHEMA_WS_UPDATE_PREFS - ) + websocket_update_prefs) hass.components.websocket_api.async_register_command( WS_TYPE_HOOK_CREATE, websocket_hook_create, SCHEMA_WS_HOOK_CREATE @@ -358,6 +347,12 @@ async def websocket_subscription(hass, connection, msg): @_require_cloud_login @websocket_api.async_response +@websocket_api.websocket_command({ + vol.Required('type'): 'cloud/update_prefs', + vol.Optional(PREF_ENABLE_GOOGLE): bool, + vol.Optional(PREF_ENABLE_ALEXA): bool, + vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), +}) async def websocket_update_prefs(hass, connection, msg): """Handle request for account info.""" cloud = hass.data[DOMAIN] diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index b0244f6b1fb..0e2abae15b0 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -3,7 +3,7 @@ from ipaddress import ip_address from .const import ( DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS, PREF_CLOUD_USER, + PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER, InvalidTrustedNetworks) STORAGE_KEY = DOMAIN @@ -29,7 +29,7 @@ class CloudPreferences: PREF_ENABLE_ALEXA: True, PREF_ENABLE_GOOGLE: True, PREF_ENABLE_REMOTE: False, - PREF_GOOGLE_ALLOW_UNLOCK: False, + PREF_GOOGLE_SECURE_DEVICES_PIN: None, PREF_CLOUDHOOKS: {}, PREF_CLOUD_USER: None, } @@ -38,14 +38,14 @@ class CloudPreferences: async def async_update(self, *, google_enabled=_UNDEF, alexa_enabled=_UNDEF, remote_enabled=_UNDEF, - google_allow_unlock=_UNDEF, cloudhooks=_UNDEF, + google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF, cloud_user=_UNDEF): """Update user preferences.""" for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_ALEXA, alexa_enabled), (PREF_ENABLE_REMOTE, remote_enabled), - (PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock), + (PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin), (PREF_CLOUDHOOKS, cloudhooks), (PREF_CLOUD_USER, cloud_user), ): @@ -85,9 +85,9 @@ class CloudPreferences: return self._prefs[PREF_ENABLE_GOOGLE] @property - def google_allow_unlock(self): + def google_secure_devices_pin(self): """Return if Google is allowed to unlock locks.""" - return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False) + return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN) @property def cloudhooks(self): diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 2d3a19afa13..c8078b7d9d2 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -20,7 +20,7 @@ from .const import ( CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY, SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, - DEFAULT_ALLOW_UNLOCK + CONF_SECURE_DEVICES_PIN ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 @@ -35,17 +35,20 @@ ENTITY_SCHEMA = vol.Schema({ vol.Optional(CONF_ROOM_HINT): cv.string, }) -GOOGLE_ASSISTANT_SCHEMA = vol.Schema({ - vol.Required(CONF_PROJECT_ID): cv.string, - vol.Optional(CONF_EXPOSE_BY_DEFAULT, - default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, - vol.Optional(CONF_EXPOSED_DOMAINS, - default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, - vol.Optional(CONF_ALLOW_UNLOCK, - default=DEFAULT_ALLOW_UNLOCK): cv.boolean, -}, extra=vol.PREVENT_EXTRA) +GOOGLE_ASSISTANT_SCHEMA = vol.All( + cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version='0.95'), + vol.Schema({ + vol.Required(CONF_PROJECT_ID): cv.string, + vol.Optional(CONF_EXPOSE_BY_DEFAULT, + default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean, + vol.Optional(CONF_EXPOSED_DOMAINS, + default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list, + vol.Optional(CONF_API_KEY): cv.string, + vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA}, + vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, + # str on purpose, makes sure it is configured correctly. + vol.Optional(CONF_SECURE_DEVICES_PIN): str, + }, extra=vol.PREVENT_EXTRA)) CONFIG_SCHEMA = vol.Schema({ DOMAIN: GOOGLE_ASSISTANT_SCHEMA diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 67c767c080b..07506611109 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -28,13 +28,13 @@ CONF_ALIASES = 'aliases' CONF_API_KEY = 'api_key' CONF_ROOM_HINT = 'room' CONF_ALLOW_UNLOCK = 'allow_unlock' +CONF_SECURE_DEVICES_PIN = 'secure_devices_pin' DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ 'climate', 'cover', 'fan', 'group', 'input_boolean', 'light', 'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock', ] -DEFAULT_ALLOW_UNLOCK = False PREFIX_TYPES = 'action.devices.types.' TYPE_CAMERA = PREFIX_TYPES + 'CAMERA' @@ -55,7 +55,7 @@ HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync' # Error codes used for SmartHomeError class -# https://developers.google.com/actions/smarthome/create-app#error_responses +# https://developers.google.com/actions/reference/smarthome/errors-exceptions ERR_DEVICE_OFFLINE = "deviceOffline" ERR_DEVICE_NOT_FOUND = "deviceNotFound" ERR_VALUE_OUT_OF_RANGE = "valueOutOfRange" @@ -64,6 +64,12 @@ ERR_PROTOCOL_ERROR = 'protocolError' ERR_UNKNOWN_ERROR = 'unknownError' ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported' +ERR_CHALLENGE_NEEDED = 'challengeNeeded' +ERR_CHALLENGE_NOT_SETUP = 'challengeFailedNotSetup' +ERR_TOO_MANY_FAILED_ATTEMPTS = 'tooManyFailedAttempts' +ERR_PIN_INCORRECT = 'pinIncorrect' +ERR_USER_CANCELLED = 'userCancelled' + # Event types EVENT_COMMAND_RECEIVED = 'google_assistant_command' EVENT_QUERY_RECEIVED = 'google_assistant_query' @@ -95,5 +101,8 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, - } + +CHALLENGE_ACK_NEEDED = 'ackNeeded' +CHALLENGE_PIN_NEEDED = 'pinNeeded' +CHALLENGE_FAILED_PIN_NEEDED = 'challengeFailedPinNeeded' diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py index 2225bb58242..3aef1e9408d 100644 --- a/homeassistant/components/google_assistant/error.py +++ b/homeassistant/components/google_assistant/error.py @@ -1,4 +1,5 @@ """Errors for Google Assistant.""" +from .const import ERR_CHALLENGE_NEEDED class SmartHomeError(Exception): @@ -11,3 +12,31 @@ class SmartHomeError(Exception): """Log error code.""" super().__init__(msg) self.code = code + + def to_response(self): + """Convert to a response format.""" + return { + 'errorCode': self.code + } + + +class ChallengeNeeded(SmartHomeError): + """Google Assistant Smart Home errors. + + https://developers.google.com/actions/smarthome/create-app#error_responses + """ + + def __init__(self, challenge_type): + """Initialize challenge needed error.""" + super().__init__(ERR_CHALLENGE_NEEDED, + 'Challenge needed: {}'.format(challenge_type)) + self.challenge_type = challenge_type + + def to_response(self): + """Convert to a response format.""" + return { + 'errorCode': self.code, + 'challengeNeeded': { + 'type': self.challenge_type + } + } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 982b840393e..71cce9de500 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -19,12 +19,12 @@ from .error import SmartHomeError class Config: """Hold the configuration for Google Assistant.""" - def __init__(self, should_expose, allow_unlock, - entity_config=None): + def __init__(self, should_expose, + entity_config=None, secure_devices_pin=None): """Initialize the configuration.""" self.should_expose = should_expose self.entity_config = entity_config or {} - self.allow_unlock = allow_unlock + self.secure_devices_pin = secure_devices_pin class RequestData: @@ -168,15 +168,18 @@ class GoogleEntity: return attrs - async def execute(self, command, data, params): + async def execute(self, data, command_payload): """Execute a command. https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute """ + command = command_payload['command'] + params = command_payload.get('params', {}) + challenge = command_payload.get('challenge', {}) executed = False for trt in self.traits(): if trt.can_execute(command, params): - await trt.execute(command, data, params) + await trt.execute(command, data, params, challenge) executed = True break diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 11d8a384165..d385d742c7d 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,12 +10,12 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, - CONF_ALLOW_UNLOCK, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, CONF_ENTITY_CONFIG, CONF_EXPOSE, - ) + CONF_SECURE_DEVICES_PIN, +) from .smart_home import async_handle_message from .helpers import Config @@ -28,7 +28,7 @@ def async_register_http(hass, cfg): expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT) exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS) entity_config = cfg.get(CONF_ENTITY_CONFIG) or {} - allow_unlock = cfg.get(CONF_ALLOW_UNLOCK, False) + secure_devices_pin = cfg.get(CONF_SECURE_DEVICES_PIN) def is_exposed(entity) -> bool: """Determine if an entity should be exposed to Google Assistant.""" @@ -53,8 +53,13 @@ def async_register_http(hass, cfg): return is_default_exposed or explicit_expose - hass.http.register_view( - GoogleAssistantView(is_exposed, entity_config, allow_unlock)) + config = Config( + should_expose=is_exposed, + entity_config=entity_config, + secure_devices_pin=secure_devices_pin + ) + + hass.http.register_view(GoogleAssistantView(config)) class GoogleAssistantView(HomeAssistantView): @@ -64,11 +69,9 @@ class GoogleAssistantView(HomeAssistantView): name = 'api:google_assistant' requires_auth = True - def __init__(self, is_exposed, entity_config, allow_unlock): + def __init__(self, config): """Initialize the Google Assistant request handler.""" - self.config = Config(is_exposed, - allow_unlock, - entity_config) + self.config = config async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 9edde36f09d..37f35edf645 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -177,14 +177,12 @@ async def handle_devices_execute(hass, data, payload): entities[entity_id] = GoogleEntity(hass, data.config, state) try: - await entities[entity_id].execute(execution['command'], - data, - execution.get('params', {})) + await entities[entity_id].execute(data, execution) except SmartHomeError as err: results[entity_id] = { 'ids': [entity_id], 'status': 'ERROR', - 'errorCode': err.code + **err.to_response() } final_results = list(results.values()) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5bec683ccc7..bad186a4edb 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -19,6 +19,7 @@ from homeassistant.components import ( from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_DEVICE_CLASS, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_LOCKED, @@ -37,8 +38,12 @@ from .const import ( ERR_VALUE_OUT_OF_RANGE, ERR_NOT_SUPPORTED, ERR_FUNCTION_NOT_SUPPORTED, + ERR_CHALLENGE_NOT_SETUP, + CHALLENGE_ACK_NEEDED, + CHALLENGE_PIN_NEEDED, + CHALLENGE_FAILED_PIN_NEEDED, ) -from .error import SmartHomeError +from .error import SmartHomeError, ChallengeNeeded _LOGGER = logging.getLogger(__name__) @@ -114,7 +119,7 @@ class _Trait: """Test if command can be executed.""" return command in self.commands - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a trait command.""" raise NotImplementedError @@ -164,7 +169,7 @@ class BrightnessTrait(_Trait): return response - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a brightness command.""" domain = self.state.domain @@ -219,7 +224,7 @@ class CameraStreamTrait(_Trait): """Return camera stream attributes.""" return self.stream_info or {} - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a get camera stream command.""" url = await self.hass.components.camera.async_request_stream( self.state.entity_id, 'hls') @@ -260,7 +265,7 @@ class OnOffTrait(_Trait): """Return OnOff query attributes.""" return {'on': self.state.state != STATE_OFF} - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute an OnOff command.""" domain = self.state.domain @@ -353,7 +358,7 @@ class ColorSettingTrait(_Trait): return response - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a color temperature command.""" if 'temperature' in params['color']: temp = color_util.color_temperature_kelvin_to_mired( @@ -424,7 +429,7 @@ class SceneTrait(_Trait): """Return scene query attributes.""" return {} - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a scene command.""" # Don't block for scripts as they can be slow. await self.hass.services.async_call( @@ -459,7 +464,7 @@ class DockTrait(_Trait): """Return dock query attributes.""" return {'isDocked': self.state.state == vacuum.STATE_DOCKED} - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a dock command.""" await self.hass.services.async_call( self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, { @@ -498,7 +503,7 @@ class StartStopTrait(_Trait): 'isPaused': self.state.state == vacuum.STATE_PAUSED, } - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: if params['start']: @@ -634,7 +639,7 @@ class TemperatureSettingTrait(_Trait): return response - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute a temperature point or mode command.""" # All sent in temperatures are always in Celsius unit = self.hass.config.units.temperature_unit @@ -748,13 +753,10 @@ class LockUnlockTrait(_Trait): """Return LockUnlock query attributes.""" return {'isLocked': self.state.state == STATE_LOCKED} - def can_execute(self, command, params): - """Test if command can be executed.""" - allowed_unlock = not params['lock'] and self.config.allow_unlock - return params['lock'] or allowed_unlock - - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute an LockUnlock command.""" + _verify_pin_challenge(data, challenge) + if params['lock']: service = lock.SERVICE_LOCK else: @@ -832,7 +834,7 @@ class FanSpeedTrait(_Trait): return response - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute an SetFanSpeed command.""" await self.hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_SPEED, { @@ -1006,7 +1008,7 @@ class ModesTrait(_Trait): return response - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute an SetModes command.""" settings = params.get('updateModeSettings') requested_source = settings.get( @@ -1097,11 +1099,16 @@ class OpenCloseTrait(_Trait): return response - async def execute(self, command, data, params): + async def execute(self, command, data, params, challenge): """Execute an Open, close, Set position command.""" domain = self.state.domain if domain == cover.DOMAIN: + if self.state.attributes.get(ATTR_DEVICE_CLASS) in ( + cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE + ): + _verify_pin_challenge(data, challenge) + position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) if params['openPercent'] == 0: await self.hass.services.async_call( @@ -1123,3 +1130,24 @@ class OpenCloseTrait(_Trait): raise SmartHomeError( ERR_FUNCTION_NOT_SUPPORTED, 'Setting a position is not supported') + + +def _verify_pin_challenge(data, challenge): + """Verify a pin challenge.""" + if not data.config.secure_devices_pin: + raise SmartHomeError( + ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up') + + if not challenge: + raise ChallengeNeeded(CHALLENGE_PIN_NEEDED) + + pin = challenge.get('pin') + + if pin != data.config.secure_devices_pin: + raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED) + + +def _verify_ack_challenge(data, challenge): + """Verify a pin challenge.""" + if not challenge or not challenge.get('ack'): + raise ChallengeNeeded(CHALLENGE_ACK_NEEDED) diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index 3a07e52724f..08ab5324b97 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -26,7 +26,7 @@ def mock_cloud_prefs(hass, prefs={}): prefs_to_set = { const.PREF_ENABLE_ALEXA: True, const.PREF_ENABLE_GOOGLE: True, - const.PREF_GOOGLE_ALLOW_UNLOCK: True, + const.PREF_GOOGLE_SECURE_DEVICES_PIN: None, } prefs_to_set.update(prefs) hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index c147f8492d7..4aebc5679a0 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -9,7 +9,8 @@ from hass_nabucasa.const import STATE_CONNECTED from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.cloud.const import ( - PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_ALLOW_UNLOCK, DOMAIN) + PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN, + DOMAIN) from tests.common import mock_coro @@ -493,21 +494,21 @@ async def test_websocket_update_preferences(hass, hass_ws_client, """Test updating preference.""" assert setup_api[PREF_ENABLE_GOOGLE] assert setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_ALLOW_UNLOCK] + assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None client = await hass_ws_client(hass) await client.send_json({ 'id': 5, 'type': 'cloud/update_prefs', 'alexa_enabled': False, 'google_enabled': False, - 'google_allow_unlock': False, + 'google_secure_devices_pin': '1234', }) response = await client.receive_json() assert response['success'] assert not setup_api[PREF_ENABLE_GOOGLE] assert not setup_api[PREF_ENABLE_ALEXA] - assert not setup_api[PREF_GOOGLE_ALLOW_UNLOCK] + assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == '1234' async def test_enabling_webhook(hass, hass_ws_client, setup_api, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 30a398fccc3..8ea6f26553d 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -22,7 +22,6 @@ from tests.common import (mock_device_registry, mock_registry, BASIC_CONFIG = helpers.Config( should_expose=lambda state: True, - allow_unlock=False ) REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -57,7 +56,6 @@ async def test_sync_message(hass): config = helpers.Config( should_expose=lambda state: state.entity_id != 'light.not_expose', - allow_unlock=False, entity_config={ 'light.demo_light': { const.CONF_ROOM_HINT: 'Living Room', @@ -146,7 +144,6 @@ async def test_sync_in_area(hass, registries): config = helpers.Config( should_expose=lambda _: True, - allow_unlock=False, entity_config={} ) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 12731978f57..8b7f0788f34 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -19,7 +19,8 @@ from homeassistant.components import ( group, ) from homeassistant.components.climate import const as climate -from homeassistant.components.google_assistant import trait, helpers, const +from homeassistant.components.google_assistant import ( + trait, helpers, const, error) from homeassistant.const import ( STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, @@ -30,7 +31,6 @@ from tests.common import async_mock_service, mock_coro BASIC_CONFIG = helpers.Config( should_expose=lambda state: True, - allow_unlock=False ) REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -41,9 +41,15 @@ BASIC_DATA = helpers.RequestData( REQ_ID, ) -UNSAFE_CONFIG = helpers.Config( +PIN_CONFIG = helpers.Config( should_expose=lambda state: True, - allow_unlock=True, + secure_devices_pin='1234' +) + +PIN_DATA = helpers.RequestData( + PIN_CONFIG, + 'test-agent', + REQ_ID, ) @@ -69,7 +75,7 @@ async def test_brightness_light(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await trt.execute( trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA, - {'brightness': 50}) + {'brightness': 50}, {}) await hass.async_block_till_done() assert len(calls) == 1 @@ -108,7 +114,7 @@ async def test_brightness_media_player(hass): hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET) await trt.execute( trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA, - {'brightness': 60}) + {'brightness': 60}, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'media_player.bla', @@ -139,7 +145,7 @@ async def test_camera_stream(hass): with patch('homeassistant.components.camera.async_request_stream', return_value=mock_coro('/api/streams/bla')): - await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}) + await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}, {}) assert trt.query_attributes() == { 'cameraStreamAccessUrl': 'http://1.1.1.1:8123/api/streams/bla' @@ -169,7 +175,7 @@ async def test_onoff_group(hass): on_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_ON) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) + {'on': True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == { ATTR_ENTITY_ID: 'group.bla', @@ -178,7 +184,7 @@ async def test_onoff_group(hass): off_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_OFF) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) + {'on': False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == { ATTR_ENTITY_ID: 'group.bla', @@ -209,7 +215,7 @@ async def test_onoff_input_boolean(hass): on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) + {'on': True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == { ATTR_ENTITY_ID: 'input_boolean.bla', @@ -219,7 +225,7 @@ async def test_onoff_input_boolean(hass): SERVICE_TURN_OFF) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) + {'on': False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == { ATTR_ENTITY_ID: 'input_boolean.bla', @@ -250,7 +256,7 @@ async def test_onoff_switch(hass): on_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_ON) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) + {'on': True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == { ATTR_ENTITY_ID: 'switch.bla', @@ -259,7 +265,7 @@ async def test_onoff_switch(hass): off_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_OFF) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) + {'on': False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == { ATTR_ENTITY_ID: 'switch.bla', @@ -287,7 +293,7 @@ async def test_onoff_fan(hass): on_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_ON) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) + {'on': True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == { ATTR_ENTITY_ID: 'fan.bla', @@ -296,7 +302,7 @@ async def test_onoff_fan(hass): off_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_OFF) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) + {'on': False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == { ATTR_ENTITY_ID: 'fan.bla', @@ -326,7 +332,7 @@ async def test_onoff_light(hass): on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) + {'on': True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == { ATTR_ENTITY_ID: 'light.bla', @@ -335,7 +341,7 @@ async def test_onoff_light(hass): off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) + {'on': False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == { ATTR_ENTITY_ID: 'light.bla', @@ -366,7 +372,7 @@ async def test_onoff_media_player(hass): on_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_ON) await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': True}) + {'on': True}, {}) assert len(on_calls) == 1 assert on_calls[0].data == { ATTR_ENTITY_ID: 'media_player.bla', @@ -377,7 +383,7 @@ async def test_onoff_media_player(hass): await trt_on.execute( trait.COMMAND_ONOFF, BASIC_DATA, - {'on': False}) + {'on': False}, {}) assert len(off_calls) == 1 assert off_calls[0].data == { ATTR_ENTITY_ID: 'media_player.bla', @@ -408,7 +414,7 @@ async def test_dock_vacuum(hass): calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_RETURN_TO_BASE) await trt.execute( - trait.COMMAND_DOCK, BASIC_DATA, {}) + trait.COMMAND_DOCK, BASIC_DATA, {}, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'vacuum.bla', @@ -433,7 +439,7 @@ async def test_startstop_vacuum(hass): start_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_START) - await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': True}) + await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': True}, {}) assert len(start_calls) == 1 assert start_calls[0].data == { ATTR_ENTITY_ID: 'vacuum.bla', @@ -441,7 +447,8 @@ async def test_startstop_vacuum(hass): stop_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_STOP) - await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': False}) + await trt.execute( + trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': False}, {}) assert len(stop_calls) == 1 assert stop_calls[0].data == { ATTR_ENTITY_ID: 'vacuum.bla', @@ -449,7 +456,8 @@ async def test_startstop_vacuum(hass): pause_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_PAUSE) - await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': True}) + await trt.execute( + trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': True}, {}) assert len(pause_calls) == 1 assert pause_calls[0].data == { ATTR_ENTITY_ID: 'vacuum.bla', @@ -457,7 +465,8 @@ async def test_startstop_vacuum(hass): unpause_calls = async_mock_service(hass, vacuum.DOMAIN, vacuum.SERVICE_START) - await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': False}) + await trt.execute( + trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': False}, {}) assert len(unpause_calls) == 1 assert unpause_calls[0].data == { ATTR_ENTITY_ID: 'vacuum.bla', @@ -502,7 +511,7 @@ async def test_color_setting_color_light(hass): 'color': { 'spectrumRGB': 1052927 } - }) + }, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'light.bla', @@ -517,7 +526,7 @@ async def test_color_setting_color_light(hass): 'value': .20, } } - }) + }, {}) assert len(calls) == 2 assert calls[1].data == { ATTR_ENTITY_ID: 'light.bla', @@ -565,14 +574,14 @@ async def test_color_setting_temperature_light(hass): 'color': { 'temperature': 5555 } - }) + }, {}) assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, BASIC_DATA, { 'color': { 'temperature': 2857 } - }) + }, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'light.bla', @@ -608,7 +617,7 @@ async def test_scene_scene(hass): assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON) - await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}) + await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'scene.bla', @@ -626,7 +635,7 @@ async def test_scene_script(hass): assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {}) calls = async_mock_service(hass, script.DOMAIN, SERVICE_TURN_ON) - await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}) + await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {}) # We don't wait till script execution is done. await hass.async_block_till_done() @@ -671,14 +680,14 @@ async def test_temperature_setting_climate_onoff(hass): hass, climate.DOMAIN, SERVICE_TURN_ON) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, { 'thermostatMode': 'on', - }) + }, {}) assert len(calls) == 1 calls = async_mock_service( hass, climate.DOMAIN, SERVICE_TURN_OFF) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, { 'thermostatMode': 'off', - }) + }, {}) assert len(calls) == 1 @@ -731,7 +740,7 @@ async def test_temperature_setting_climate_range(hass): trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, BASIC_DATA, { 'thermostatTemperatureSetpointHigh': 25, 'thermostatTemperatureSetpointLow': 20, - }) + }, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'climate.bla', @@ -743,7 +752,7 @@ async def test_temperature_setting_climate_range(hass): hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, { 'thermostatMode': 'heatcool', - }) + }, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'climate.bla', @@ -753,7 +762,7 @@ async def test_temperature_setting_climate_range(hass): with pytest.raises(helpers.SmartHomeError) as err: await trt.execute( trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA, - {'thermostatTemperatureSetpoint': -100}) + {'thermostatTemperatureSetpoint': -100}, {}) assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE hass.config.units.temperature_unit = TEMP_CELSIUS @@ -799,11 +808,11 @@ async def test_temperature_setting_climate_setpoint(hass): with pytest.raises(helpers.SmartHomeError): await trt.execute( trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA, - {'thermostatTemperatureSetpoint': -100}) + {'thermostatTemperatureSetpoint': -100}, {}) await trt.execute( trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA, - {'thermostatTemperatureSetpoint': 19}) + {'thermostatTemperatureSetpoint': 19}, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'climate.bla', @@ -851,7 +860,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): await trt.execute( trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA, - {'thermostatTemperatureSetpoint': 19}) + {'thermostatTemperatureSetpoint': 19}, {}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'climate.bla', @@ -867,7 +876,7 @@ async def test_lock_unlock_lock(hass): trt = trait.LockUnlockTrait(hass, State('lock.front_door', lock.STATE_UNLOCKED), - BASIC_CONFIG) + PIN_CONFIG) assert trt.sync_attributes() == {} @@ -878,7 +887,26 @@ async def test_lock_unlock_lock(hass): assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': True}) calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK) - await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': True}) + + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': True}, {}) + assert len(calls) == 0 + assert err.code == const.ERR_CHALLENGE_NEEDED + assert err.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': True}, + {'pin': 9999}) + assert len(calls) == 0 + assert err.code == const.ERR_CHALLENGE_NEEDED + assert err.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + await trt.execute(trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': True}, + {'pin': '1234'}) assert len(calls) == 1 assert calls[0].data == { @@ -894,19 +922,7 @@ async def test_lock_unlock_unlock(hass): trt = trait.LockUnlockTrait(hass, State('lock.front_door', lock.STATE_LOCKED), - BASIC_CONFIG) - - assert trt.sync_attributes() == {} - - assert trt.query_attributes() == { - 'isLocked': True - } - - assert not trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False}) - - trt = trait.LockUnlockTrait(hass, - State('lock.front_door', lock.STATE_LOCKED), - UNSAFE_CONFIG) + PIN_CONFIG) assert trt.sync_attributes() == {} @@ -917,7 +933,26 @@ async def test_lock_unlock_unlock(hass): assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False}) calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK) - await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}) + + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': False}, {}) + assert len(calls) == 0 + assert err.code == const.ERR_CHALLENGE_NEEDED + assert err.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': False}, + {'pin': 9999}) + assert len(calls) == 0 + assert err.code == const.ERR_CHALLENGE_NEEDED + assert err.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + await trt.execute( + trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': False}, {'pin': '1234'}) assert len(calls) == 1 assert calls[0].data == { @@ -1000,7 +1035,7 @@ async def test_fan_speed(hass): calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED) await trt.execute( - trait.COMMAND_FANSPEED, BASIC_DATA, {'fanSpeed': 'medium'}) + trait.COMMAND_FANSPEED, BASIC_DATA, {'fanSpeed': 'medium'}, {}) assert len(calls) == 1 assert calls[0].data == { @@ -1089,7 +1124,7 @@ async def test_modes(hass): trait.COMMAND_MODES, BASIC_DATA, { 'updateModeSettings': { trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): 'media' - }}) + }}, {}) assert len(calls) == 1 assert calls[0].data == { @@ -1145,7 +1180,58 @@ async def test_openclose_cover(hass): hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) await trt.execute( trait.COMMAND_OPENCLOSE, BASIC_DATA, - {'openPercent': 50}) + {'openPercent': 50}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'cover.bla', + cover.ATTR_POSITION: 50 + } + + +@pytest.mark.parametrize('device_class', ( + cover.DEVICE_CLASS_DOOR, + cover.DEVICE_CLASS_GARAGE, +)) +async def test_openclose_cover_secure(hass, device_class): + """Test OpenClose trait support for cover domain.""" + assert helpers.get_google_type(cover.DOMAIN, device_class) is not None + assert trait.OpenCloseTrait.supported( + cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class) + + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + ATTR_DEVICE_CLASS: device_class, + cover.ATTR_CURRENT_POSITION: 75 + }), PIN_CONFIG) + + assert trt.sync_attributes() == {} + assert trt.query_attributes() == { + 'openPercent': 75 + } + + calls = async_mock_service( + hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + + # No challenge data + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_OPENCLOSE, PIN_DATA, + {'openPercent': 50}, {}) + assert len(calls) == 0 + assert err.code == const.ERR_CHALLENGE_NEEDED + assert err.challenge_type == const.CHALLENGE_PIN_NEEDED + + # invalid pin + with pytest.raises(error.ChallengeNeeded) as err: + await trt.execute( + trait.COMMAND_OPENCLOSE, PIN_DATA, + {'openPercent': 50}, {'pin': '9999'}) + assert len(calls) == 0 + assert err.code == const.ERR_CHALLENGE_NEEDED + assert err.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED + + await trt.execute( + trait.COMMAND_OPENCLOSE, PIN_DATA, + {'openPercent': 50}, {'pin': '1234'}) assert len(calls) == 1 assert calls[0].data == { ATTR_ENTITY_ID: 'cover.bla', From f584878204e3ad5fe15101f71769349624f24003 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:51:48 +0200 Subject: [PATCH 056/346] Drop unnecessary block_till_done (#23251) --- tests/components/mqtt/test_lock.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index cc629b2165d..56152870cc6 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -32,14 +32,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', 'LOCK') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is STATE_LOCKED async_fire_mqtt_message(hass, 'state-topic', 'UNLOCK') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is STATE_UNLOCKED @@ -63,14 +60,11 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): assert state.state is STATE_UNLOCKED async_fire_mqtt_message(hass, 'state-topic', '{"val":"LOCK"}') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is STATE_LOCKED async_fire_mqtt_message(hass, 'state-topic', '{"val":"UNLOCK"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is STATE_UNLOCKED @@ -170,14 +164,11 @@ async def test_default_availability_payload(hass, mqtt_mock): assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is not STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is STATE_UNAVAILABLE @@ -203,14 +194,11 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is not STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.state is STATE_UNAVAILABLE @@ -228,7 +216,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert '100' == state.attributes.get('val') @@ -246,7 +233,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.attributes.get('val') is None @@ -265,7 +251,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('lock.test') assert state.attributes.get('val') is None @@ -290,8 +275,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.beer') assert '100' == state.attributes.get('val') @@ -299,19 +282,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.beer') assert '75' == state.attributes.get('val') @@ -335,7 +313,6 @@ async def test_unique_id(hass): }] }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(lock.DOMAIN)) == 1 @@ -356,7 +333,6 @@ async def test_discovery_removal_lock(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.beer') assert state is None @@ -384,7 +360,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.milk') assert state is not None @@ -418,7 +393,6 @@ async def test_discovery_update_lock(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.beer') assert state is not None assert state.name == 'Milk' @@ -454,7 +428,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -495,7 +468,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -506,7 +478,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -537,7 +508,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('lock.beer', new_entity_id='lock.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('lock.beer') assert state is None From 73a7d5e6f468b9e2a480fe9b0ead904577bdcc6b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:52:23 +0200 Subject: [PATCH 057/346] Drop unnecessary block_till_done, improve tests (#23252) --- tests/components/mqtt/test_sensor.py | 580 +++++++++++++-------------- 1 file changed, 269 insertions(+), 311 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 45267484211..db8f7620864 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the MQTT sensor platform.""" from datetime import datetime, timedelta import json -import unittest from unittest.mock import ANY, patch from homeassistant.components import mqtt @@ -9,361 +8,340 @@ from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE import homeassistant.core as ha -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import ( - MockConfigEntry, assert_setup_component, async_fire_mqtt_message, - async_mock_mqtt_component, fire_mqtt_message, get_test_home_assistant, - mock_component, mock_mqtt_component, mock_registry) + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + async_fire_time_changed, mock_registry) -class TestSensorMQTT(unittest.TestCase): - """Test the MQTT sensor.""" +async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): + """Test the setting of the value via MQTT.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit' + } + }) - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - mock_mqtt_component(self.hass) + async_fire_mqtt_message(hass, 'test-topic', '100') + state = hass.states.get('sensor.test') - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() + assert '100' == state.state + assert 'fav unit' == \ + state.attributes.get('unit_of_measurement') - def test_setting_sensor_value_via_mqtt_message(self): - """Test the setting of the value via MQTT.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit' - } - }) - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') +async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): + """Test the expiration of the value.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'expire_after': '4', + 'force_update': True + } + }) - assert '100' == state.state - assert 'fav unit' == \ - state.attributes.get('unit_of_measurement') + state = hass.states.get('sensor.test') + assert 'unknown' == state.state - @patch('homeassistant.core.dt_util.utcnow') - def test_setting_sensor_value_expires(self, mock_utcnow): - """Test the expiration of the value.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'expire_after': '4', - 'force_update': True - } - }) + now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) + with patch(('homeassistant.helpers.event.' + 'dt_util.utcnow'), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() - state = self.hass.states.get('sensor.test') - assert 'unknown' == state.state + # Value was set correctly. + state = hass.states.get('sensor.test') + assert '100' == state.state - now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) - mock_utcnow.return_value = now - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() - # Value was set correctly. - state = self.hass.states.get('sensor.test') - assert '100' == state.state + # Value is not yet expired + state = hass.states.get('sensor.test') + assert '100' == state.state - # Time jump +3s - now = now + timedelta(seconds=3) - self._send_time_changed(now) - self.hass.block_till_done() + # Next message resets timer + with patch(('homeassistant.helpers.event.' + 'dt_util.utcnow'), return_value=now): + async_fire_time_changed(hass, now) + async_fire_mqtt_message(hass, 'test-topic', '101') + await hass.async_block_till_done() - # Value is not yet expired - state = self.hass.states.get('sensor.test') - assert '100' == state.state + # Value was updated correctly. + state = hass.states.get('sensor.test') + assert '101' == state.state - # Next message resets timer - mock_utcnow.return_value = now - fire_mqtt_message(self.hass, 'test-topic', '101') - self.hass.block_till_done() + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() - # Value was updated correctly. - state = self.hass.states.get('sensor.test') - assert '101' == state.state + # Value is not yet expired + state = hass.states.get('sensor.test') + assert '101' == state.state - # Time jump +3s - now = now + timedelta(seconds=3) - self._send_time_changed(now) - self.hass.block_till_done() + # Time jump +2s + now = now + timedelta(seconds=2) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() - # Value is not yet expired - state = self.hass.states.get('sensor.test') - assert '101' == state.state + # Value is expired now + state = hass.states.get('sensor.test') + assert 'unknown' == state.state - # Time jump +2s - now = now + timedelta(seconds=2) - self._send_time_changed(now) - self.hass.block_till_done() - # Value is expired now - state = self.hass.states.get('sensor.test') - assert 'unknown' == state.state +async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of the value via MQTT with JSON payload.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'value_template': '{{ value_json.val }}' + } + }) - def test_setting_sensor_value_via_mqtt_json_message(self): - """Test the setting of the value via MQTT with JSON payload.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'value_template': '{{ value_json.val }}' - } - }) + async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') + state = hass.states.get('sensor.test') - fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') + assert '100' == state.state - assert '100' == state.state - def test_force_update_disabled(self): - """Test force update option.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit' - } - }) +async def test_force_update_disabled(hass, mqtt_mock): + """Test force update option.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit' + } + }) - events = [] + events = [] - @ha.callback - def callback(event): - events.append(event) + @ha.callback + def callback(event): + events.append(event) - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 1 == len(events) + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() + assert 1 == len(events) - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 1 == len(events) + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() + assert 1 == len(events) - def test_force_update_enabled(self): - """Test force update option.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'force_update': True - } - }) - events = [] +async def test_force_update_enabled(hass, mqtt_mock): + """Test force update option.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'force_update': True + } + }) - @ha.callback - def callback(event): - events.append(event) + events = [] - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) + @ha.callback + def callback(event): + events.append(event) - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 1 == len(events) + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 2 == len(events) + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() + assert 1 == len(events) - def test_default_availability_payload(self): - """Test availability by default payload with defined topic.""" - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'availability_topic': 'availability-topic' - } - }) + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() + assert 2 == len(events) - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'availability_topic': 'availability-topic' + } + }) - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE != state.state + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'availability-topic', 'online') - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE != state.state - def test_custom_availability_payload(self): - """Test availability by custom payload with defined topic.""" - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - }) + async_fire_mqtt_message(hass, 'availability-topic', 'offline') - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE != state.state +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + async_fire_mqtt_message(hass, 'availability-topic', 'good') - def _send_time_changed(self, now): - """Send a time changed event.""" - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE != state.state - def test_setting_sensor_attribute_via_mqtt_json_message(self): - """Test the setting of attribute via MQTT with JSON payload.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'json_attributes': 'val' - } - }) + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state - assert '100' == \ - state.attributes.get('val') - @patch('homeassistant.components.mqtt.sensor._LOGGER') - def test_update_with_json_attrs_not_dict(self, mock_logger): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'json_attributes': 'val' - } - }) +async def test_setting_sensor_attribute_via_legacy_mqtt_json_message( + hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) - fire_mqtt_message(self.hass, 'test-topic', '[ "list", "of", "things"]') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') + async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') + state = hass.states.get('sensor.test') - assert state.attributes.get('val') is None - assert mock_logger.warning.called + assert '100' == \ + state.attributes.get('val') - @patch('homeassistant.components.mqtt.sensor._LOGGER') - def test_update_with_json_attrs_bad_JSON(self, mock_logger): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'json_attributes': 'val' - } - }) - fire_mqtt_message(self.hass, 'test-topic', 'This is not JSON') - self.hass.block_till_done() +async def test_update_with_legacy_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) - state = self.hass.states.get('sensor.test') - assert state.attributes.get('val') is None - assert mock_logger.warning.called - assert mock_logger.debug.called + async_fire_mqtt_message(hass, 'test-topic', '[ "list", "of", "things"]') + state = hass.states.get('sensor.test') - def test_update_with_json_attrs_and_template(self): - """Test attributes get extracted from a JSON result.""" - mock_component(self.hass, 'mqtt') - assert setup_component(self.hass, sensor.DOMAIN, { - sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'unit_of_measurement': 'fav unit', - 'value_template': '{{ value_json.val }}', - 'json_attributes': 'val' - } - }) + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text - fire_mqtt_message(self.hass, 'test-topic', '{ "val": "100" }') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') - assert '100' == \ - state.attributes.get('val') - assert '100' == state.state +async def test_update_with_legacy_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'json_attributes': 'val' + } + }) - 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' - } - }) + async_fire_mqtt_message(hass, 'test-topic', 'This is not JSON') - 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 = hass.states.get('sensor.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text - 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 + +async def test_update_with_legacy_json_attrs_and_template(hass, mqtt_mock): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'unit_of_measurement': 'fav unit', + 'value_template': '{{ value_json.val }}', + 'json_attributes': 'val' + } + }) + + async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') + state = hass.states.get('sensor.test') + + assert '100' == \ + state.attributes.get('val') + assert '100' == state.state + + +async def test_invalid_device_class(hass, mqtt_mock): + """Test device_class option with invalid value.""" + assert await async_setup_component(hass, sensor.DOMAIN, { + sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'device_class': 'foobarnotreal' + } + }) + + state = hass.states.get('sensor.test') + assert state is None + + +async def test_valid_device_class(hass, mqtt_mock): + """Test device_class option with valid values.""" + assert await async_setup_component(hass, 'sensor', { + 'sensor': [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'device_class': 'temperature' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + }] + }) + await hass.async_block_till_done() + + state = hass.states.get('sensor.test_1') + assert state.attributes['device_class'] == 'temperature' + state = hass.states.get('sensor.test_2') + assert 'device_class' not in state.attributes async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -378,7 +356,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('sensor.test') assert '100' == state.attributes.get('val') @@ -398,7 +375,6 @@ async def test_setting_attribute_with_template(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', json.dumps( {"Timer1": {"Arm": 0, "Time": "22:18"}})) - await hass.async_block_till_done() state = hass.states.get('sensor.test') assert 0 == state.attributes.get('Arm') @@ -417,7 +393,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('sensor.test') assert state.attributes.get('val') is None @@ -436,7 +411,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('sensor.test') assert state.attributes.get('val') is None @@ -461,8 +435,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert '100' == state.attributes.get('val') @@ -470,19 +442,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert '75' == state.attributes.get('val') @@ -505,7 +472,6 @@ async def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 @@ -527,7 +493,6 @@ async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert state is None @@ -555,7 +520,6 @@ async def test_discovery_update_sensor(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert state is not None @@ -589,7 +553,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.milk') assert state is not None @@ -624,7 +587,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -664,7 +626,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -675,7 +636,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -705,7 +665,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('sensor.beer', new_entity_id='sensor.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.beer') assert state is None @@ -743,7 +702,6 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None From 31e514ec157d7f0bf828e43b95eabe2948a81505 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 19 Apr 2019 23:53:58 +0200 Subject: [PATCH 058/346] Add missing services.yaml file for hue (#23217) * Add hue services.yaml * Add lifx services.yaml * Add lutron services.yaml * Update lifx services.yaml * Update hue services.yaml * Revert lifx services.yaml as it is not necessary * Remove hue from lights/services.yaml --- homeassistant/components/hue/services.yaml | 11 +++++++++++ homeassistant/components/light/services.yaml | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml index e69de29bb2d..68eaf6ac377 100644 --- a/homeassistant/components/hue/services.yaml +++ b/homeassistant/components/hue/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available hue services + +hue_activate_scene: + description: Activate a hue scene stored in the hue hub. + fields: + group_name: + description: Name of hue group/room from the hue app. + example: "Living Room" + scene_name: + description: Name of hue scene from the hue app. + example: "Energize" diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index d4985258368..ef944d75efc 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -71,16 +71,6 @@ toggle: '...': description: All turn_on parameters can be used. -hue_activate_scene: - description: Activate a hue scene stored in the hue hub. - fields: - group_name: - description: Name of hue group/room from the hue app. - example: "Living Room" - scene_name: - description: Name of hue scene from the hue app. - example: "Energize" - lifx_set_state: description: Set a color/brightness and possibliy turn the light on/off. fields: From 9d8d8afa8241806b5539ed0555a2f6d6703bdff5 Mon Sep 17 00:00:00 2001 From: Tomer Figenblat Date: Sat, 20 Apr 2019 00:54:48 +0300 Subject: [PATCH 059/346] Added component named switcher_kis switcher water heater integration. (#22325) * Added component named switcher_kis switcher water heater integration. * Fixed conflicts. * Updated requirements. * Added manifest.json file and updated CODEOWNERS. * Fixed requirements_all.txt. * Better component tests. * Removed unnecessary parameter from fixture function. * Removed tests section from mypy.ini. * Remove unused ENTITY_ID_FORMAT. * Stop udp bridge when failed to setup the component. * Replace DISCOVERY_ constants prefix with DATA_. * Various change requests. * Fixed constant name change remifications. * Added explicit name to fixture. * Various change requests. * More various change requests. * Added EventType for homeassistant.core.Event. * Switched from event driven data distribution to dispatcher type plus clean-ups. * Removed name and icon keys from the component configuration. * Various change requests. * Various change reqeusts and clean-ups. * Removed unnecessary DEPENDENCIES constant from swith platform. * Replaced configuration data guard with assert. * Removed unused constants. * Removed confusing type casting for mypy sake. * Refactor property device_name to name. * Removed None guard effecting mypy only. * Removed unnecessary function from switch entity. * Removed None guard in use by mypy only. * Removed unused constant. * Removed unnecessary context manager. * Stopped messing around with mypy.ini. * Referring to typing.TYPE_CHECKING for non-runtime imports. * Added test requierment correctyly. * Replaced queue.get() with queue.get_nowait() to avoid backing up intervals requests. * Revert changes in mypy.ini. * Changed attributes content to device properties instead of entity properties. * Fixed typo in constant name. * Remove unnecessary async keyword from callable. * Waiting for tasks on event loop to end. * Added callback decorator to callable. --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/switcher_kis/__init__.py | 93 ++++++++++++ .../components/switcher_kis/manifest.json | 12 ++ .../components/switcher_kis/switch.py | 142 ++++++++++++++++++ homeassistant/helpers/typing.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/switcher_kis/__init__.py | 1 + tests/components/switcher_kis/conftest.py | 110 ++++++++++++++ tests/components/switcher_kis/consts.py | 26 ++++ tests/components/switcher_kis/test_init.py | 49 ++++++ 13 files changed, 443 insertions(+) create mode 100644 homeassistant/components/switcher_kis/__init__.py create mode 100644 homeassistant/components/switcher_kis/manifest.json create mode 100644 homeassistant/components/switcher_kis/switch.py create mode 100644 tests/components/switcher_kis/__init__.py create mode 100644 tests/components/switcher_kis/conftest.py create mode 100644 tests/components/switcher_kis/consts.py create mode 100644 tests/components/switcher_kis/test_init.py diff --git a/.coveragerc b/.coveragerc index cb0c50f72fe..ac674b9fada 100644 --- a/.coveragerc +++ b/.coveragerc @@ -562,6 +562,7 @@ omit = homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbot/switch.py + homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthru/sensor.py homeassistant/components/synology/camera.py diff --git a/CODEOWNERS b/CODEOWNERS index a6dd61e4ffb..c2cd1f4553a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -206,6 +206,7 @@ homeassistant/components/supla/* @mwegrzynek homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen +homeassistant/components/switcher_kis/* @tomerfi homeassistant/components/switchmate/* @danielhiversen homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py new file mode 100644 index 00000000000..43ca0abc2a0 --- /dev/null +++ b/homeassistant/components/switcher_kis/__init__.py @@ -0,0 +1,93 @@ +"""Home Assistant Switcher Component.""" + +from asyncio import QueueEmpty, TimeoutError as Asyncio_TimeoutError, wait_for +from datetime import datetime, timedelta +from logging import getLogger +from typing import Dict, Optional + +import voluptuous as vol + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import EventType, HomeAssistantType + +_LOGGER = getLogger(__name__) + +DOMAIN = 'switcher_kis' + +CONF_DEVICE_ID = 'device_id' +CONF_DEVICE_PASSWORD = 'device_password' +CONF_PHONE_ID = 'phone_id' + +DATA_DEVICE = 'device' + +SIGNAL_SWITCHER_DEVICE_UPDATE = 'switcher_device_update' + +ATTR_AUTO_OFF_SET = 'auto_off_set' +ATTR_ELECTRIC_CURRENT = 'electric_current' +ATTR_REMAINING_TIME = 'remaining_time' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_PHONE_ID): cv.string, + vol.Required(CONF_DEVICE_ID): cv.string, + vol.Required(CONF_DEVICE_PASSWORD): cv.string + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: + """Set up the switcher component.""" + from aioswitcher.bridge import SwitcherV2Bridge + + phone_id = config[DOMAIN][CONF_PHONE_ID] + device_id = config[DOMAIN][CONF_DEVICE_ID] + device_password = config[DOMAIN][CONF_DEVICE_PASSWORD] + + v2bridge = SwitcherV2Bridge( + hass.loop, phone_id, device_id, device_password) + + await v2bridge.start() + + async def async_stop_bridge(event: EventType) -> None: + """On homeassistant stop, gracefully stop the bridge if running.""" + await v2bridge.stop() + + hass.async_add_job(hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, async_stop_bridge)) + + try: + device_data = await wait_for( + v2bridge.queue.get(), timeout=5.0, loop=hass.loop) + except (Asyncio_TimeoutError, RuntimeError): + _LOGGER.exception("failed to get response from device") + await v2bridge.stop() + return False + + hass.data[DOMAIN] = { + DATA_DEVICE: device_data + } + + hass.async_create_task(async_load_platform( + hass, SWITCH_DOMAIN, DOMAIN, None, config)) + + @callback + def device_updates(timestamp: Optional[datetime]) -> None: + """Use for updating the device data from the queue.""" + if v2bridge.running: + try: + device_new_data = v2bridge.queue.get_nowait() + if device_new_data: + async_dispatcher_send( + hass, SIGNAL_SWITCHER_DEVICE_UPDATE, device_new_data) + except QueueEmpty: + pass + + async_track_time_interval(hass, device_updates, timedelta(seconds=4)) + + return True diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json new file mode 100644 index 00000000000..140caf51936 --- /dev/null +++ b/homeassistant/components/switcher_kis/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "switcher_kis", + "name": "Switcher", + "documentation": "https://www.home-assistant.io/components/switcher_kis/", + "codeowners": [ + "@tomerfi" + ], + "requirements": [ + "aioswitcher==2019.3.21" + ], + "dependencies": [] +} diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py new file mode 100644 index 00000000000..c66c6b52e0c --- /dev/null +++ b/homeassistant/components/switcher_kis/switch.py @@ -0,0 +1,142 @@ +"""Home Assistant Switcher Component Switch platform.""" + +from logging import getLogger +from typing import Callable, Dict, TYPE_CHECKING + +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_AUTO_OFF_SET, ATTR_ELECTRIC_CURRENT, ATTR_REMAINING_TIME, + DATA_DEVICE, DOMAIN, SIGNAL_SWITCHER_DEVICE_UPDATE) + +if TYPE_CHECKING: + from aioswitcher.devices import SwitcherV2Device + from aioswitcher.api.messages import SwitcherV2ControlResponseMSG + + +_LOGGER = getLogger(__name__) + +DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { + 'power_consumption': ATTR_CURRENT_POWER_W, + 'electric_current': ATTR_ELECTRIC_CURRENT, + 'remaining_time': ATTR_REMAINING_TIME, + 'auto_off_set': ATTR_AUTO_OFF_SET +} + + +async def async_setup_platform(hass: HomeAssistantType, config: Dict, + async_add_entities: Callable, + discovery_info: Dict) -> None: + """Set up the switcher platform for the switch component.""" + assert DOMAIN in hass.data + async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])]) + + +class SwitcherControl(SwitchDevice): + """Home Assistant switch entity.""" + + def __init__(self, device_data: 'SwitcherV2Device') -> None: + """Initialize the entity.""" + self._self_initiated = False + self._device_data = device_data + self._state = device_data.state + + @property + def name(self) -> str: + """Return the device's name.""" + return self._device_data.name + + @property + def should_poll(self) -> bool: + """Return False, entity pushes its state to HA.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "{}-{}".format( + self._device_data.device_id, self._device_data.mac_addr) + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + from aioswitcher.consts import STATE_ON as SWITCHER_STATE_ON + return self._state == SWITCHER_STATE_ON + + @property + def current_power_w(self) -> int: + """Return the current power usage in W.""" + return self._device_data.power_consumption + + @property + def device_state_attributes(self) -> Dict: + """Return the optional state attributes.""" + from aioswitcher.consts import WAITING_TEXT + + attribs = {} + + for prop, attr in DEVICE_PROPERTIES_TO_HA_ATTRIBUTES.items(): + value = getattr(self._device_data, prop) + if value and value is not WAITING_TEXT: + attribs[attr] = value + + return attribs + + @property + def available(self) -> bool: + """Return True if entity is available.""" + from aioswitcher.consts import (STATE_OFF as SWITCHER_STATE_OFF, + STATE_ON as SWITCHER_STATE_ON) + return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF] + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + async_dispatcher_connect( + self.hass, SIGNAL_SWITCHER_DEVICE_UPDATE, self.async_update_data) + + async def async_update_data(self, device_data: 'SwitcherV2Device') -> None: + """Update the entity data.""" + if device_data: + if self._self_initiated: + self._self_initiated = False + else: + self._device_data = device_data + self._state = self._device_data.state + self.async_schedule_update_ha_state() + + async def async_turn_on(self, **kwargs: Dict) -> None: + """Turn the entity on. + + This method must be run in the event loop and returns a coroutine. + """ + await self._control_device(True) + + async def async_turn_off(self, **kwargs: Dict) -> None: + """Turn the entity off. + + This method must be run in the event loop and returns a coroutine. + """ + await self._control_device(False) + + async def _control_device(self, send_on: bool) -> None: + """Turn the entity on or off.""" + from aioswitcher.api import SwitcherV2Api + from aioswitcher.consts import (COMMAND_OFF, COMMAND_ON, + STATE_OFF as SWITCHER_STATE_OFF, + STATE_ON as SWITCHER_STATE_ON) + + response = None # type: SwitcherV2ControlResponseMSG + async with SwitcherV2Api( + self.hass.loop, self._device_data.ip_addr, + self._device_data.phone_id, self._device_data.device_id, + self._device_data.device_password) as swapi: + response = await swapi.control_device( + COMMAND_ON if send_on else COMMAND_OFF) + + if response and response.successful: + self._self_initiated = True + self._state = \ + SWITCHER_STATE_ON if send_on else SWITCHER_STATE_OFF + self.async_schedule_update_ha_state() diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 91b49283be8..e9a8d0749b0 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -7,6 +7,7 @@ import homeassistant.core GPSType = Tuple[float, float] ConfigType = Dict[str, Any] +EventType = homeassistant.core.Event HomeAssistantType = homeassistant.core.HomeAssistant ServiceDataType = Dict[str, Any] TemplateVarsType = Optional[Dict[str, Any]] diff --git a/requirements_all.txt b/requirements_all.txt index c1e381e760b..5fa04616a8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -145,6 +145,9 @@ aiolifx_effects==0.2.1 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 +# homeassistant.components.switcher_kis +aioswitcher==2019.3.21 + # homeassistant.components.unifi aiounifi==4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e5012d76e0..383aec75958 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -51,6 +51,9 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==1.9.1 +# homeassistant.components.switcher_kis +aioswitcher==2019.3.21 + # homeassistant.components.unifi aiounifi==4 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f71b8944d7c..63b0ef737e2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -48,6 +48,7 @@ TEST_REQUIREMENTS = ( 'aiohttp_cors', 'aiohue', 'aiounifi', + 'aioswitcher', 'apns2', 'av', 'axis', diff --git a/tests/components/switcher_kis/__init__.py b/tests/components/switcher_kis/__init__.py new file mode 100644 index 00000000000..46fbe073ab0 --- /dev/null +++ b/tests/components/switcher_kis/__init__.py @@ -0,0 +1 @@ +"""Test cases and object for the Switcher integration tests.""" diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py new file mode 100644 index 00000000000..d0398d448e9 --- /dev/null +++ b/tests/components/switcher_kis/conftest.py @@ -0,0 +1,110 @@ +"""Common fixtures and objects for the Switcher integration tests.""" + +from asyncio import Queue +from datetime import datetime +from typing import Any, Generator, Optional + +from asynctest import CoroutineMock, patch +from pytest import fixture + +from .consts import ( + DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME, + DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS, + DUMMY_MAC_ADDRESS, DUMMY_PHONE_ID, DUMMY_POWER_CONSUMPTION, + DUMMY_REMAINING_TIME) + + +@patch('aioswitcher.devices.SwitcherV2Device') +class MockSwitcherV2Device: + """Class for mocking the aioswitcher.devices.SwitcherV2Device object.""" + + def __init__(self) -> None: + """Initialize the object.""" + self._last_state_change = datetime.now() + + @property + def device_id(self) -> str: + """Return the device id.""" + return DUMMY_DEVICE_ID + + @property + def ip_addr(self) -> str: + """Return the ip address.""" + return DUMMY_IP_ADDRESS + + @property + def mac_addr(self) -> str: + """Return the mac address.""" + return DUMMY_MAC_ADDRESS + + @property + def name(self) -> str: + """Return the device name.""" + return DUMMY_DEVICE_NAME + + @property + def state(self) -> str: + """Return the device state.""" + return DUMMY_DEVICE_STATE + + @property + def remaining_time(self) -> Optional[str]: + """Return the time left to auto-off.""" + return DUMMY_REMAINING_TIME + + @property + def auto_off_set(self) -> str: + """Return the auto-off configuration value.""" + return DUMMY_AUTO_OFF_SET + + @property + def power_consumption(self) -> int: + """Return the power consumption in watts.""" + return DUMMY_POWER_CONSUMPTION + + @property + def electric_current(self) -> float: + """Return the power consumption in amps.""" + return DUMMY_ELECTRIC_CURRENT + + @property + def phone_id(self) -> str: + """Return the phone id.""" + return DUMMY_PHONE_ID + + @property + def last_data_update(self) -> datetime: + """Return the timestamp of the last update.""" + return datetime.now() + + @property + def last_state_change(self) -> datetime: + """Return the timestamp of the state change.""" + return self._last_state_change + + +@fixture(name='mock_bridge') +def mock_bridge_fixture() -> Generator[None, Any, None]: + """Fixture for mocking aioswitcher.bridge.SwitcherV2Bridge.""" + queue = Queue() # type: Queue + + async def mock_queue(): + """Mock asyncio's Queue.""" + await queue.put(MockSwitcherV2Device()) + return await queue.get() + + mock_bridge = CoroutineMock() + + patchers = [ + patch('aioswitcher.bridge.SwitcherV2Bridge.start', new=mock_bridge), + patch('aioswitcher.bridge.SwitcherV2Bridge.stop', new=mock_bridge), + patch('aioswitcher.bridge.SwitcherV2Bridge.queue', get=mock_queue) + ] + + for patcher in patchers: + patcher.start() + + yield + + for patcher in patchers: + patcher.stop() diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py new file mode 100644 index 00000000000..47efe8d03c9 --- /dev/null +++ b/tests/components/switcher_kis/consts.py @@ -0,0 +1,26 @@ +"""Constants for the Switcher integration tests.""" + +from homeassistant.components.switcher_kis import ( + CONF_DEVICE_ID, CONF_DEVICE_PASSWORD, CONF_PHONE_ID, DOMAIN) + +DUMMY_AUTO_OFF_SET = '01:30:00' +DUMMY_DEVICE_ID = 'a123bc' +DUMMY_DEVICE_NAME = "Device Name" +DUMMY_DEVICE_PASSWORD = '12345678' +DUMMY_DEVICE_STATE = 'on' +DUMMY_ELECTRIC_CURRENT = 12.8 +DUMMY_ICON = 'mdi:dummy-icon' +DUMMY_IP_ADDRESS = '192.168.100.157' +DUMMY_MAC_ADDRESS = 'A1:B2:C3:45:67:D8' +DUMMY_NAME = 'boiler' +DUMMY_PHONE_ID = '1234' +DUMMY_POWER_CONSUMPTION = 2780 +DUMMY_REMAINING_TIME = '01:29:32' + +MANDATORY_CONFIGURATION = { + DOMAIN: { + CONF_PHONE_ID: DUMMY_PHONE_ID, + CONF_DEVICE_ID: DUMMY_DEVICE_ID, + CONF_DEVICE_PASSWORD: DUMMY_DEVICE_PASSWORD + } +} diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py new file mode 100644 index 00000000000..0defb113747 --- /dev/null +++ b/tests/components/switcher_kis/test_init.py @@ -0,0 +1,49 @@ +"""Test cases for the switcher_kis component.""" + +from typing import Any, Generator + +from homeassistant.components.switcher_kis import (DOMAIN, DATA_DEVICE) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +from .consts import ( + DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME, + DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS, + DUMMY_MAC_ADDRESS, DUMMY_PHONE_ID, DUMMY_POWER_CONSUMPTION, + DUMMY_REMAINING_TIME, MANDATORY_CONFIGURATION) + + +async def test_failed_config(hass: HomeAssistantType) -> None: + """Test failed configuration.""" + assert await async_setup_component( + hass, DOMAIN, MANDATORY_CONFIGURATION) is False + + +async def test_minimal_config(hass: HomeAssistantType, + mock_bridge: Generator[None, Any, None] + ) -> None: + """Test setup with configuration minimal entries.""" + assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) + + +async def test_discovery_data_bucket( + hass: HomeAssistantType, + mock_bridge: Generator[None, Any, None] + ) -> None: + """Test the event send with the updated device.""" + assert await async_setup_component( + hass, DOMAIN, MANDATORY_CONFIGURATION) + + await hass.async_block_till_done() + + device = hass.data[DOMAIN].get(DATA_DEVICE) + assert device.device_id == DUMMY_DEVICE_ID + assert device.ip_addr == DUMMY_IP_ADDRESS + assert device.mac_addr == DUMMY_MAC_ADDRESS + assert device.name == DUMMY_DEVICE_NAME + assert device.state == DUMMY_DEVICE_STATE + assert device.remaining_time == DUMMY_REMAINING_TIME + assert device.auto_off_set == DUMMY_AUTO_OFF_SET + assert device.power_consumption == DUMMY_POWER_CONSUMPTION + assert device.electric_current == DUMMY_ELECTRIC_CURRENT + assert device.phone_id == DUMMY_PHONE_ID From 28c411c74215290f3c8cb9a439a204eaae5457ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:58:44 +0200 Subject: [PATCH 060/346] Drop unnecessary block_till_done for MQTT fan tests (#23253) --- tests/components/mqtt/test_fan.py | 60 ------------------------------- 1 file changed, 60 deletions(-) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index c00de8522b9..bd19ec526a3 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -53,52 +53,37 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', 'StAtE_On') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_ON async_fire_mqtt_message(hass, 'state-topic', 'StAtE_OfF') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_OFF assert state.attributes.get('oscillating') is False async_fire_mqtt_message(hass, 'oscillation-state-topic', 'OsC_On') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.attributes.get('oscillating') is True async_fire_mqtt_message(hass, 'oscillation-state-topic', 'OsC_OfF') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.attributes.get('oscillating') is False assert fan.SPEED_OFF == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_lOw') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_LOW == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_mEdium') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_MEDIUM == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_High') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_HIGH == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_OfF') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_OFF == state.attributes.get('speed') @@ -126,54 +111,39 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', '{"val":"ON"}') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_ON async_fire_mqtt_message(hass, 'state-topic', '{"val":"OFF"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_OFF assert state.attributes.get('oscillating') is False async_fire_mqtt_message( hass, 'oscillation-state-topic', '{"val":"oscillate_on"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.attributes.get('oscillating') is True async_fire_mqtt_message( hass, 'oscillation-state-topic', '{"val":"oscillate_off"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.attributes.get('oscillating') is False assert fan.SPEED_OFF == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"low"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_LOW == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"medium"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_MEDIUM == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"high"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_HIGH == state.attributes.get('speed') async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"off"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert fan.SPEED_OFF == state.attributes.get('speed') @@ -384,28 +354,22 @@ async def test_default_availability_payload(hass, mqtt_mock): assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'online') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is not STATE_UNAVAILABLE assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'availability_topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'state-topic', '1') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'online') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is not STATE_UNAVAILABLE @@ -429,28 +393,22 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'good') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is not STATE_UNAVAILABLE assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'availability_topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'state-topic', '1') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'good') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.state is not STATE_UNAVAILABLE @@ -473,7 +431,6 @@ async def test_discovery_removal_fan(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.beer') assert state is None @@ -501,7 +458,6 @@ async def test_discovery_update_fan(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.beer') assert state is not None @@ -533,7 +489,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.milk') assert state is not None @@ -554,7 +509,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert '100' == state.attributes.get('val') @@ -572,7 +526,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.attributes.get('val') is None @@ -591,7 +544,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('fan.test') assert state.attributes.get('val') is None @@ -616,8 +568,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.beer') assert '100' == state.attributes.get('val') @@ -625,19 +575,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.beer') assert '75' == state.attributes.get('val') @@ -662,7 +607,6 @@ async def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(fan.DOMAIN)) == 1 @@ -694,7 +638,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -735,7 +678,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -746,7 +688,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -777,7 +718,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('fan.beer', new_entity_id='fan.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('fan.beer') assert state is None From 2c42e1a5cb7ea3695ed83209c118ab0dad63a3f2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Apr 2019 23:59:16 +0200 Subject: [PATCH 061/346] Drop unnecessary block_till_done for MQTT tests (#23254) * Drop unnecessary block_till_done * Drop unnecessary block_till_done --- tests/components/mqtt/test_discovery.py | 10 ---------- tests/components/mqtt/test_subscription.py | 12 ------------ 2 files changed, 22 deletions(-) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index ba72db52a8f..42513a2e900 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -236,8 +236,6 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'test_topic/some/base/topic', 'ON') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.DiscoveryExpansionTest1') assert state.state == STATE_ON @@ -271,8 +269,6 @@ async def test_implicit_state_topic_alarm(hass, mqtt_mock, caplog): async_fire_mqtt_message( hass, 'homeassistant/alarm_control_panel/bla/state', 'armed_away') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('alarm_control_panel.Test1') assert state.state == 'armed_away' @@ -305,8 +301,6 @@ async def test_implicit_state_topic_binary_sensor(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/state', 'ON') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('binary_sensor.Test1') assert state.state == 'on' @@ -339,8 +333,6 @@ async def test_implicit_state_topic_sensor(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/state', '1234') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('sensor.Test1') assert state.state == '1234' @@ -374,8 +366,6 @@ async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/state', 'ON') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.Test1') assert state.state == 'off' diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index cd274079e01..180b7af5bef 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -33,15 +33,12 @@ async def test_subscribe_topics(hass, mqtt_mock, caplog): 'msg_callback': record_calls2}}) async_fire_mqtt_message(hass, 'test-topic1', 'test-payload1') - await hass.async_block_till_done() assert 1 == len(calls1) assert 'test-topic1' == calls1[0][0].topic assert 'test-payload1' == calls1[0][0].payload assert 0 == len(calls2) async_fire_mqtt_message(hass, 'test-topic2', 'test-payload2') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 1 == len(calls1) assert 1 == len(calls2) assert 'test-topic2' == calls2[0][0].topic @@ -52,7 +49,6 @@ async def test_subscribe_topics(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'test-topic1', 'test-payload') async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - await hass.async_block_till_done() assert 1 == len(calls1) assert 1 == len(calls2) @@ -82,13 +78,10 @@ async def test_modify_topics(hass, mqtt_mock, caplog): 'msg_callback': record_calls2}}) async_fire_mqtt_message(hass, 'test-topic1', 'test-payload') - await hass.async_block_till_done() assert 1 == len(calls1) assert 0 == len(calls2) async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 1 == len(calls1) assert 1 == len(calls2) @@ -99,14 +92,10 @@ async def test_modify_topics(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'test-topic1', 'test-payload') async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 1 == len(calls1) assert 1 == len(calls2) async_fire_mqtt_message(hass, 'test-topic1_1', 'test-payload') - await hass.async_block_till_done() - await hass.async_block_till_done() assert 2 == len(calls1) assert 'test-topic1_1' == calls1[1][0].topic assert 'test-payload' == calls1[1][0].payload @@ -117,7 +106,6 @@ async def test_modify_topics(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'test-topic1_1', 'test-payload') async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - await hass.async_block_till_done() assert 2 == len(calls1) assert 1 == len(calls2) From eebb452fb546aa13117c37024ca353b6670da4b0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Apr 2019 01:07:28 +0200 Subject: [PATCH 062/346] Drop unnecessary block_till_done, improve tests for MQTT Cover tests (#23255) --- tests/components/mqtt/test_cover.py | 1843 +++++++++++++-------------- 1 file changed, 917 insertions(+), 926 deletions(-) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 47681e0de10..5ca8a1aa649 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,6 +1,5 @@ """The tests for the MQTT cover platform.""" import json -import unittest from unittest.mock import ANY from homeassistant.components import cover, mqtt @@ -13,1071 +12,1081 @@ from homeassistant.const import ( SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, - fire_mqtt_message, get_test_home_assistant, mock_mqtt_component, mock_registry) -class TestCoverMQTT(unittest.TestCase): - """Test the MQTT cover.""" +async def test_state_via_state_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.mock_publish = mock_mqtt_component(self.hass) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + assert not state.attributes.get(ATTR_ASSUMED_STATE) - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() + async_fire_mqtt_message(hass, 'state-topic', STATE_CLOSED) - def test_state_via_state_topic(self): - """Test the controlling state via topic.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - }) + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state - assert not state.attributes.get(ATTR_ASSUMED_STATE) + async_fire_mqtt_message(hass, 'state-topic', STATE_OPEN) - fire_mqtt_message(self.hass, 'state-topic', STATE_CLOSED) - self.hass.block_till_done() + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state - state = self.hass.states.get('cover.test') - assert STATE_CLOSED == state.state - fire_mqtt_message(self.hass, 'state-topic', STATE_OPEN) - self.hass.block_till_done() +async def test_position_via_position_topic(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'get-position-topic', + 'position_open': 100, + 'position_closed': 0, + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) - state = self.hass.states.get('cover.test') - assert STATE_OPEN == state.state + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + assert not state.attributes.get(ATTR_ASSUMED_STATE) - def test_position_via_position_topic(self): - """Test the controlling state via topic.""" - self.assertTrue(setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'get-position-topic', - 'position_open': 100, - 'position_closed': 0, - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - })) + async_fire_mqtt_message(hass, 'get-position-topic', '0') - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state - assert not state.attributes.get(ATTR_ASSUMED_STATE) + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state - fire_mqtt_message(self.hass, 'get-position-topic', '0') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'get-position-topic', '100') - state = self.hass.states.get('cover.test') - assert STATE_CLOSED == state.state + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state - fire_mqtt_message(self.hass, 'get-position-topic', '100') - self.hass.block_till_done() - state = self.hass.states.get('cover.test') - assert STATE_OPEN == state.state +async def test_state_via_template(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'value_template': '\ + {% if (value | multiply(0.01) | int) == 0 %}\ + closed\ + {% else %}\ + open\ + {% endif %}' + } + }) - def test_state_via_template(self): - """Test the controlling state via topic.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'value_template': '\ - {% if (value | multiply(0.01) | int) == 0 %}\ - closed\ - {% else %}\ - open\ - {% endif %}' - } - }) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + async_fire_mqtt_message(hass, 'state-topic', '10000') - fire_mqtt_message(self.hass, 'state-topic', '10000') - self.hass.block_till_done() + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state - state = self.hass.states.get('cover.test') - assert STATE_OPEN == state.state + async_fire_mqtt_message(hass, 'state-topic', '99') - fire_mqtt_message(self.hass, 'state-topic', '99') - self.hass.block_till_done() + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state - state = self.hass.states.get('cover.test') - assert STATE_CLOSED == state.state - def test_position_via_template(self): - """Test the controlling state via topic.""" - self.assertTrue(setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'get-position-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'value_template': '{{ (value | multiply(0.01)) | int }}' - } - })) +async def test_position_via_template(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'get-position-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'value_template': '{{ (value | multiply(0.01)) | int }}' + } + }) - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_UNKNOWN, state.state) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state - fire_mqtt_message(self.hass, 'get-position-topic', '10000') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'get-position-topic', '10000') - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_OPEN, state.state) + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state - fire_mqtt_message(self.hass, 'get-position-topic', '5000') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'get-position-topic', '5000') - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_OPEN, state.state) + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state - fire_mqtt_message(self.hass, 'get-position-topic', '99') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'get-position-topic', '99') - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_CLOSED, state.state) + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state - def test_optimistic_state_change(self): - """Test changing state optimistically.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'command_topic': 'command-topic', - 'qos': 0, - } - }) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state - assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_optimistic_state_change(hass, mqtt_mock): + """Test changing state optimistically.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'command_topic': 'command-topic', + 'qos': 0, + } + }) - self.hass.services.call( + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + assert state.attributes.get(ATTR_ASSUMED_STATE) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'command-topic', 'OPEN', 0, False) - self.mock_publish.async_publish.reset_mock() - state = self.hass.states.get('cover.test') - assert STATE_OPEN == state.state + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'OPEN', 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state - self.hass.services.call( + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'command-topic', 'CLOSE', 0, False) - state = self.hass.states.get('cover.test') - assert STATE_CLOSED == state.state + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'CLOSE', 0, False) + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state - def test_send_open_cover_command(self): - """Test the sending of open_cover.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 2 - } - }) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state +async def test_send_open_cover_command(hass, mqtt_mock): + """Test the sending of open_cover.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + }) - self.hass.services.call( + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'command-topic', 'OPEN', 2, False) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'OPEN', 2, False) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state - def test_send_close_cover_command(self): - """Test the sending of close_cover.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 2 - } - }) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state +async def test_send_close_cover_command(hass, mqtt_mock): + """Test the sending of close_cover.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + }) - self.hass.services.call( + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'command-topic', 'CLOSE', 2, False) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'CLOSE', 2, False) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state - def test_send_stop__cover_command(self): - """Test the sending of stop_cover.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 2 - } - }) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state +async def test_send_stop__cover_command(hass, mqtt_mock): + """Test the sending of stop_cover.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 2 + } + }) - self.hass.services.call( + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'command-topic', 'STOP', 2, False) - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'STOP', 2, False) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state - def test_current_cover_position(self): - """Test the current cover position.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'get-position-topic', - 'command_topic': 'command-topic', - 'position_open': 100, - 'position_closed': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - }) - state_attributes_dict = self.hass.states.get( - 'cover.test').attributes - assert not ('current_position' in state_attributes_dict) - assert not ('current_tilt_position' in state_attributes_dict) - assert not (4 & self.hass.states.get( - 'cover.test').attributes['supported_features'] == 4) +async def test_current_cover_position(hass, mqtt_mock): + """Test the current cover position.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'get-position-topic', + 'command_topic': 'command-topic', + 'position_open': 100, + 'position_closed': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) - fire_mqtt_message(self.hass, 'get-position-topic', '0') - self.hass.block_till_done() - current_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 0 == current_cover_position + state_attributes_dict = hass.states.get( + 'cover.test').attributes + assert not ('current_position' in state_attributes_dict) + assert not ('current_tilt_position' in state_attributes_dict) + assert not (4 & hass.states.get( + 'cover.test').attributes['supported_features'] == 4) - fire_mqtt_message(self.hass, 'get-position-topic', '50') - self.hass.block_till_done() - current_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 50 == current_cover_position + async_fire_mqtt_message(hass, 'get-position-topic', '0') + current_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 0 == current_cover_position - fire_mqtt_message(self.hass, 'get-position-topic', 'non-numeric') - self.hass.block_till_done() - current_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 50 == current_cover_position + async_fire_mqtt_message(hass, 'get-position-topic', '50') + current_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 50 == current_cover_position - fire_mqtt_message(self.hass, 'get-position-topic', '101') - self.hass.block_till_done() - current_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 100 == current_cover_position + async_fire_mqtt_message(hass, 'get-position-topic', 'non-numeric') + current_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 50 == current_cover_position - def test_current_cover_position_inverted(self): - """Test the current cover position.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'get-position-topic', - 'command_topic': 'command-topic', - 'position_open': 0, - 'position_closed': 100, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - }) + async_fire_mqtt_message(hass, 'get-position-topic', '101') + current_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 100 == current_cover_position - state_attributes_dict = self.hass.states.get( - 'cover.test').attributes - assert not ('current_position' in state_attributes_dict) - assert not ('current_tilt_position' in state_attributes_dict) - assert not (4 & self.hass.states.get( - 'cover.test').attributes['supported_features'] == 4) - fire_mqtt_message(self.hass, 'get-position-topic', '100') - self.hass.block_till_done() - current_percentage_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 0 == current_percentage_cover_position - assert STATE_CLOSED == self.hass.states.get( - 'cover.test').state +async def test_current_cover_position_inverted(hass, mqtt_mock): + """Test the current cover position.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'get-position-topic', + 'command_topic': 'command-topic', + 'position_open': 0, + 'position_closed': 100, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) - fire_mqtt_message(self.hass, 'get-position-topic', '0') - self.hass.block_till_done() - current_percentage_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 100 == current_percentage_cover_position - assert STATE_OPEN == self.hass.states.get( - 'cover.test').state + state_attributes_dict = hass.states.get( + 'cover.test').attributes + assert not ('current_position' in state_attributes_dict) + assert not ('current_tilt_position' in state_attributes_dict) + assert not (4 & hass.states.get( + 'cover.test').attributes['supported_features'] == 4) - fire_mqtt_message(self.hass, 'get-position-topic', '50') - self.hass.block_till_done() - current_percentage_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 50 == current_percentage_cover_position - assert STATE_OPEN == self.hass.states.get( - 'cover.test').state + async_fire_mqtt_message(hass, 'get-position-topic', '100') + current_percentage_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 0 == current_percentage_cover_position + assert STATE_CLOSED == hass.states.get( + 'cover.test').state - fire_mqtt_message(self.hass, 'get-position-topic', 'non-numeric') - self.hass.block_till_done() - current_percentage_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 50 == current_percentage_cover_position - assert STATE_OPEN == self.hass.states.get( - 'cover.test').state + async_fire_mqtt_message(hass, 'get-position-topic', '0') + current_percentage_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 100 == current_percentage_cover_position + assert STATE_OPEN == hass.states.get( + 'cover.test').state - fire_mqtt_message(self.hass, 'get-position-topic', '101') - self.hass.block_till_done() - current_percentage_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 0 == current_percentage_cover_position - assert STATE_CLOSED == self.hass.states.get( - 'cover.test').state + async_fire_mqtt_message(hass, 'get-position-topic', '50') + current_percentage_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 50 == current_percentage_cover_position + assert STATE_OPEN == hass.states.get( + 'cover.test').state - def test_set_cover_position(self): - """Test setting cover position.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'get-position-topic', - 'command_topic': 'command-topic', - 'set_position_topic': 'set-position-topic', - 'position_open': 100, - 'position_closed': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - }) + async_fire_mqtt_message(hass, 'get-position-topic', 'non-numeric') + current_percentage_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 50 == current_percentage_cover_position + assert STATE_OPEN == hass.states.get( + 'cover.test').state - state_attributes_dict = self.hass.states.get( - 'cover.test').attributes - assert not ('current_position' in state_attributes_dict) - assert not ('current_tilt_position' in state_attributes_dict) - assert 4 & self.hass.states.get( - 'cover.test').attributes['supported_features'] == 4 + async_fire_mqtt_message(hass, 'get-position-topic', '101') + current_percentage_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 0 == current_percentage_cover_position + assert STATE_CLOSED == hass.states.get( + 'cover.test').state - fire_mqtt_message(self.hass, 'get-position-topic', '22') - self.hass.block_till_done() - state_attributes_dict = self.hass.states.get( - 'cover.test').attributes - assert 'current_position' in state_attributes_dict - assert not ('current_tilt_position' in state_attributes_dict) - current_cover_position = self.hass.states.get( - 'cover.test').attributes['current_position'] - assert 22 == current_cover_position - def test_set_position_templated(self): - """Test setting cover position via template.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'get-position-topic', - 'command_topic': 'command-topic', - 'position_open': 100, - 'position_closed': 0, - 'set_position_topic': 'set-position-topic', - 'set_position_template': '{{100-62}}', - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - }) +async def test_set_cover_position(hass, mqtt_mock): + """Test setting cover position.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'get-position-topic', + 'command_topic': 'command-topic', + 'set_position_topic': 'set-position-topic', + 'position_open': 100, + 'position_closed': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) - self.hass.services.call( + state_attributes_dict = hass.states.get( + 'cover.test').attributes + assert not ('current_position' in state_attributes_dict) + assert not ('current_tilt_position' in state_attributes_dict) + assert 4 & hass.states.get( + 'cover.test').attributes['supported_features'] == 4 + + async_fire_mqtt_message(hass, 'get-position-topic', '22') + state_attributes_dict = hass.states.get( + 'cover.test').attributes + assert 'current_position' in state_attributes_dict + assert not ('current_tilt_position' in state_attributes_dict) + current_cover_position = hass.states.get( + 'cover.test').attributes['current_position'] + assert 22 == current_cover_position + + +async def test_set_position_templated(hass, mqtt_mock): + """Test setting cover position via template.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'get-position-topic', + 'command_topic': 'command-topic', + 'position_open': 100, + 'position_closed': 0, + 'set_position_topic': 'set-position-topic', + 'set_position_template': '{{100-62}}', + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'set-position-topic', '38', 0, False) + mqtt_mock.async_publish.assert_called_once_with( + 'set-position-topic', '38', 0, False) - def test_set_position_untemplated(self): - """Test setting cover position via template.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'position_topic': 'state-topic', - 'command_topic': 'command-topic', - 'set_position_topic': 'position-topic', - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP' - } - }) - self.hass.services.call( +async def test_set_position_untemplated(hass, mqtt_mock): + """Test setting cover position via template.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'position_topic': 'state-topic', + 'command_topic': 'command-topic', + 'set_position_topic': 'position-topic', + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP' + } + }) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'position-topic', 62, 0, False) + mqtt_mock.async_publish.assert_called_once_with( + 'position-topic', 62, 0, False) - def test_no_command_topic(self): - """Test with no command topic.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command', - 'tilt_status_topic': 'tilt-status' - } - }) - assert 240 == self.hass.states.get( - 'cover.test').attributes['supported_features'] +async def test_no_command_topic(hass, mqtt_mock): + """Test with no command topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command', + 'tilt_status_topic': 'tilt-status' + } + }) - def test_with_command_topic_and_tilt(self): - """Test with command topic and tilt config.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'command_topic': 'test', - 'platform': 'mqtt', - 'name': 'test', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command', - 'tilt_status_topic': 'tilt-status' - } - }) + assert 240 == hass.states.get( + 'cover.test').attributes['supported_features'] - assert 251 == self.hass.states.get( - 'cover.test').attributes['supported_features'] - def test_tilt_defaults(self): - """Test the defaults.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command', - 'tilt_status_topic': 'tilt-status' - } - }) +async def test_with_command_topic_and_tilt(hass, mqtt_mock): + """Test with command topic and tilt config.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'command_topic': 'test', + 'platform': 'mqtt', + 'name': 'test', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command', + 'tilt_status_topic': 'tilt-status' + } + }) - state_attributes_dict = self.hass.states.get( - 'cover.test').attributes - assert 'current_tilt_position' in state_attributes_dict + assert 251 == hass.states.get( + 'cover.test').attributes['supported_features'] - current_cover_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert STATE_UNKNOWN == current_cover_position - def test_tilt_via_invocation_defaults(self): - """Test tilt defaults on close/open.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic' - } - }) +async def test_tilt_defaults(hass, mqtt_mock): + """Test the defaults.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command', + 'tilt_status_topic': 'tilt-status' + } + }) - self.hass.services.call( + state_attributes_dict = hass.states.get( + 'cover.test').attributes + assert 'current_tilt_position' in state_attributes_dict + + current_cover_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert STATE_UNKNOWN == current_cover_position + + +async def test_tilt_via_invocation_defaults(hass, mqtt_mock): + """Test tilt defaults on close/open.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic' + } + }) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'tilt-command-topic', 100, 0, False) - self.mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 100, 0, False) + mqtt_mock.async_publish.reset_mock() - self.hass.services.call( + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'tilt-command-topic', 0, 0, False) + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 0, 0, False) - def test_tilt_given_value(self): - """Test tilting to a given value.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'tilt_opened_value': 400, - 'tilt_closed_value': 125 - } - }) - self.hass.services.call( +async def test_tilt_given_value(hass, mqtt_mock): + """Test tilting to a given value.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'tilt_opened_value': 400, + 'tilt_closed_value': 125 + } + }) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'tilt-command-topic', 400, 0, False) - self.mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 400, 0, False) + mqtt_mock.async_publish.reset_mock() - self.hass.services.call( + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'}, blocking=True) - self.hass.block_till_done() + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'tilt-command-topic', 125, 0, False) + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 125, 0, False) - def test_tilt_via_topic(self): - """Test tilt by updating status via MQTT.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'tilt_opened_value': 400, - 'tilt_closed_value': 125 - } - }) - fire_mqtt_message(self.hass, 'tilt-status-topic', '0') - self.hass.block_till_done() +async def test_tilt_via_topic(hass, mqtt_mock): + """Test tilt by updating status via MQTT.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'tilt_opened_value': 400, + 'tilt_closed_value': 125 + } + }) - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 0 == current_cover_tilt_position + async_fire_mqtt_message(hass, 'tilt-status-topic', '0') - fire_mqtt_message(self.hass, 'tilt-status-topic', '50') - self.hass.block_till_done() + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 0 == current_cover_tilt_position - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 50 == current_cover_tilt_position + async_fire_mqtt_message(hass, 'tilt-status-topic', '50') - def test_tilt_via_topic_altered_range(self): - """Test tilt status via MQTT with altered tilt range.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'tilt_opened_value': 400, - 'tilt_closed_value': 125, - 'tilt_min': 0, - 'tilt_max': 50 - } - }) + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 50 == current_cover_tilt_position - fire_mqtt_message(self.hass, 'tilt-status-topic', '0') - self.hass.block_till_done() - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 0 == current_cover_tilt_position +async def test_tilt_via_topic_altered_range(hass, mqtt_mock): + """Test tilt status via MQTT with altered tilt range.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'tilt_opened_value': 400, + 'tilt_closed_value': 125, + 'tilt_min': 0, + 'tilt_max': 50 + } + }) - fire_mqtt_message(self.hass, 'tilt-status-topic', '50') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'tilt-status-topic', '0') - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 100 == current_cover_tilt_position + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 0 == current_cover_tilt_position - fire_mqtt_message(self.hass, 'tilt-status-topic', '25') - self.hass.block_till_done() + async_fire_mqtt_message(hass, 'tilt-status-topic', '50') - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 50 == current_cover_tilt_position + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 100 == current_cover_tilt_position - def test_tilt_position(self): - """Test tilt via method invocation.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'tilt_opened_value': 400, - 'tilt_closed_value': 125 - } - }) + async_fire_mqtt_message(hass, 'tilt-status-topic', '25') - self.hass.services.call( + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 50 == current_cover_tilt_position + + +async def test_tilt_position(hass, mqtt_mock): + """Test tilt via method invocation.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'tilt_opened_value': 400, + 'tilt_closed_value': 125 + } + }) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, - blocking=True) - self.hass.block_till_done() + blocking=True)) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'tilt-command-topic', 50, 0, False) + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 50, 0, False) - def test_tilt_position_altered_range(self): - """Test tilt via method invocation with altered range.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'qos': 0, - 'payload_open': 'OPEN', - 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'tilt_opened_value': 400, - 'tilt_closed_value': 125, - 'tilt_min': 0, - 'tilt_max': 50 - } - }) - self.hass.services.call( +async def test_tilt_position_altered_range(hass, mqtt_mock): + """Test tilt via method invocation with altered range.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'qos': 0, + 'payload_open': 'OPEN', + 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'tilt_opened_value': 400, + 'tilt_closed_value': 125, + 'tilt_min': 0, + 'tilt_max': 50 + } + }) + + hass.async_add_job( + hass.services.async_call( cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, - blocking=True) - self.hass.block_till_done() + blocking=True)) + await hass.async_block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'tilt-command-topic', 25, 0, False) + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 25, 0, False) - def test_find_percentage_in_range_defaults(self): - """Test find percentage in range with default range.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 100, 'position_closed': 0, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 100, 'tilt_closed_position': 0, - 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, - 'tilt_invert_state': False, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) - assert 44 == mqtt_cover.find_percentage_in_range(44) - assert 44 == mqtt_cover.find_percentage_in_range(44, 'cover') +async def test_find_percentage_in_range_defaults(hass, mqtt_mock): + """Test find percentage in range with default range.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 100, 'position_closed': 0, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 100, 'tilt_closed_position': 0, + 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, + 'tilt_invert_state': False, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - def test_find_percentage_in_range_altered(self): - """Test find percentage in range with altered range.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 180, 'position_closed': 80, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 180, 'tilt_closed_position': 80, - 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, - 'tilt_invert_state': False, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) + assert 44 == mqtt_cover.find_percentage_in_range(44) + assert 44 == mqtt_cover.find_percentage_in_range(44, 'cover') - assert 40 == mqtt_cover.find_percentage_in_range(120) - assert 40 == mqtt_cover.find_percentage_in_range(120, 'cover') - def test_find_percentage_in_range_defaults_inverted(self): - """Test find percentage in range with default range but inverted.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 0, 'position_closed': 100, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 100, 'tilt_closed_position': 0, - 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, - 'tilt_invert_state': True, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) +async def test_find_percentage_in_range_altered(hass, mqtt_mock): + """Test find percentage in range with altered range.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 180, 'position_closed': 80, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 180, 'tilt_closed_position': 80, + 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, + 'tilt_invert_state': False, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - assert 56 == mqtt_cover.find_percentage_in_range(44) - assert 56 == mqtt_cover.find_percentage_in_range(44, 'cover') + assert 40 == mqtt_cover.find_percentage_in_range(120) + assert 40 == mqtt_cover.find_percentage_in_range(120, 'cover') - def test_find_percentage_in_range_altered_inverted(self): - """Test find percentage in range with altered range and inverted.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 80, 'position_closed': 180, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 180, 'tilt_closed_position': 80, - 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, - 'tilt_invert_state': True, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) - assert 60 == mqtt_cover.find_percentage_in_range(120) - assert 60 == mqtt_cover.find_percentage_in_range(120, 'cover') +async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): + """Test find percentage in range with default range but inverted.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 0, 'position_closed': 100, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 100, 'tilt_closed_position': 0, + 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, + 'tilt_invert_state': True, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - def test_find_in_range_defaults(self): - """Test find in range with default range.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 100, 'position_closed': 0, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 100, 'tilt_closed_position': 0, - 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, - 'tilt_invert_state': False, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) + assert 56 == mqtt_cover.find_percentage_in_range(44) + assert 56 == mqtt_cover.find_percentage_in_range(44, 'cover') - assert 44 == mqtt_cover.find_in_range_from_percent(44) - assert 44 == mqtt_cover.find_in_range_from_percent(44, 'cover') - def test_find_in_range_altered(self): - """Test find in range with altered range.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 180, 'position_closed': 80, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 180, 'tilt_closed_position': 80, - 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, - 'tilt_invert_state': False, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) +async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): + """Test find percentage in range with altered range and inverted.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 80, 'position_closed': 180, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 180, 'tilt_closed_position': 80, + 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, + 'tilt_invert_state': True, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - assert 120 == mqtt_cover.find_in_range_from_percent(40) - assert 120 == mqtt_cover.find_in_range_from_percent(40, 'cover') + assert 60 == mqtt_cover.find_percentage_in_range(120) + assert 60 == mqtt_cover.find_percentage_in_range(120, 'cover') - def test_find_in_range_defaults_inverted(self): - """Test find in range with default range but inverted.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 0, 'position_closed': 100, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 100, 'tilt_closed_position': 0, - 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, - 'tilt_invert_state': True, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) - assert 44 == mqtt_cover.find_in_range_from_percent(56) - assert 44 == mqtt_cover.find_in_range_from_percent(56, 'cover') +async def test_find_in_range_defaults(hass, mqtt_mock): + """Test find in range with default range.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 100, 'position_closed': 0, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 100, 'tilt_closed_position': 0, + 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, + 'tilt_invert_state': False, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - def test_find_in_range_altered_inverted(self): - """Test find in range with altered range and inverted.""" - mqtt_cover = MqttCover( - { - 'name': 'cover.test', - 'state_topic': 'state-topic', - 'get_position_topic': None, - 'command_topic': 'command-topic', - 'availability_topic': None, - 'tilt_command_topic': 'tilt-command-topic', - 'tilt_status_topic': 'tilt-status-topic', - 'qos': 0, - 'retain': False, - 'state_open': 'OPEN', 'state_closed': 'CLOSE', - 'position_open': 80, 'position_closed': 180, - 'payload_open': 'OPEN', 'payload_close': 'CLOSE', - 'payload_stop': 'STOP', - 'payload_available': None, 'payload_not_available': None, - 'optimistic': False, 'value_template': None, - 'tilt_open_position': 180, 'tilt_closed_position': 80, - 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, - 'tilt_invert_state': True, - 'set_position_topic': None, 'set_position_template': None, - 'unique_id': None, 'device_config': None, - }, - None, - None) + assert 44 == mqtt_cover.find_in_range_from_percent(44) + assert 44 == mqtt_cover.find_in_range_from_percent(44, 'cover') - assert 120 == mqtt_cover.find_in_range_from_percent(60) - assert 120 == mqtt_cover.find_in_range_from_percent(60, 'cover') - def test_availability_without_topic(self): - """Test availability without defined availability topic.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic' - } - }) +async def test_find_in_range_altered(hass, mqtt_mock): + """Test find in range with altered range.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 180, 'position_closed': 80, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 180, 'tilt_closed_position': 80, + 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, + 'tilt_invert_state': False, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state + assert 120 == mqtt_cover.find_in_range_from_percent(40) + assert 120 == mqtt_cover.find_in_range_from_percent(40, 'cover') - def test_availability_by_defaults(self): - """Test availability by defaults with defined topic.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'availability_topic': 'availability-topic' - } - }) - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state +async def test_find_in_range_defaults_inverted(hass, mqtt_mock): + """Test find in range with default range but inverted.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 0, 'position_closed': 100, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 100, 'tilt_closed_position': 0, + 'tilt_min': 0, 'tilt_max': 100, 'tilt_optimistic': False, + 'tilt_invert_state': True, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() + assert 44 == mqtt_cover.find_in_range_from_percent(56) + assert 44 == mqtt_cover.find_in_range_from_percent(56, 'cover') - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() +async def test_find_in_range_altered_inverted(hass, mqtt_mock): + """Test find in range with altered range and inverted.""" + mqtt_cover = MqttCover( + { + 'name': 'cover.test', + 'state_topic': 'state-topic', + 'get_position_topic': None, + 'command_topic': 'command-topic', + 'availability_topic': None, + 'tilt_command_topic': 'tilt-command-topic', + 'tilt_status_topic': 'tilt-status-topic', + 'qos': 0, + 'retain': False, + 'state_open': 'OPEN', 'state_closed': 'CLOSE', + 'position_open': 80, 'position_closed': 180, + 'payload_open': 'OPEN', 'payload_close': 'CLOSE', + 'payload_stop': 'STOP', + 'payload_available': None, 'payload_not_available': None, + 'optimistic': False, 'value_template': None, + 'tilt_open_position': 180, 'tilt_closed_position': 80, + 'tilt_min': 80, 'tilt_max': 180, 'tilt_optimistic': False, + 'tilt_invert_state': True, + 'set_position_topic': None, 'set_position_template': None, + 'unique_id': None, 'device_config': None, + }, + None, + None) - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state + assert 120 == mqtt_cover.find_in_range_from_percent(60) + assert 120 == mqtt_cover.find_in_range_from_percent(60, 'cover') - def test_availability_by_custom_payload(self): - """Test availability by custom payload with defined topic.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'state-topic', - 'command_topic': 'command-topic', - 'availability_topic': 'availability-topic', - 'payload_available': 'good', - 'payload_not_available': 'nogood' - } - }) - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic' + } + }) - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE != state.state - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() +async def test_availability_by_defaults(hass, mqtt_mock): + """Test availability by defaults with defined topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability-topic' + } + }) - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE == state.state - def test_valid_device_class(self): - """Test the setting of a valid sensor class.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'device_class': 'garage', - 'state_topic': 'test-topic', - } - }) + async_fire_mqtt_message(hass, 'availability-topic', 'online') + await hass.async_block_till_done() - state = self.hass.states.get('cover.test') - assert 'garage' == state.attributes.get('device_class') + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE != state.state - def test_invalid_device_class(self): - """Test the setting of an invalid sensor class.""" - assert setup_component(self.hass, cover.DOMAIN, { - cover.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'device_class': 'abc123', - 'state_topic': 'test-topic', - } - }) + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + await hass.async_block_till_done() - state = self.hass.states.get('cover.test') - assert state is None + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE == state.state + + +async def test_availability_by_custom_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + } + }) + + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + await hass.async_block_till_done() + + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + await hass.async_block_till_done() + + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE == state.state + + +async def test_valid_device_class(hass, mqtt_mock): + """Test the setting of a valid sensor class.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'garage', + 'state_topic': 'test-topic', + } + }) + + state = hass.states.get('cover.test') + assert 'garage' == state.attributes.get('device_class') + + +async def test_invalid_device_class(hass, mqtt_mock): + """Test the setting of an invalid sensor class.""" + assert await async_setup_component(hass, cover.DOMAIN, { + cover.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'abc123', + 'state_topic': 'test-topic', + } + }) + + state = hass.states.get('cover.test') + assert state is None async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -1092,7 +1101,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('cover.test') assert '100' == state.attributes.get('val') @@ -1110,7 +1118,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('cover.test') assert state.attributes.get('val') is None @@ -1129,7 +1136,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('cover.test') assert state.attributes.get('val') is None @@ -1154,8 +1160,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.beer') assert '100' == state.attributes.get('val') @@ -1163,19 +1167,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.beer') assert '75' == state.attributes.get('val') @@ -1197,7 +1196,6 @@ async def test_discovery_removal_cover(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.beer') assert state is None @@ -1224,7 +1222,6 @@ async def test_discovery_update_cover(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.beer') assert state is not None @@ -1258,7 +1255,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.milk') assert state is not None @@ -1285,7 +1281,6 @@ async def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() assert len(hass.states.async_entity_ids(cover.DOMAIN)) == 1 @@ -1317,7 +1312,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1358,7 +1352,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1369,7 +1362,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -1399,7 +1391,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('cover.beer', new_entity_id='cover.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('cover.beer') assert state is None From 1e0bc97f56b2acdbc229e5a50dd22e808e83d62c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 20 Apr 2019 01:08:11 +0200 Subject: [PATCH 063/346] Drop unnecessary block_till_done (#23256) --- tests/components/mqtt/test_switch.py | 46 ---------------------------- 1 file changed, 46 deletions(-) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 7917803aa07..dfd05424ca7 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -42,14 +42,11 @@ async def test_controlling_state_via_topic(hass, mock_publish): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', '1') - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_ON == state.state async_fire_mqtt_message(hass, 'state-topic', '0') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_OFF == state.state @@ -115,15 +112,11 @@ async def test_controlling_state_via_topic_and_json_message( assert STATE_OFF == state.state async_fire_mqtt_message(hass, 'state-topic', '{"val":"beer on"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_ON == state.state async_fire_mqtt_message(hass, 'state-topic', '{"val":"beer off"}') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_OFF == state.state @@ -147,30 +140,22 @@ async def test_default_availability_payload(hass, mock_publish): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability_topic', 'online') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_OFF == state.state assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'availability_topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'state-topic', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability_topic', 'online') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_ON == state.state @@ -196,29 +181,22 @@ async def test_custom_availability_payload(hass, mock_publish): assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability_topic', 'good') - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_OFF == state.state assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'availability_topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'state-topic', '1') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_UNAVAILABLE == state.state async_fire_mqtt_message(hass, 'availability_topic', 'good') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_ON == state.state @@ -244,15 +222,11 @@ async def test_custom_state_payload(hass, mock_publish): assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', 'HIGH') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_ON == state.state async_fire_mqtt_message(hass, 'state-topic', 'LOW') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.test') assert STATE_OFF == state.state @@ -270,7 +244,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('switch.test') assert '100' == state.attributes.get('val') @@ -288,7 +261,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('switch.test') assert state.attributes.get('val') is None @@ -307,7 +279,6 @@ async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('switch.test') assert state.attributes.get('val') is None @@ -332,8 +303,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert '100' == state.attributes.get('val') @@ -341,19 +310,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert '100' == state.attributes.get('val') # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert '75' == state.attributes.get('val') @@ -378,8 +342,6 @@ async def test_unique_id(hass): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 2 # all switches group is 1, unique id created is 1 @@ -399,7 +361,6 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert state is not None @@ -408,7 +369,6 @@ async def test_discovery_removal_switch(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert state is None @@ -441,7 +401,6 @@ async def test_discovery_update_switch(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert state is not None @@ -474,7 +433,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.milk') assert state is not None @@ -510,7 +468,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -551,7 +508,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -562,7 +518,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -593,7 +548,6 @@ async def test_entity_id_update(hass, mqtt_mock): registry.async_update_entity('switch.beer', new_entity_id='switch.milk') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('switch.beer') assert state is None From e2ed2ecdc066d884fc38cd97341d535372a3840c Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 19 Apr 2019 18:56:34 -0500 Subject: [PATCH 064/346] Return 0 instead of None (#23261) --- homeassistant/components/plex/media_player.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 9ff00ed1c23..4cb4204f274 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -672,7 +672,7 @@ class PlexClient(MediaPlayerDevice): def supported_features(self): """Flag media player features that are supported.""" if not self._is_player_active: - return None + return 0 # force show all controls if self.config.get(CONF_SHOW_ALL_CONTROLS): @@ -683,7 +683,7 @@ class PlexClient(MediaPlayerDevice): # only show controls when we know what device is connecting if not self._make: - return None + return 0 # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( @@ -708,7 +708,7 @@ class PlexClient(MediaPlayerDevice): SUPPORT_VOLUME_SET | SUPPORT_PLAY | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE) - return None + return 0 def set_volume_level(self, volume): """Set volume level, range 0..1.""" From a3ecde01eef7a47613c4f36eeb4215ee3a1ddaa2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 19 Apr 2019 16:57:45 -0700 Subject: [PATCH 065/346] Updated frontend to 20190419.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 4821c39ff32..ae91178e4c4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190417.0" + "home-assistant-frontend==20190419.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 5fa04616a8e..640daf76cfd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -557,7 +557,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190417.0 +home-assistant-frontend==20190419.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 383aec75958..2b2448fb447 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -139,7 +139,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190417.0 +home-assistant-frontend==20190419.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From b697bb7a26146cf9365b43c5cca89807676f2435 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 19 Apr 2019 21:22:40 -0500 Subject: [PATCH 066/346] Update pyheos and log service errors in HEOS integration (#23222) * Update pyheos and command error handling * Correct comment and remove unnecessary autospec --- homeassistant/components/heos/manifest.json | 2 +- homeassistant/components/heos/media_player.py | 29 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/heos/conftest.py | 8 +- tests/components/heos/test_media_player.py | 208 +++++++++++++----- 6 files changed, 192 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 2977345f97d..97b53935614 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -3,7 +3,7 @@ "name": "Heos", "documentation": "https://www.home-assistant.io/components/heos", "requirements": [ - "pyheos==0.3.1" + "pyheos==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 0da9db31bb2..8821591df20 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,5 +1,6 @@ """Denon HEOS Media Player.""" -from functools import reduce +from functools import reduce, wraps +import logging from operator import ior from typing import Sequence @@ -21,6 +22,8 @@ BASE_SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ SUPPORT_VOLUME_STEP | SUPPORT_CLEAR_PLAYLIST | \ SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOURCE +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -36,6 +39,20 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry, async_add_entities(devices, True) +def log_command_error(command: str): + """Return decorator that logs command failure.""" + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + from pyheos import CommandError + try: + await func(*args, **kwargs) + except CommandError as ex: + _LOGGER.error("Unable to %s: %s", command, ex) + return wrapper + return decorator + + class HeosMediaPlayer(MediaPlayerDevice): """The HEOS player.""" @@ -101,42 +118,52 @@ class HeosMediaPlayer(MediaPlayerDevice): self.hass.helpers.dispatcher.async_dispatcher_connect( SIGNAL_HEOS_SOURCES_UPDATED, self._sources_updated)) + @log_command_error("clear playlist") async def async_clear_playlist(self): """Clear players playlist.""" await self._player.clear_queue() + @log_command_error("pause") async def async_media_pause(self): """Send pause command.""" await self._player.pause() + @log_command_error("play") async def async_media_play(self): """Send play command.""" await self._player.play() + @log_command_error("move to previous track") async def async_media_previous_track(self): """Send previous track command.""" await self._player.play_previous() + @log_command_error("move to next track") async def async_media_next_track(self): """Send next track command.""" await self._player.play_next() + @log_command_error("stop") async def async_media_stop(self): """Send stop command.""" await self._player.stop() + @log_command_error("set mute") async def async_mute_volume(self, mute): """Mute the volume.""" await self._player.set_mute(mute) + @log_command_error("select source") async def async_select_source(self, source): """Select input source.""" await self._source_manager.play_source(source, self._player) + @log_command_error("set shuffle") async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" await self._player.set_play_mode(self._player.repeat, shuffle) + @log_command_error("set volume level") async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" await self._player.set_volume(int(volume * 100)) diff --git a/requirements_all.txt b/requirements_all.txt index 640daf76cfd..b5510c421b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1082,7 +1082,7 @@ pygtt==1.1.2 pyhaversion==2.2.0 # homeassistant.components.heos -pyheos==0.3.1 +pyheos==0.4.0 # homeassistant.components.hikvision pyhik==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b2448fb447..6c58aa863f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -220,7 +220,7 @@ pydeconz==54 pydispatcher==2.0.5 # homeassistant.components.heos -pyheos==0.3.1 +pyheos==0.4.0 # homeassistant.components.homematic pyhomematic==0.1.58 diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 211153b1cc7..496f143d51f 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -44,7 +44,7 @@ def config_fixture(): @pytest.fixture(name="players") def player_fixture(dispatcher): """Create a mock HeosPlayer.""" - player = Mock(HeosPlayer, autospec=True) + player = Mock(HeosPlayer) player.heos.dispatcher = dispatcher player.player_id = 1 player.name = "Test Player" @@ -77,11 +77,11 @@ def player_fixture(dispatcher): @pytest.fixture(name="favorites") def favorites_fixture() -> Dict[int, HeosSource]: """Create favorites fixture.""" - station = Mock(HeosSource, autospec=True) + station = Mock(HeosSource) station.type = const.TYPE_STATION station.name = "Today's Hits Radio" station.media_id = '123456789' - radio = Mock(HeosSource, autospec=True) + radio = Mock(HeosSource) radio.type = const.TYPE_STATION radio.name = "Classical MPR (Classical Music)" radio.media_id = 's1234' @@ -94,7 +94,7 @@ def favorites_fixture() -> Dict[int, HeosSource]: @pytest.fixture(name="input_sources") def input_sources_fixture() -> Sequence[InputSource]: """Create a set of input sources for testing.""" - source = Mock(InputSource, autospec=True) + source = Mock(InputSource) source.player_id = 1 source.input_name = const.INPUT_AUX_IN_1 source.name = "HEOS Drive - Line In 1" diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index dd36c2c013d..0870f82b3ff 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the Heos Media Player platform.""" import asyncio -from pyheos import const +from pyheos import const, CommandError from homeassistant.components.heos import media_player from homeassistant.components.heos.const import ( @@ -162,67 +162,142 @@ async def test_updates_from_user_changed( assert state.attributes[ATTR_INPUT_SOURCE_LIST] == source_list -async def test_services(hass, config_entry, config, controller): - """Tests player commands.""" +async def test_clear_playlist(hass, config_entry, config, controller, caplog): + """Test the clear playlist service.""" await setup_platform(hass, config_entry, config) player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_CLEAR_PLAYLIST, + {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) + assert player.clear_queue.call_count == 1 + player.clear_queue.reset_mock() + player.clear_queue.side_effect = CommandError(None, "Failure", 1) + assert "Unable to clear playlist: Failure (1)" in caplog.text - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_CLEAR_PLAYLIST, - {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) - assert player.clear_queue.call_count == 1 - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PAUSE, - {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) - assert player.pause.call_count == 1 +async def test_pause(hass, config_entry, config, controller, caplog): + """Test the pause service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PAUSE, + {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) + assert player.pause.call_count == 1 + player.pause.reset_mock() + player.pause.side_effect = CommandError(None, "Failure", 1) + assert "Unable to pause: Failure (1)" in caplog.text - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PLAY, - {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) - assert player.play.call_count == 1 - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, - {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) - assert player.play_previous.call_count == 1 +async def test_play(hass, config_entry, config, controller, caplog): + """Test the play service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PLAY, + {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) + assert player.play.call_count == 1 + player.play.reset_mock() + player.play.side_effect = CommandError(None, "Failure", 1) + assert "Unable to play: Failure (1)" in caplog.text - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, - {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) - assert player.play_next.call_count == 1 - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_STOP, - {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) - assert player.stop.call_count == 1 +async def test_previous_track(hass, config_entry, config, controller, caplog): + """Test the previous track service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_PREVIOUS_TRACK, + {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) + assert player.play_previous.call_count == 1 + player.play_previous.reset_mock() + player.play_previous.side_effect = CommandError(None, "Failure", 1) + assert "Unable to move to previous track: Failure (1)" in caplog.text - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, - {ATTR_ENTITY_ID: 'media_player.test_player', - ATTR_MEDIA_VOLUME_MUTED: True}, blocking=True) - player.set_mute.assert_called_once_with(True) - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_SHUFFLE_SET, - {ATTR_ENTITY_ID: 'media_player.test_player', - ATTR_MEDIA_SHUFFLE: True}, blocking=True) - player.set_play_mode.assert_called_once_with(player.repeat, True) +async def test_next_track(hass, config_entry, config, controller, caplog): + """Test the next track service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_NEXT_TRACK, + {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) + assert player.play_next.call_count == 1 + player.play_next.reset_mock() + player.play_next.side_effect = CommandError(None, "Failure", 1) + assert "Unable to move to next track: Failure (1)" in caplog.text - player.reset_mock() - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: 'media_player.test_player', - ATTR_MEDIA_VOLUME_LEVEL: 1}, blocking=True) - player.set_volume.assert_called_once_with(100) - assert isinstance(player.set_volume.call_args[0][0], int) + +async def test_stop(hass, config_entry, config, controller, caplog): + """Test the stop service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_MEDIA_STOP, + {ATTR_ENTITY_ID: 'media_player.test_player'}, blocking=True) + assert player.stop.call_count == 1 + player.stop.reset_mock() + player.stop.side_effect = CommandError(None, "Failure", 1) + assert "Unable to stop: Failure (1)" in caplog.text + + +async def test_volume_mute(hass, config_entry, config, controller, caplog): + """Test the volume mute service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_MUTE, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_MEDIA_VOLUME_MUTED: True}, blocking=True) + assert player.set_mute.call_count == 1 + player.set_mute.reset_mock() + player.set_mute.side_effect = CommandError(None, "Failure", 1) + assert "Unable to set mute: Failure (1)" in caplog.text + + +async def test_shuffle_set(hass, config_entry, config, controller, caplog): + """Test the shuffle set service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_SHUFFLE_SET, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_MEDIA_SHUFFLE: True}, blocking=True) + player.set_play_mode.assert_called_once_with(player.repeat, True) + player.set_play_mode.reset_mock() + player.set_play_mode.side_effect = CommandError(None, "Failure", 1) + assert "Unable to set shuffle: Failure (1)" in caplog.text + + +async def test_volume_set(hass, config_entry, config, controller, caplog): + """Test the volume set service.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_VOLUME_SET, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_MEDIA_VOLUME_LEVEL: 1}, blocking=True) + player.set_volume.assert_called_once_with(100) + player.set_volume.reset_mock() + player.set_volume.side_effect = CommandError(None, "Failure", 1) + assert "Unable to set volume level: Failure (1)" in caplog.text async def test_select_favorite( @@ -270,6 +345,22 @@ async def test_select_radio_favorite( assert state.attributes[ATTR_INPUT_SOURCE] == favorite.name +async def test_select_radio_favorite_command_error( + hass, config_entry, config, controller, favorites, caplog): + """Tests command error loged when playing favorite.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + # Test set radio preset + favorite = favorites[2] + player.play_favorite.side_effect = CommandError(None, "Failure", 1) + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_INPUT_SOURCE: favorite.name}, blocking=True) + player.play_favorite.assert_called_once_with(2) + assert "Unable to select source: Failure (1)" in caplog.text + + async def test_select_input_source( hass, config_entry, config, controller, input_sources): """Tests selecting input source and state.""" @@ -304,6 +395,21 @@ async def test_select_input_unknown( assert "Unknown source: Unknown" in caplog.text +async def test_select_input_command_error( + hass, config_entry, config, controller, caplog, input_sources): + """Tests selecting an unknown input.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + input_source = input_sources[0] + player.play_input_source.side_effect = CommandError(None, "Failure", 1) + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_INPUT_SOURCE: input_source.name}, blocking=True) + player.play_input_source.assert_called_once_with(input_source) + assert "Unable to select source: Failure (1)" in caplog.text + + async def test_unload_config_entry(hass, config_entry, config, controller): """Test the player is removed when the config entry is unloaded.""" await setup_platform(hass, config_entry, config) From df32830f1764edf718a91dce891010480ebdec16 Mon Sep 17 00:00:00 2001 From: damarco Date: Sat, 20 Apr 2019 16:12:28 +0200 Subject: [PATCH 067/346] Bump zigpy-deconz (#23270) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9fd0629fcb2..fb30c09d26b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "requirements": [ "bellows-homeassistant==0.7.2", "zha-quirks==0.0.8", - "zigpy-deconz==0.1.3", + "zigpy-deconz==0.1.4", "zigpy-homeassistant==0.3.1", "zigpy-xbee-homeassistant==0.1.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index b5510c421b0..bfc20731598 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1848,7 +1848,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.1.3 +zigpy-deconz==0.1.4 # homeassistant.components.zha zigpy-homeassistant==0.3.1 From b3c7142030a8f7c58a34c19723f68d6e0295b1a1 Mon Sep 17 00:00:00 2001 From: damarco Date: Sun, 21 Apr 2019 00:04:30 +0200 Subject: [PATCH 068/346] Bump zigpy and zigpy-xbee (#23275) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index fb30c09d26b..c8bc0479f30 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,8 +6,8 @@ "bellows-homeassistant==0.7.2", "zha-quirks==0.0.8", "zigpy-deconz==0.1.4", - "zigpy-homeassistant==0.3.1", - "zigpy-xbee-homeassistant==0.1.3" + "zigpy-homeassistant==0.3.2", + "zigpy-xbee-homeassistant==0.2.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index bfc20731598..0926f79d763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1851,10 +1851,10 @@ ziggo-mediabox-xl==1.1.0 zigpy-deconz==0.1.4 # homeassistant.components.zha -zigpy-homeassistant==0.3.1 +zigpy-homeassistant==0.3.2 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.1.3 +zigpy-xbee-homeassistant==0.2.0 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c58aa863f6..a093ccdc1ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -333,4 +333,4 @@ vultr==0.1.2 wakeonlan==1.1.6 # homeassistant.components.zha -zigpy-homeassistant==0.3.1 +zigpy-homeassistant==0.3.2 From 80653824d9c2ca44baf551c6aa4e7a83ed1f26f0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 20 Apr 2019 21:15:19 -0600 Subject: [PATCH 069/346] Add ctags file to .gitignore (#23279) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b486032c741..75ab19881ac 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,7 @@ Scripts/ # vimmy stuff *.swp *.swo - +tags ctags.tmp # vagrant stuff From a8632480ffd68035d7f486b27a4fbef9940e4900 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sun, 21 Apr 2019 06:52:20 +0200 Subject: [PATCH 070/346] Upgrade xmltodict to 0.12.0 (#23277) --- homeassistant/components/bluesound/manifest.json | 2 +- homeassistant/components/startca/manifest.json | 2 +- homeassistant/components/ted5000/manifest.json | 2 +- homeassistant/components/yr/manifest.json | 2 +- homeassistant/components/zestimate/manifest.json | 2 +- requirements_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index 9016502b5d3..7731f845005 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -3,7 +3,7 @@ "name": "Bluesound", "documentation": "https://www.home-assistant.io/components/bluesound", "requirements": [ - "xmltodict==0.11.0" + "xmltodict==0.12.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index 1d13936f592..d2f9e90c41a 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -3,7 +3,7 @@ "name": "Startca", "documentation": "https://www.home-assistant.io/components/startca", "requirements": [ - "xmltodict==0.11.0" + "xmltodict==0.12.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index cf0439345dc..9cc50405bad 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -3,7 +3,7 @@ "name": "Ted5000", "documentation": "https://www.home-assistant.io/components/ted5000", "requirements": [ - "xmltodict==0.11.0" + "xmltodict==0.12.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json index ec12f6cdac4..88daadd35aa 100644 --- a/homeassistant/components/yr/manifest.json +++ b/homeassistant/components/yr/manifest.json @@ -3,7 +3,7 @@ "name": "Yr", "documentation": "https://www.home-assistant.io/components/yr", "requirements": [ - "xmltodict==0.11.0" + "xmltodict==0.12.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 1d67ddbd581..4d1a55eaa09 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -3,7 +3,7 @@ "name": "Zestimate", "documentation": "https://www.home-assistant.io/components/zestimate", "requirements": [ - "xmltodict==0.11.0" + "xmltodict==0.12.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 0926f79d763..ec9d2347fd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1812,7 +1812,7 @@ xknx==0.10.0 # homeassistant.components.ted5000 # homeassistant.components.yr # homeassistant.components.zestimate -xmltodict==0.11.0 +xmltodict==0.12.0 # homeassistant.components.xs1 xs1-api-client==2.3.5 From 3b0660ae896d89e339d19634bf909815dbff1304 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Sun, 21 Apr 2019 09:03:17 +0200 Subject: [PATCH 071/346] Upgrade pyotp to 2.2.7 (#23274) --- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/auth/mfa_modules/totp.py | 2 +- homeassistant/components/otp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 310abff9484..396a0fb8d3f 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow -REQUIREMENTS = ['pyotp==2.2.6'] +REQUIREMENTS = ['pyotp==2.2.7'] CONF_MESSAGE = 'message' diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index dc51152f565..bb07d9e479f 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from . import MultiFactorAuthModule, MULTI_FACTOR_AUTH_MODULES, \ MULTI_FACTOR_AUTH_MODULE_SCHEMA, SetupFlow -REQUIREMENTS = ['pyotp==2.2.6', 'PyQRCode==1.2.1'] +REQUIREMENTS = ['pyotp==2.2.7', 'PyQRCode==1.2.1'] CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 3eb24e0f1c6..cea246af328 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -3,7 +3,7 @@ "name": "Otp", "documentation": "https://www.home-assistant.io/components/otp", "requirements": [ - "pyotp==2.2.6" + "pyotp==2.2.7" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ec9d2347fd1..7bb24ef8bfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1216,7 +1216,7 @@ pyotgw==0.4b3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.6 +pyotp==2.2.7 # homeassistant.components.owlet pyowlet==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a093ccdc1ad..e0051c8edaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ pyopenuv==1.0.9 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp -pyotp==2.2.6 +pyotp==2.2.7 # homeassistant.components.ps4 pyps4-homeassistant==0.5.2 From 357631d65939db4a36d868e7fc2fc9f4b5466876 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Mon, 22 Apr 2019 09:30:49 +0200 Subject: [PATCH 072/346] Add homematicip cloud temperature sensor from thermostats (#23263) --- .../components/homematicip_cloud/sensor.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 201a5be6c51..316bf1f4cd8 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -38,6 +38,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): devices.append(HomematicipHeatingThermostat(home, device)) + devices.append(HomematicipTemperatureSensor(home, device)) if isinstance(device, (AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorWithoutDisplay, AsyncTemperatureHumiditySensorOutdoor, @@ -46,15 +47,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): AsyncWeatherSensorPro)): devices.append(HomematicipTemperatureSensor(home, device)) devices.append(HomematicipHumiditySensor(home, device)) - if isinstance(device, (AsyncMotionDetectorIndoor, + if isinstance(device, (AsyncLightSensor, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): devices.append(HomematicipIlluminanceSensor(home, device)) - if isinstance(device, AsyncLightSensor): - devices.append(HomematicipLightSensor(home, device)) if isinstance(device, (AsyncPlugableSwitchMeasuring, AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring)): @@ -181,6 +180,9 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): @property def state(self): """Return the state.""" + if hasattr(self._device, 'valveActualTemperature'): + return self._device.valveActualTemperature + return self._device.actualTemperature @property @@ -213,6 +215,9 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): @property def state(self): """Return the state.""" + if hasattr(self._device, 'averageIllumination'): + return self._device.averageIllumination + return self._device.illumination @property @@ -221,15 +226,6 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): return 'lx' -class HomematicipLightSensor(HomematicipIlluminanceSensor): - """Represenation of a HomematicIP Illuminance device.""" - - @property - def state(self): - """Return the state.""" - return self._device.averageIllumination - - class HomematicipPowerSensor(HomematicipGenericDevice): """Represenation of a HomematicIP power measuring device.""" From a89c7f8feb6ac921793972900f652a3f9748d9e3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 22 Apr 2019 14:48:50 +0200 Subject: [PATCH 073/346] Improve MQTT tests (#23296) * Improve MQTT tests * Tweak --- .../mqtt/test_alarm_control_panel.py | 42 ++- tests/components/mqtt/test_binary_sensor.py | 56 ++-- tests/components/mqtt/test_climate.py | 205 +++++++------- tests/components/mqtt/test_cover.py | 265 ++++++++---------- tests/components/mqtt/test_fan.py | 28 +- tests/components/mqtt/test_init.py | 102 ++++--- tests/components/mqtt/test_light.py | 193 ++++++------- tests/components/mqtt/test_light_json.py | 154 +++++----- tests/components/mqtt/test_light_template.py | 94 +++---- tests/components/mqtt/test_lock.py | 8 +- tests/components/mqtt/test_sensor.py | 59 ++-- tests/components/mqtt/test_subscription.py | 44 +-- tests/components/mqtt/test_switch.py | 52 ++-- 13 files changed, 625 insertions(+), 677 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 882f748fe4c..4514e5285aa 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -53,14 +53,13 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): entity_id = 'alarm_control_panel.test' - assert STATE_UNKNOWN == \ - hass.states.get(entity_id).state + assert hass.states.get(entity_id).state == STATE_UNKNOWN for state in (STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED): async_fire_mqtt_message(hass, 'alarm/state', state) - assert state == hass.states.get(entity_id).state + assert hass.states.get(entity_id).state == state async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): @@ -76,11 +75,10 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): entity_id = 'alarm_control_panel.test' - assert STATE_UNKNOWN == \ - hass.states.get(entity_id).state + assert hass.states.get(entity_id).state == STATE_UNKNOWN async_fire_mqtt_message(hass, 'alarm/state', 'unsupported state') - assert STATE_UNKNOWN == hass.states.get(entity_id).state + assert hass.states.get(entity_id).state == STATE_UNKNOWN async def test_arm_home_publishes_mqtt(hass, mqtt_mock): @@ -120,7 +118,7 @@ async def test_arm_home_not_publishes_mqtt_with_invalid_code_when_req( call_count = mqtt_mock.async_publish.call_count common.async_alarm_arm_home(hass, 'abcd') await hass.async_block_till_done() - assert call_count == mqtt_mock.async_publish.call_count + assert mqtt_mock.async_publish.call_count == call_count async def test_arm_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): @@ -182,7 +180,7 @@ async def test_arm_away_not_publishes_mqtt_with_invalid_code_when_req( call_count = mqtt_mock.async_publish.call_count common.async_alarm_arm_away(hass, 'abcd') await hass.async_block_till_done() - assert call_count == mqtt_mock.async_publish.call_count + assert mqtt_mock.async_publish.call_count == call_count async def test_arm_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): @@ -244,7 +242,7 @@ async def test_arm_night_not_publishes_mqtt_with_invalid_code_when_req( call_count = mqtt_mock.async_publish.call_count common.async_alarm_arm_night(hass, 'abcd') await hass.async_block_till_done() - assert call_count == mqtt_mock.async_publish.call_count + assert mqtt_mock.async_publish.call_count == call_count async def test_arm_night_publishes_mqtt_when_code_not_req(hass, mqtt_mock): @@ -353,7 +351,7 @@ async def test_disarm_not_publishes_mqtt_with_invalid_code_when_req( call_count = mqtt_mock.async_publish.call_count common.async_alarm_disarm(hass, 'abcd') await hass.async_block_till_done() - assert call_count == mqtt_mock.async_publish.call_count + assert mqtt_mock.async_publish.call_count == call_count async def test_default_availability_payload(hass, mqtt_mock): @@ -370,17 +368,17 @@ async def test_default_availability_payload(hass, mqtt_mock): }) state = hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') state = hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') state = hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_custom_availability_payload(hass, mqtt_mock): @@ -399,17 +397,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): }) state = hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -427,7 +425,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('alarm_control_panel.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_state_via_state_topic_template(hass, mqtt_mock): @@ -448,12 +446,12 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): }) state = hass.states.get('alarm_control_panel.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, 'test-topic', '100') state = hass.states.get('alarm_control_panel.test') - assert STATE_ALARM_ARMED_AWAY == state.state + assert state.state == STATE_ALARM_ARMED_AWAY async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -513,7 +511,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('alarm_control_panel.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message( @@ -523,12 +521,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('alarm_control_panel.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('alarm_control_panel.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 2c8faf66549..70394a62f06 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -29,15 +29,15 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }) state = hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'test-topic', 'ON') state = hass.states.get('binary_sensor.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async_fire_mqtt_message(hass, 'test-topic', 'OFF') state = hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_setting_sensor_value_via_mqtt_message_and_template( @@ -56,15 +56,15 @@ async def test_setting_sensor_value_via_mqtt_message_and_template( }) state = hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'test-topic', '') state = hass.states.get('binary_sensor.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async_fire_mqtt_message(hass, 'test-topic', '') state = hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_valid_device_class(hass, mqtt_mock): @@ -79,7 +79,7 @@ async def test_valid_device_class(hass, mqtt_mock): }) state = hass.states.get('binary_sensor.test') - assert 'motion' == state.attributes.get('device_class') + assert state.attributes.get('device_class') == 'motion' async def test_invalid_device_class(hass, mqtt_mock): @@ -108,7 +108,7 @@ async def test_availability_without_topic(hass, mqtt_mock): }) state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async def test_availability_by_defaults(hass, mqtt_mock): @@ -123,17 +123,17 @@ async def test_availability_by_defaults(hass, mqtt_mock): }) state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_availability_by_custom_payload(hass, mqtt_mock): @@ -150,17 +150,17 @@ async def test_availability_by_custom_payload(hass, mqtt_mock): }) state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_force_update_disabled(hass, mqtt_mock): @@ -186,11 +186,11 @@ async def test_force_update_disabled(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', 'ON') await hass.async_block_till_done() - assert 1 == len(events) + assert len(events) == 1 async_fire_mqtt_message(hass, 'test-topic', 'ON') await hass.async_block_till_done() - assert 1 == len(events) + assert len(events) == 1 async def test_force_update_enabled(hass, mqtt_mock): @@ -217,11 +217,11 @@ async def test_force_update_enabled(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', 'ON') await hass.async_block_till_done() - assert 1 == len(events) + assert len(events) == 1 async_fire_mqtt_message(hass, 'test-topic', 'ON') await hass.async_block_till_done() - assert 2 == len(events) + assert len(events) == 2 async def test_off_delay(hass, mqtt_mock): @@ -250,20 +250,20 @@ async def test_off_delay(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', 'ON') await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') - assert STATE_ON == state.state - assert 1 == len(events) + assert state.state == STATE_ON + assert len(events) == 1 async_fire_mqtt_message(hass, 'test-topic', 'ON') await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') - assert STATE_ON == state.state - assert 2 == len(events) + assert state.state == STATE_ON + assert len(events) == 2 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() state = hass.states.get('binary_sensor.test') - assert STATE_OFF == state.state - assert 3 == len(events) + assert state.state == STATE_OFF + assert len(events) == 3 async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -280,7 +280,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('binary_sensor.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -338,7 +338,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('binary_sensor.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/binary_sensor/bla/config', @@ -348,12 +348,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('binary_sensor.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('binary_sensor.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 15321301997..11e2984cbb3 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -45,12 +45,12 @@ async def test_setup_params(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert 21 == state.attributes.get('temperature') - assert "low" == state.attributes.get('fan_mode') - assert "off" == state.attributes.get('swing_mode') - assert "off" == state.attributes.get('operation_mode') - assert DEFAULT_MIN_TEMP == state.attributes.get('min_temp') - assert DEFAULT_MAX_TEMP == state.attributes.get('max_temp') + assert state.attributes.get('temperature') == 21 + assert state.attributes.get('fan_mode') == 'low' + assert state.attributes.get('swing_mode') == 'off' + assert state.attributes.get('operation_mode') == 'off' + assert state.attributes.get('min_temp') == DEFAULT_MIN_TEMP + assert state.attributes.get('max_temp') == DEFAULT_MAX_TEMP async def test_supported_features(hass, mqtt_mock): @@ -87,16 +87,16 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state + assert state.attributes.get('operation_mode') == 'off' + assert state.state == 'off' common.async_set_operation_mode(hass, None, ENTITY_CLIMATE) await hass.async_block_till_done() assert ("string value is None for dictionary value @ " "data['operation_mode']")\ in caplog.text state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state + assert state.attributes.get('operation_mode') == 'off' + assert state.state == 'off' async def test_set_operation(hass, mqtt_mock): @@ -104,13 +104,13 @@ async def test_set_operation(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - common.async_set_operation_mode(hass, "cool", ENTITY_CLIMATE) + assert state.attributes.get('operation_mode') == 'off' + assert state.state == 'off' + common.async_set_operation_mode(hass, 'cool', ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - assert "cool" == state.state + assert state.attributes.get('operation_mode') == 'cool' + assert state.state == 'cool' mqtt_mock.async_publish.assert_called_once_with( 'mode-topic', 'cool', 0, False) @@ -123,23 +123,23 @@ async def test_set_operation_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') is None - assert "unknown" == state.state + assert state.state == 'unknown' - common.async_set_operation_mode(hass, "cool", ENTITY_CLIMATE) + common.async_set_operation_mode(hass, 'cool', ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') is None - assert "unknown" == state.state + assert state.state == 'unknown' async_fire_mqtt_message(hass, 'mode-state', 'cool') state = hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - assert "cool" == state.state + assert state.attributes.get('operation_mode') == 'cool' + assert state.state == 'cool' async_fire_mqtt_message(hass, 'mode-state', 'bogus mode') state = hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') - assert "cool" == state.state + assert state.attributes.get('operation_mode') == 'cool' + assert state.state == 'cool' async def test_set_operation_with_power_command(hass, mqtt_mock): @@ -149,24 +149,24 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state - common.async_set_operation_mode(hass, "on", ENTITY_CLIMATE) + assert state.attributes.get('operation_mode') == 'off' + assert state.state == 'off' + common.async_set_operation_mode(hass, 'on', ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('operation_mode') - assert "on" == state.state + assert state.attributes.get('operation_mode') == 'on' + assert state.state == 'on' mqtt_mock.async_publish.assert_has_calls([ unittest.mock.call('power-command', 'ON', 0, False), unittest.mock.call('mode-topic', 'on', 0, False) ]) mqtt_mock.async_publish.reset_mock() - common.async_set_operation_mode(hass, "off", ENTITY_CLIMATE) + common.async_set_operation_mode(hass, 'off', ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('operation_mode') - assert "off" == state.state + assert state.attributes.get('operation_mode') == 'off' + assert state.state == 'off' mqtt_mock.async_publish.assert_has_calls([ unittest.mock.call('power-command', 'OFF', 0, False), unittest.mock.call('mode-topic', 'off', 0, False) @@ -179,13 +179,13 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert "low" == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'low' common.async_set_fan_mode(hass, None, ENTITY_CLIMATE) await hass.async_block_till_done() assert "string value is None for dictionary value @ data['fan_mode']"\ in caplog.text state = hass.states.get(ENTITY_CLIMATE) - assert "low" == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'low' async def test_set_fan_mode_pessimistic(hass, mqtt_mock): @@ -204,11 +204,11 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'fan-state', 'high') state = hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'high' async_fire_mqtt_message(hass, 'fan-state', 'bogus mode') state = hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'high' async def test_set_fan_mode(hass, mqtt_mock): @@ -216,13 +216,13 @@ async def test_set_fan_mode(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert "low" == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'low' common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'fan-mode-topic', 'high', 0, False) state = hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'high' async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): @@ -230,13 +230,13 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'off' common.async_set_swing_mode(hass, None, ENTITY_CLIMATE) await hass.async_block_till_done() assert "string value is None for dictionary value @ data['swing_mode']"\ in caplog.text state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'off' async def test_set_swing_pessimistic(hass, mqtt_mock): @@ -255,11 +255,11 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'swing-state', 'on') state = hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'on' async_fire_mqtt_message(hass, 'swing-state', 'bogus state') state = hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'on' async def test_set_swing(hass, mqtt_mock): @@ -267,13 +267,13 @@ async def test_set_swing(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert "off" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'off' common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'swing-mode-topic', 'on', 0, False) state = hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'on' async def test_set_target_temperature(hass, mqtt_mock): @@ -281,11 +281,11 @@ async def test_set_target_temperature(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert 21 == state.attributes.get('temperature') + assert state.attributes.get('temperature') == 21 common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert 'heat' == state.attributes.get('operation_mode') + assert state.attributes.get('operation_mode') == 'heat' mqtt_mock.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) mqtt_mock.async_publish.reset_mock() @@ -293,19 +293,19 @@ async def test_set_target_temperature(hass, mqtt_mock): entity_id=ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert 47 == state.attributes.get('temperature') + assert state.attributes.get('temperature') == 47 mqtt_mock.async_publish.assert_called_once_with( 'temperature-topic', 47, 0, False) # also test directly supplying the operation mode to set_temperature mqtt_mock.async_publish.reset_mock() common.async_set_temperature(hass, temperature=21, - operation_mode="cool", + operation_mode='cool', entity_id=ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert 'cool' == state.attributes.get('operation_mode') - assert 21 == state.attributes.get('temperature') + assert state.attributes.get('operation_mode') == 'cool' + assert state.attributes.get('temperature') == 21 mqtt_mock.async_publish.assert_has_calls([ unittest.mock.call('mode-topic', 'cool', 0, False), unittest.mock.call('temperature-topic', 21, 0, False) @@ -331,11 +331,11 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'temperature-state', '1701') state = hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('temperature') + assert state.attributes.get('temperature') == 1701 async_fire_mqtt_message(hass, 'temperature-state', 'not a number') state = hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('temperature') + assert state.attributes.get('temperature') == 1701 async def test_set_target_temperature_low_high(hass, mqtt_mock): @@ -347,9 +347,8 @@ async def test_set_target_temperature_low_high(hass, mqtt_mock): entity_id=ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - print(state.attributes) - assert 20 == state.attributes.get('target_temp_low') - assert 23 == state.attributes.get('target_temp_high') + assert state.attributes.get('target_temp_low') == 20 + assert state.attributes.get('target_temp_high') == 23 mqtt_mock.async_publish.assert_any_call( 'temperature-low-topic', 20, 0, False) mqtt_mock.async_publish.assert_any_call( @@ -378,21 +377,21 @@ async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'temperature-low-state', '1701') state = hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('target_temp_low') + assert state.attributes.get('target_temp_low') == 1701 assert state.attributes.get('target_temp_high') is None async_fire_mqtt_message(hass, 'temperature-high-state', '1703') state = hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('target_temp_low') - assert 1703 == state.attributes.get('target_temp_high') + assert state.attributes.get('target_temp_low') == 1701 + assert state.attributes.get('target_temp_high') == 1703 async_fire_mqtt_message(hass, 'temperature-low-state', 'not a number') state = hass.states.get(ENTITY_CLIMATE) - assert 1701 == state.attributes.get('target_temp_low') + assert state.attributes.get('target_temp_low') == 1701 async_fire_mqtt_message(hass, 'temperature-high-state', 'not a number') state = hass.states.get(ENTITY_CLIMATE) - assert 1703 == state.attributes.get('target_temp_high') + assert state.attributes.get('target_temp_high') == 1703 async def test_receive_mqtt_temperature(hass, mqtt_mock): @@ -403,7 +402,7 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock): async_fire_mqtt_message(hass, 'current_temperature', '47') state = hass.states.get(ENTITY_CLIMATE) - assert 47 == state.attributes.get('current_temperature') + assert state.attributes.get('current_temperature') == 47 async def test_set_away_mode_pessimistic(hass, mqtt_mock): @@ -413,24 +412,24 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' common.async_set_away_mode(hass, True, ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' async_fire_mqtt_message(hass, 'away-state', 'ON') state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'on' async_fire_mqtt_message(hass, 'away-state', 'OFF') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' async_fire_mqtt_message(hass, 'away-state', 'nonsense') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' async def test_set_away_mode(hass, mqtt_mock): @@ -442,21 +441,21 @@ async def test_set_away_mode(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' common.async_set_away_mode(hass, True, ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'away-mode-topic', 'AN', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'on' common.async_set_away_mode(hass, False, ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'away-mode-topic', 'AUS', 0, False) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' async def test_set_hold_pessimistic(hass, mqtt_mock): @@ -475,11 +474,11 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'hold-state', 'on') state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('hold_mode') + assert state.attributes.get('hold_mode') == 'on' async_fire_mqtt_message(hass, 'hold-state', 'off') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('hold_mode') + assert state.attributes.get('hold_mode') == 'off' async def test_set_hold(hass, mqtt_mock): @@ -494,14 +493,14 @@ async def test_set_hold(hass, mqtt_mock): 'hold-topic', 'on', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('hold_mode') + assert state.attributes.get('hold_mode') == 'on' common.async_set_hold_mode(hass, 'off', ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'hold-topic', 'off', 0, False) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('hold_mode') + assert state.attributes.get('hold_mode') == 'off' async def test_set_aux_pessimistic(hass, mqtt_mock): @@ -511,24 +510,24 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) await hass.async_block_till_done() state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' async_fire_mqtt_message(hass, 'aux-state', 'ON') state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'on' async_fire_mqtt_message(hass, 'aux-state', 'OFF') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' async_fire_mqtt_message(hass, 'aux-state', 'nonsense') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' async def test_set_aux(hass, mqtt_mock): @@ -536,21 +535,21 @@ async def test_set_aux(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'aux-topic', 'ON', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'on' common.async_set_aux_heat(hass, False, ENTITY_CLIMATE) await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( 'aux-topic', 'OFF', 0, False) state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' async def test_custom_availability_payload(hass, mqtt_mock): @@ -563,17 +562,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) state = hass.states.get('climate.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('climate.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('climate.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_set_with_templates(hass, mqtt_mock, caplog): @@ -604,25 +603,25 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): assert state.attributes.get('operation_mode') is None async_fire_mqtt_message(hass, 'mode-state', '"cool"') state = hass.states.get(ENTITY_CLIMATE) - assert "cool" == state.attributes.get('operation_mode') + assert state.attributes.get('operation_mode') == 'cool' # Fan Mode assert state.attributes.get('fan_mode') is None async_fire_mqtt_message(hass, 'fan-state', '"high"') state = hass.states.get(ENTITY_CLIMATE) - assert 'high' == state.attributes.get('fan_mode') + assert state.attributes.get('fan_mode') == 'high' # Swing Mode assert state.attributes.get('swing_mode') is None async_fire_mqtt_message(hass, 'swing-state', '"on"') state = hass.states.get(ENTITY_CLIMATE) - assert "on" == state.attributes.get('swing_mode') + assert state.attributes.get('swing_mode') == 'on' # Temperature - with valid value assert state.attributes.get('temperature') is None async_fire_mqtt_message(hass, 'temperature-state', '"1031"') state = hass.states.get(ENTITY_CLIMATE) - assert 1031 == state.attributes.get('temperature') + assert state.attributes.get('temperature') == 1031 # Temperature - with invalid value async_fire_mqtt_message(hass, 'temperature-state', '"-INVALID-"') @@ -630,22 +629,22 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): # make sure, the invalid value gets logged... assert "Could not parse temperature from -INVALID-" in caplog.text # ... but the actual value stays unchanged. - assert 1031 == state.attributes.get('temperature') + assert state.attributes.get('temperature') == 1031 # Away Mode - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' async_fire_mqtt_message(hass, 'away-state', '"ON"') state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'on' # Away Mode with JSON values async_fire_mqtt_message(hass, 'away-state', 'false') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'off' async_fire_mqtt_message(hass, 'away-state', 'true') state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('away_mode') + assert state.attributes.get('away_mode') == 'on' # Hold Mode assert state.attributes.get('hold_mode') is None @@ -653,23 +652,23 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): { "attribute": "somemode" } """) state = hass.states.get(ENTITY_CLIMATE) - assert 'somemode' == state.attributes.get('hold_mode') + assert state.attributes.get('hold_mode') == 'somemode' # Aux mode - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' async_fire_mqtt_message(hass, 'aux-state', 'switchmeon') state = hass.states.get(ENTITY_CLIMATE) - assert 'on' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'on' # anything other than 'switchmeon' should turn Aux mode off async_fire_mqtt_message(hass, 'aux-state', 'somerandomstring') state = hass.states.get(ENTITY_CLIMATE) - assert 'off' == state.attributes.get('aux_heat') + assert state.attributes.get('aux_heat') == 'off' # Current temperature async_fire_mqtt_message(hass, 'current-temperature', '"74656"') state = hass.states.get(ENTITY_CLIMATE) - assert 74656 == state.attributes.get('current_temperature') + assert state.attributes.get('current_temperature') == 74656 async def test_min_temp_custom(hass, mqtt_mock): @@ -683,7 +682,7 @@ async def test_min_temp_custom(hass, mqtt_mock): min_temp = state.attributes.get('min_temp') assert isinstance(min_temp, float) - assert 26 == state.attributes.get('min_temp') + assert state.attributes.get('min_temp') == 26 async def test_max_temp_custom(hass, mqtt_mock): @@ -697,7 +696,7 @@ async def test_max_temp_custom(hass, mqtt_mock): max_temp = state.attributes.get('max_temp') assert isinstance(max_temp, float) - assert 60 == max_temp + assert max_temp == 60 async def test_temp_step_custom(hass, mqtt_mock): @@ -711,7 +710,7 @@ async def test_temp_step_custom(hass, mqtt_mock): temp_step = state.attributes.get('target_temp_step') assert isinstance(temp_step, float) - assert 0.01 == temp_step + assert temp_step == 0.01 async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -729,7 +728,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('climate.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -791,7 +790,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('climate.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/climate/bla/config', @@ -801,12 +800,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('climate.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('climate.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 5ca8a1aa649..8bf136c6f0f 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -35,18 +35,18 @@ async def test_state_via_state_topic(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', STATE_CLOSED) state = hass.states.get('cover.test') - assert STATE_CLOSED == state.state + assert state.state == STATE_CLOSED async_fire_mqtt_message(hass, 'state-topic', STATE_OPEN) state = hass.states.get('cover.test') - assert STATE_OPEN == state.state + assert state.state == STATE_OPEN async def test_position_via_position_topic(hass, mqtt_mock): @@ -67,18 +67,18 @@ async def test_position_via_position_topic(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'get-position-topic', '0') state = hass.states.get('cover.test') - assert STATE_CLOSED == state.state + assert state.state == STATE_CLOSED async_fire_mqtt_message(hass, 'get-position-topic', '100') state = hass.states.get('cover.test') - assert STATE_OPEN == state.state + assert state.state == STATE_OPEN async def test_state_via_template(hass, mqtt_mock): @@ -100,17 +100,17 @@ async def test_state_via_template(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, 'state-topic', '10000') state = hass.states.get('cover.test') - assert STATE_OPEN == state.state + assert state.state == STATE_OPEN async_fire_mqtt_message(hass, 'state-topic', '99') state = hass.states.get('cover.test') - assert STATE_CLOSED == state.state + assert state.state == STATE_CLOSED async def test_position_via_template(hass, mqtt_mock): @@ -127,22 +127,22 @@ async def test_position_via_template(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN async_fire_mqtt_message(hass, 'get-position-topic', '10000') state = hass.states.get('cover.test') - assert STATE_OPEN == state.state + assert state.state == STATE_OPEN async_fire_mqtt_message(hass, 'get-position-topic', '5000') state = hass.states.get('cover.test') - assert STATE_OPEN == state.state + assert state.state == STATE_OPEN async_fire_mqtt_message(hass, 'get-position-topic', '99') state = hass.states.get('cover.test') - assert STATE_CLOSED == state.state + assert state.state == STATE_CLOSED async def test_optimistic_state_change(hass, mqtt_mock): @@ -157,31 +157,27 @@ async def test_optimistic_state_change(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'OPEN', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('cover.test') - assert STATE_OPEN == state.state + assert state.state == STATE_OPEN - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'CLOSE', 0, False) state = hass.states.get('cover.test') - assert STATE_CLOSED == state.state + assert state.state == STATE_CLOSED async def test_send_open_cover_command(hass, mqtt_mock): @@ -197,18 +193,16 @@ async def test_send_open_cover_command(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'OPEN', 2, False) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN async def test_send_close_cover_command(hass, mqtt_mock): @@ -224,18 +218,16 @@ async def test_send_close_cover_command(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'CLOSE', 2, False) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN async def test_send_stop__cover_command(hass, mqtt_mock): @@ -251,18 +243,16 @@ async def test_send_stop__cover_command(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'STOP', 2, False) state = hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state + assert state.state == STATE_UNKNOWN async def test_current_cover_position(hass, mqtt_mock): @@ -291,22 +281,22 @@ async def test_current_cover_position(hass, mqtt_mock): async_fire_mqtt_message(hass, 'get-position-topic', '0') current_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 0 == current_cover_position + assert current_cover_position == 0 async_fire_mqtt_message(hass, 'get-position-topic', '50') current_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 50 == current_cover_position + assert current_cover_position == 50 async_fire_mqtt_message(hass, 'get-position-topic', 'non-numeric') current_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 50 == current_cover_position + assert current_cover_position == 50 async_fire_mqtt_message(hass, 'get-position-topic', '101') current_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 100 == current_cover_position + assert current_cover_position == 100 async def test_current_cover_position_inverted(hass, mqtt_mock): @@ -335,37 +325,32 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): async_fire_mqtt_message(hass, 'get-position-topic', '100') current_percentage_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 0 == current_percentage_cover_position - assert STATE_CLOSED == hass.states.get( - 'cover.test').state + assert current_percentage_cover_position == 0 + assert hass.states.get('cover.test').state == STATE_CLOSED async_fire_mqtt_message(hass, 'get-position-topic', '0') current_percentage_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 100 == current_percentage_cover_position - assert STATE_OPEN == hass.states.get( - 'cover.test').state + assert current_percentage_cover_position == 100 + assert hass.states.get('cover.test').state == STATE_OPEN async_fire_mqtt_message(hass, 'get-position-topic', '50') current_percentage_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 50 == current_percentage_cover_position - assert STATE_OPEN == hass.states.get( - 'cover.test').state + assert current_percentage_cover_position == 50 + assert hass.states.get('cover.test').state == STATE_OPEN async_fire_mqtt_message(hass, 'get-position-topic', 'non-numeric') current_percentage_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 50 == current_percentage_cover_position - assert STATE_OPEN == hass.states.get( - 'cover.test').state + assert current_percentage_cover_position == 50 + assert hass.states.get('cover.test').state == STATE_OPEN async_fire_mqtt_message(hass, 'get-position-topic', '101') current_percentage_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 0 == current_percentage_cover_position - assert STATE_CLOSED == hass.states.get( - 'cover.test').state + assert current_percentage_cover_position == 0 + assert hass.states.get('cover.test').state == STATE_CLOSED async def test_set_cover_position(hass, mqtt_mock): @@ -399,7 +384,7 @@ async def test_set_cover_position(hass, mqtt_mock): assert not ('current_tilt_position' in state_attributes_dict) current_cover_position = hass.states.get( 'cover.test').attributes['current_position'] - assert 22 == current_cover_position + assert current_cover_position == 22 async def test_set_position_templated(hass, mqtt_mock): @@ -420,11 +405,10 @@ async def test_set_position_templated(hass, mqtt_mock): } }) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'set-position-topic', '38', 0, False) @@ -445,11 +429,10 @@ async def test_set_position_untemplated(hass, mqtt_mock): } }) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'position-topic', 62, 0, False) @@ -470,8 +453,8 @@ async def test_no_command_topic(hass, mqtt_mock): } }) - assert 240 == hass.states.get( - 'cover.test').attributes['supported_features'] + assert hass.states.get( + 'cover.test').attributes['supported_features'] == 240 async def test_with_command_topic_and_tilt(hass, mqtt_mock): @@ -490,8 +473,8 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock): } }) - assert 251 == hass.states.get( - 'cover.test').attributes['supported_features'] + assert hass.states.get( + 'cover.test').attributes['supported_features'] == 251 async def test_tilt_defaults(hass, mqtt_mock): @@ -517,7 +500,7 @@ async def test_tilt_defaults(hass, mqtt_mock): current_cover_position = hass.states.get( 'cover.test').attributes['current_tilt_position'] - assert STATE_UNKNOWN == current_cover_position + assert current_cover_position == STATE_UNKNOWN async def test_tilt_via_invocation_defaults(hass, mqtt_mock): @@ -537,21 +520,17 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): } }) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'tilt-command-topic', 100, 0, False) mqtt_mock.async_publish.reset_mock() - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'tilt-command-topic', 0, 0, False) @@ -576,21 +555,17 @@ async def test_tilt_given_value(hass, mqtt_mock): } }) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_OPEN_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'tilt-command-topic', 400, 0, False) mqtt_mock.async_publish.reset_mock() - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, - {ATTR_ENTITY_ID: 'cover.test'})) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: 'cover.test'}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'tilt-command-topic', 125, 0, False) @@ -619,13 +594,13 @@ async def test_tilt_via_topic(hass, mqtt_mock): current_cover_tilt_position = hass.states.get( 'cover.test').attributes['current_tilt_position'] - assert 0 == current_cover_tilt_position + assert current_cover_tilt_position == 0 async_fire_mqtt_message(hass, 'tilt-status-topic', '50') current_cover_tilt_position = hass.states.get( 'cover.test').attributes['current_tilt_position'] - assert 50 == current_cover_tilt_position + assert current_cover_tilt_position == 50 async def test_tilt_via_topic_altered_range(hass, mqtt_mock): @@ -653,19 +628,19 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): current_cover_tilt_position = hass.states.get( 'cover.test').attributes['current_tilt_position'] - assert 0 == current_cover_tilt_position + assert current_cover_tilt_position == 0 async_fire_mqtt_message(hass, 'tilt-status-topic', '50') current_cover_tilt_position = hass.states.get( 'cover.test').attributes['current_tilt_position'] - assert 100 == current_cover_tilt_position + assert current_cover_tilt_position == 100 async_fire_mqtt_message(hass, 'tilt-status-topic', '25') current_cover_tilt_position = hass.states.get( 'cover.test').attributes['current_tilt_position'] - assert 50 == current_cover_tilt_position + assert current_cover_tilt_position == 50 async def test_tilt_position(hass, mqtt_mock): @@ -687,12 +662,10 @@ async def test_tilt_position(hass, mqtt_mock): } }) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, - blocking=True)) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'tilt-command-topic', 50, 0, False) @@ -719,12 +692,10 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): } }) - hass.async_add_job( - hass.services.async_call( - cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, - {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, - blocking=True)) - await hass.async_block_till_done() + await hass.services.async_call( + cover.DOMAIN, SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: 'cover.test', ATTR_TILT_POSITION: 50}, + blocking=True) mqtt_mock.async_publish.assert_called_once_with( 'tilt-command-topic', 25, 0, False) @@ -758,8 +729,8 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): None, None) - assert 44 == mqtt_cover.find_percentage_in_range(44) - assert 44 == mqtt_cover.find_percentage_in_range(44, 'cover') + assert mqtt_cover.find_percentage_in_range(44) == 44 + assert mqtt_cover.find_percentage_in_range(44, 'cover') == 44 async def test_find_percentage_in_range_altered(hass, mqtt_mock): @@ -790,8 +761,8 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): None, None) - assert 40 == mqtt_cover.find_percentage_in_range(120) - assert 40 == mqtt_cover.find_percentage_in_range(120, 'cover') + assert mqtt_cover.find_percentage_in_range(120) == 40 + assert mqtt_cover.find_percentage_in_range(120, 'cover') == 40 async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): @@ -822,8 +793,8 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): None, None) - assert 56 == mqtt_cover.find_percentage_in_range(44) - assert 56 == mqtt_cover.find_percentage_in_range(44, 'cover') + assert mqtt_cover.find_percentage_in_range(44) == 56 + assert mqtt_cover.find_percentage_in_range(44, 'cover') == 56 async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): @@ -854,8 +825,8 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): None, None) - assert 60 == mqtt_cover.find_percentage_in_range(120) - assert 60 == mqtt_cover.find_percentage_in_range(120, 'cover') + assert mqtt_cover.find_percentage_in_range(120) == 60 + assert mqtt_cover.find_percentage_in_range(120, 'cover') == 60 async def test_find_in_range_defaults(hass, mqtt_mock): @@ -886,8 +857,8 @@ async def test_find_in_range_defaults(hass, mqtt_mock): None, None) - assert 44 == mqtt_cover.find_in_range_from_percent(44) - assert 44 == mqtt_cover.find_in_range_from_percent(44, 'cover') + assert mqtt_cover.find_in_range_from_percent(44) == 44 + assert mqtt_cover.find_in_range_from_percent(44, 'cover') == 44 async def test_find_in_range_altered(hass, mqtt_mock): @@ -918,8 +889,8 @@ async def test_find_in_range_altered(hass, mqtt_mock): None, None) - assert 120 == mqtt_cover.find_in_range_from_percent(40) - assert 120 == mqtt_cover.find_in_range_from_percent(40, 'cover') + assert mqtt_cover.find_in_range_from_percent(40) == 120 + assert mqtt_cover.find_in_range_from_percent(40, 'cover') == 120 async def test_find_in_range_defaults_inverted(hass, mqtt_mock): @@ -950,8 +921,8 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): None, None) - assert 44 == mqtt_cover.find_in_range_from_percent(56) - assert 44 == mqtt_cover.find_in_range_from_percent(56, 'cover') + assert mqtt_cover.find_in_range_from_percent(56) == 44 + assert mqtt_cover.find_in_range_from_percent(56, 'cover') == 44 async def test_find_in_range_altered_inverted(hass, mqtt_mock): @@ -982,8 +953,8 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): None, None) - assert 120 == mqtt_cover.find_in_range_from_percent(60) - assert 120 == mqtt_cover.find_in_range_from_percent(60, 'cover') + assert mqtt_cover.find_in_range_from_percent(60) == 120 + assert mqtt_cover.find_in_range_from_percent(60, 'cover') == 120 async def test_availability_without_topic(hass, mqtt_mock): @@ -998,7 +969,7 @@ async def test_availability_without_topic(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async def test_availability_by_defaults(hass, mqtt_mock): @@ -1014,19 +985,19 @@ async def test_availability_by_defaults(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') await hass.async_block_till_done() state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') await hass.async_block_till_done() state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_availability_by_custom_payload(hass, mqtt_mock): @@ -1044,19 +1015,19 @@ async def test_availability_by_custom_payload(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') await hass.async_block_till_done() state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') await hass.async_block_till_done() state = hass.states.get('cover.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_valid_device_class(hass, mqtt_mock): @@ -1071,7 +1042,7 @@ async def test_valid_device_class(hass, mqtt_mock): }) state = hass.states.get('cover.test') - assert 'garage' == state.attributes.get('device_class') + assert state.attributes.get('device_class') == 'garage' async def test_invalid_device_class(hass, mqtt_mock): @@ -1103,7 +1074,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('cover.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -1161,7 +1132,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('cover.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/cover/bla/config', @@ -1171,12 +1142,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('cover.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('cover.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_discovery_removal_cover(hass, mqtt_mock, caplog): diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index bd19ec526a3..31aebecc236 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -69,23 +69,23 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): state = hass.states.get('fan.test') assert state.attributes.get('oscillating') is False - assert fan.SPEED_OFF == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_OFF async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_lOw') state = hass.states.get('fan.test') - assert fan.SPEED_LOW == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_LOW async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_mEdium') state = hass.states.get('fan.test') - assert fan.SPEED_MEDIUM == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_MEDIUM async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_High') state = hass.states.get('fan.test') - assert fan.SPEED_HIGH == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_HIGH async_fire_mqtt_message(hass, 'speed-state-topic', 'speed_OfF') state = hass.states.get('fan.test') - assert fan.SPEED_OFF == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_OFF async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): @@ -129,23 +129,23 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): state = hass.states.get('fan.test') assert state.attributes.get('oscillating') is False - assert fan.SPEED_OFF == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_OFF async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"low"}') state = hass.states.get('fan.test') - assert fan.SPEED_LOW == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_LOW async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"medium"}') state = hass.states.get('fan.test') - assert fan.SPEED_MEDIUM == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_MEDIUM async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"high"}') state = hass.states.get('fan.test') - assert fan.SPEED_HIGH == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_HIGH async_fire_mqtt_message(hass, 'speed-state-topic', '{"val":"off"}') state = hass.states.get('fan.test') - assert fan.SPEED_OFF == state.attributes.get('speed') + assert state.attributes.get('speed') == fan.SPEED_OFF async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): @@ -511,7 +511,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('fan.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -569,7 +569,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('fan.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/fan/bla/config', @@ -579,12 +579,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('fan.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('fan.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index dc9299e4a35..b0d1de36efe 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -82,11 +82,11 @@ class TestMQTTComponent(unittest.TestCase): self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'test-topic' == \ - self.calls[0][0].data['service_data'][mqtt.ATTR_TOPIC] - assert 'test-payload' == \ - self.calls[0][0].data['service_data'][mqtt.ATTR_PAYLOAD] + assert len(self.calls) == 1 + assert self.calls[0][0].data['service_data'][mqtt.ATTR_TOPIC] == \ + 'test-topic' + assert self.calls[0][0].data['service_data'][mqtt.ATTR_PAYLOAD] == \ + 'test-payload' def test_service_call_without_topic_does_not_publish(self): """Test the service call if topic is missing.""" @@ -105,7 +105,7 @@ class TestMQTTComponent(unittest.TestCase): mqtt.publish_template(self.hass, "test/topic", "{{ 1+1 }}") self.hass.block_till_done() assert self.hass.data['mqtt'].async_publish.called - assert self.hass.data['mqtt'].async_publish.call_args[0][1] == "2" + assert self.hass.data['mqtt'].async_publish.call_args[0][1] == '2' def test_service_call_with_payload_doesnt_render_template(self): """Test the service call with unrendered template. @@ -307,7 +307,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic', '°C') self.hass.block_till_done() - assert 1 == len(self.calls) + assert len(self.calls) == 1 def test_subscribe_topic(self): """Test the subscription of a topic.""" @@ -316,16 +316,16 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'test-topic' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == 'test-topic' + assert self.calls[0][0].payload == 'test-payload' unsub() fire_mqtt_message(self.hass, 'test-topic', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) + assert len(self.calls) == 1 def test_subscribe_topic_not_match(self): """Test if subscribed topic is not a match.""" @@ -334,7 +334,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'another-test-topic', 'test-payload') self.hass.block_till_done() - assert 0 == len(self.calls) + assert len(self.calls) == 0 def test_subscribe_topic_level_wildcard(self): """Test the subscription of wildcard topics.""" @@ -343,9 +343,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic/bier/on', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'test-topic/bier/on' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == 'test-topic/bier/on' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_level_wildcard_no_subtree_match(self): """Test the subscription of wildcard topics.""" @@ -354,7 +354,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic/bier', 'test-payload') self.hass.block_till_done() - assert 0 == len(self.calls) + assert len(self.calls) == 0 def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match(self): """Test the subscription of wildcard topics.""" @@ -363,7 +363,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic-123', 'test-payload') self.hass.block_till_done() - assert 0 == len(self.calls) + assert len(self.calls) == 0 def test_subscribe_topic_subtree_wildcard_subtree_topic(self): """Test the subscription of wildcard topics.""" @@ -372,9 +372,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic/bier/on', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'test-topic/bier/on' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == 'test-topic/bier/on' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_subtree_wildcard_root_topic(self): """Test the subscription of wildcard topics.""" @@ -383,9 +383,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'test-topic', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'test-topic' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == 'test-topic' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_subtree_wildcard_no_match(self): """Test the subscription of wildcard topics.""" @@ -394,7 +394,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'another-test-topic', 'test-payload') self.hass.block_till_done() - assert 0 == len(self.calls) + assert len(self.calls) == 0 def test_subscribe_topic_level_wildcard_and_wildcard_root_topic(self): """Test the subscription of wildcard topics.""" @@ -403,9 +403,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'hi/test-topic', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'hi/test-topic' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == 'hi/test-topic' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic(self): """Test the subscription of wildcard topics.""" @@ -414,9 +414,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'hi/test-topic/here-iam', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert 'hi/test-topic/here-iam' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == 'hi/test-topic/here-iam' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match(self): """Test the subscription of wildcard topics.""" @@ -425,7 +425,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'hi/here-iam/test-topic', 'test-payload') self.hass.block_till_done() - assert 0 == len(self.calls) + assert len(self.calls) == 0 def test_subscribe_topic_level_wildcard_and_wildcard_no_match(self): """Test the subscription of wildcard topics.""" @@ -434,7 +434,7 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, 'hi/another-test-topic', 'test-payload') self.hass.block_till_done() - assert 0 == len(self.calls) + assert len(self.calls) == 0 def test_subscribe_topic_sys_root(self): """Test the subscription of $ root topics.""" @@ -443,9 +443,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, '$test-topic/subtree/on', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert '$test-topic/subtree/on' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == '$test-topic/subtree/on' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_sys_root_and_wildcard_topic(self): """Test the subscription of $ root and wildcard topics.""" @@ -454,9 +454,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, '$test-topic/some-topic', 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert '$test-topic/some-topic' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == '$test-topic/some-topic' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_topic_sys_root_and_wildcard_subtree_topic(self): """Test the subscription of $ root and wildcard subtree topics.""" @@ -466,9 +466,9 @@ class TestMQTTCallbacks(unittest.TestCase): 'test-payload') self.hass.block_till_done() - assert 1 == len(self.calls) - assert '$test-topic/subtree/some-topic' == self.calls[0][0].topic - assert 'test-payload' == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == '$test-topic/subtree/some-topic' + assert self.calls[0][0].payload == 'test-payload' def test_subscribe_special_characters(self): """Test the subscription to topics with special characters.""" @@ -479,9 +479,9 @@ class TestMQTTCallbacks(unittest.TestCase): fire_mqtt_message(self.hass, topic, payload) self.hass.block_till_done() - assert 1 == len(self.calls) - assert topic == self.calls[0][0].topic - assert payload == self.calls[0][0].payload + assert len(self.calls) == 1 + assert self.calls[0][0].topic == topic + assert self.calls[0][0].payload == payload def test_mqtt_failed_connection_results_in_disconnect(self): """Test if connection failure leads to disconnect.""" @@ -507,9 +507,8 @@ class TestMQTTCallbacks(unittest.TestCase): self.hass.data['mqtt']._mqttc.reconnect.side_effect = [1, 1, 1, 0] self.hass.data['mqtt']._mqtt_on_disconnect(None, None, 1) assert self.hass.data['mqtt']._mqttc.reconnect.called - assert 4 == len(self.hass.data['mqtt']._mqttc.reconnect.mock_calls) - assert [1, 2, 4] == \ - [call[1][0] for call in mock_sleep.mock_calls] + assert len(self.hass.data['mqtt']._mqttc.reconnect.mock_calls) == 4 + assert [call[1][0] for call in mock_sleep.mock_calls] == [1, 2, 4] def test_retained_message_on_subscribe_received(self): """Test every subscriber receives retained message on subscribe.""" @@ -567,21 +566,18 @@ class TestMQTTCallbacks(unittest.TestCase): mock.call('test/state', 0), mock.call('test/state', 1) ] - assert self.hass.data['mqtt']._mqttc.subscribe.mock_calls == \ - expected + assert self.hass.data['mqtt']._mqttc.subscribe.mock_calls == expected unsub() self.hass.block_till_done() - assert self.hass.data['mqtt']._mqttc.unsubscribe.call_count == \ - 0 + assert self.hass.data['mqtt']._mqttc.unsubscribe.call_count == 0 self.hass.data['mqtt']._mqtt_on_disconnect(None, None, 0) self.hass.data['mqtt']._mqtt_on_connect(None, None, None, 0) self.hass.block_till_done() expected.append(mock.call('test/state', 1)) - assert self.hass.data['mqtt']._mqttc.subscribe.mock_calls == \ - expected + assert self.hass.data['mqtt']._mqttc.subscribe.mock_calls == expected @asyncio.coroutine diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 7b0157aeb7e..75fd92dddc0 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -194,7 +194,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -205,7 +205,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -243,7 +243,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -256,42 +256,40 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb/status', '1') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 255, 255) == state.attributes.get('rgb_color') - assert 255 == state.attributes.get('brightness') - assert 150 == state.attributes.get('color_temp') - assert 'none' == state.attributes.get('effect') - assert (0, 0) == state.attributes.get('hs_color') - assert 255 == state.attributes.get('white_value') - assert (0.323, 0.329) == state.attributes.get('xy_color') + assert state.state == STATE_ON + assert state.attributes.get('rgb_color') == (255, 255, 255) + assert state.attributes.get('brightness') == 255 + assert state.attributes.get('color_temp') == 150 + assert state.attributes.get('effect') == 'none' + assert state.attributes.get('hs_color') == (0, 0) + assert state.attributes.get('white_value') == 255 + assert state.attributes.get('xy_color') == (0.323, 0.329) async_fire_mqtt_message(hass, 'test_light_rgb/status', '0') state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'test_light_rgb/status', '1') async_fire_mqtt_message(hass, 'test_light_rgb/brightness/status', '100') light_state = hass.states.get('light.test') - assert 100 == \ - light_state.attributes['brightness'] + assert light_state.attributes['brightness'] == 100 async_fire_mqtt_message(hass, 'test_light_rgb/color_temp/status', '300') light_state = hass.states.get('light.test') - assert 300 == light_state.attributes['color_temp'] + assert light_state.attributes['color_temp'] == 300 async_fire_mqtt_message(hass, 'test_light_rgb/effect/status', 'rainbow') light_state = hass.states.get('light.test') - assert 'rainbow' == light_state.attributes['effect'] + assert light_state.attributes['effect'] == 'rainbow' async_fire_mqtt_message(hass, 'test_light_rgb/white_value/status', '100') light_state = hass.states.get('light.test') - assert 100 == \ - light_state.attributes['white_value'] + assert light_state.attributes['white_value'] == 100 async_fire_mqtt_message(hass, 'test_light_rgb/status', '1') @@ -299,22 +297,19 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): '125,125,125') light_state = hass.states.get('light.test') - assert (255, 255, 255) == \ - light_state.attributes.get('rgb_color') + assert light_state.attributes.get('rgb_color') == (255, 255, 255) async_fire_mqtt_message(hass, 'test_light_rgb/hs/status', '200,50') light_state = hass.states.get('light.test') - assert (200, 50) == \ - light_state.attributes.get('hs_color') + assert light_state.attributes.get('hs_color') == (200, 50) async_fire_mqtt_message(hass, 'test_light_rgb/xy/status', '0.675,0.322') light_state = hass.states.get('light.test') - assert (0.672, 0.324) == \ - light_state.attributes.get('xy_color') + assert light_state.attributes.get('xy_color') == (0.672, 0.324) async def test_brightness_controlling_scale(hass, mqtt_mock): @@ -336,28 +331,27 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('brightness') is None assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'test_scale/status', 'on') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('brightness') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 255 async_fire_mqtt_message(hass, 'test_scale/status', 'off') state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'test_scale/status', 'on') async_fire_mqtt_message(hass, 'test_scale/brightness/status', '99') light_state = hass.states.get('light.test') - assert 255 == \ - light_state.attributes['brightness'] + assert light_state.attributes['brightness'] == 255 async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): @@ -378,7 +372,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('brightness') is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -386,12 +380,12 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_scale_rgb/rgb/status', '255,0,0') state = hass.states.get('light.test') - assert 255 == state.attributes.get('brightness') + assert state.attributes.get('brightness') == 255 async_fire_mqtt_message(hass, 'test_scale_rgb/rgb/status', '127,0,0') state = hass.states.get('light.test') - assert 127 == state.attributes.get('brightness') + assert state.attributes.get('brightness') == 127 async def test_white_value_controlling_scale(hass, mqtt_mock): @@ -413,28 +407,27 @@ async def test_white_value_controlling_scale(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('white_value') is None assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'test_scale/status', 'on') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('white_value') == 255 async_fire_mqtt_message(hass, 'test_scale/status', 'off') state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'test_scale/status', 'on') async_fire_mqtt_message(hass, 'test_scale/white_value/status', '99') light_state = hass.states.get('light.test') - assert 255 == \ - light_state.attributes['white_value'] + assert light_state.attributes['white_value'] == 255 async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): @@ -471,7 +464,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('brightness') is None assert state.attributes.get('rgb_color') is None @@ -489,24 +482,24 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): '{"hello": "75"}') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 50 == state.attributes.get('brightness') - assert (84, 169, 255) == state.attributes.get('rgb_color') - assert 300 == state.attributes.get('color_temp') - assert 'rainbow' == state.attributes.get('effect') - assert 75 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 50 + assert state.attributes.get('rgb_color') == (84, 169, 255) + assert state.attributes.get('color_temp') == 300 + assert state.attributes.get('effect') == 'rainbow' + assert state.attributes.get('white_value') == 75 async_fire_mqtt_message(hass, 'test_light_rgb/hs/status', '{"hello": [100,50]}') state = hass.states.get('light.test') - assert (100, 50) == state.attributes.get('hs_color') + assert state.attributes.get('hs_color') == (100, 50) async_fire_mqtt_message(hass, 'test_light_rgb/xy/status', '{"hello": [0.123,0.123]}') state = hass.states.get('light.test') - assert (0.14, 0.131) == state.attributes.get('xy_color') + assert state.attributes.get('xy_color') == (0.14, 0.131) async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): @@ -539,12 +532,12 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 95 == state.attributes.get('brightness') - assert (100, 100) == state.attributes.get('hs_color') - assert 'random' == state.attributes.get('effect') - assert 100 == state.attributes.get('color_temp') - assert 50 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 95 + assert state.attributes.get('hs_color') == (100, 100) + assert state.attributes.get('effect') == 'random' + assert state.attributes.get('color_temp') == 100 + assert state.attributes.get('white_value') == 50 assert state.attributes.get(ATTR_ASSUMED_STATE) common.async_turn_on(hass, 'light.test') @@ -554,7 +547,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 'test_light_rgb/set', 'on', 2, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON common.async_turn_off(hass, 'light.test') await hass.async_block_till_done() @@ -563,7 +556,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 'test_light_rgb/set', 'off', 2, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF mqtt_mock.reset_mock() common.async_turn_on(hass, 'light.test', @@ -584,12 +577,12 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ], any_order=True) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 128, 0) == state.attributes['rgb_color'] - assert 50 == state.attributes['brightness'] - assert (30.118, 100) == state.attributes['hs_color'] - assert 80 == state.attributes['white_value'] - assert (0.611, 0.375) == state.attributes['xy_color'] + assert state.state == STATE_ON + assert state.attributes['rgb_color'] == (255, 128, 0) + assert state.attributes['brightness'] == 50 + assert state.attributes['hs_color'] == (30.118, 100) + assert state.attributes['white_value'] == 80 + assert state.attributes['xy_color'] == (0.611, 0.375) async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): @@ -609,7 +602,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 64]) await hass.async_block_till_done() @@ -620,8 +613,8 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): ], any_order=True) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 128, 63) == state.attributes['rgb_color'] + assert state.state == STATE_ON + assert state.attributes['rgb_color'] == (255, 128, 63) async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): @@ -640,7 +633,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', color_temp=100) await hass.async_block_till_done() @@ -651,8 +644,8 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): ], any_order=True) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 100 == state.attributes['color_temp'] + assert state.state == STATE_ON + assert state.attributes['color_temp'] == 100 async def test_show_brightness_if_only_command_topic(hass, mqtt_mock): @@ -668,14 +661,14 @@ async def test_show_brightness_if_only_command_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('brightness') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('brightness') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 255 async def test_show_color_temp_only_if_command_topic(hass, mqtt_mock): @@ -691,14 +684,14 @@ async def test_show_color_temp_only_if_command_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('color_temp') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 150 == state.attributes.get('color_temp') + assert state.state == STATE_ON + assert state.attributes.get('color_temp') == 150 async def test_show_effect_only_if_command_topic(hass, mqtt_mock): @@ -714,14 +707,14 @@ async def test_show_effect_only_if_command_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('effect') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 'none' == state.attributes.get('effect') + assert state.state == STATE_ON + assert state.attributes.get('effect') == 'none' async def test_show_hs_if_only_command_topic(hass, mqtt_mock): @@ -737,14 +730,14 @@ async def test_show_hs_if_only_command_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('hs_color') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (0, 0) == state.attributes.get('hs_color') + assert state.state == STATE_ON + assert state.attributes.get('hs_color') == (0, 0) async def test_show_white_value_if_only_command_topic(hass, mqtt_mock): @@ -760,14 +753,14 @@ async def test_show_white_value_if_only_command_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('white_value') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('white_value') == 255 async def test_show_xy_if_only_command_topic(hass, mqtt_mock): @@ -783,14 +776,14 @@ async def test_show_xy_if_only_command_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('xy_color') is None async_fire_mqtt_message(hass, 'test_light_rgb/status', 'ON') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (0.323, 0.329) == state.attributes.get('xy_color') + assert state.state == STATE_ON + assert state.attributes.get('xy_color') == (0.323, 0.329) async def test_on_command_first(hass, mqtt_mock): @@ -806,7 +799,7 @@ async def test_on_command_first(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', brightness=50) await hass.async_block_till_done() @@ -839,7 +832,7 @@ async def test_on_command_last(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', brightness=50) await hass.async_block_till_done() @@ -874,7 +867,7 @@ async def test_on_command_brightness(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF # Turn on w/ no brightness - should set to max common.async_turn_on(hass, 'light.test') @@ -927,7 +920,7 @@ async def test_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', brightness=127) await hass.async_block_till_done() @@ -962,17 +955,17 @@ async def test_default_availability_payload(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_custom_availability_payload(hass, mqtt_mock): @@ -991,17 +984,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -1018,7 +1011,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('light.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -1076,7 +1069,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('light.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', @@ -1086,12 +1079,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('light.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('light.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index e4f2a3b7ef8..018f706a1a0 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -143,8 +143,8 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( }) state = hass.states.get('light.test') - assert STATE_OFF == state.state - assert 40 == state.attributes.get(ATTR_SUPPORTED_FEATURES) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -156,7 +156,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON"}') state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -187,8 +187,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state - assert 191 == state.attributes.get(ATTR_SUPPORTED_FEATURES) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -208,68 +208,64 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): '"white_value":150}') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 255, 255) == state.attributes.get('rgb_color') - assert 255 == state.attributes.get('brightness') - assert 155 == state.attributes.get('color_temp') - assert 'colorloop' == state.attributes.get('effect') - assert 150 == state.attributes.get('white_value') - assert (0.323, 0.329) == state.attributes.get('xy_color') - assert (0.0, 0.0) == state.attributes.get('hs_color') + assert state.state == STATE_ON + assert state.attributes.get('rgb_color') == (255, 255, 255) + assert state.attributes.get('brightness') == 255 + assert state.attributes.get('color_temp') == 155 + assert state.attributes.get('effect') == 'colorloop' + assert state.attributes.get('white_value') == 150 + assert state.attributes.get('xy_color') == (0.323, 0.329) + assert state.attributes.get('hs_color') == (0.0, 0.0) # Turn the light off async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"OFF"}') state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "brightness":100}') light_state = hass.states.get('light.test') - assert 100 == \ - light_state.attributes['brightness'] + assert light_state.attributes['brightness'] == 100 async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", ' '"color":{"r":125,"g":125,"b":125}}') light_state = hass.states.get('light.test') - assert (255, 255, 255) == \ - light_state.attributes.get('rgb_color') + assert light_state.attributes.get('rgb_color') == (255, 255, 255) async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "color":{"x":0.135,"y":0.135}}') light_state = hass.states.get('light.test') - assert (0.141, 0.14) == \ - light_state.attributes.get('xy_color') + assert light_state.attributes.get('xy_color') == (0.141, 0.14) async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "color":{"h":180,"s":50}}') light_state = hass.states.get('light.test') - assert (180.0, 50.0) == \ - light_state.attributes.get('hs_color') + assert light_state.attributes.get('hs_color') == (180.0, 50.0) async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "color_temp":155}') light_state = hass.states.get('light.test') - assert 155 == light_state.attributes.get('color_temp') + assert light_state.attributes.get('color_temp') == 155 async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "effect":"colorloop"}') light_state = hass.states.get('light.test') - assert 'colorloop' == light_state.attributes.get('effect') + assert light_state.attributes.get('effect') == 'colorloop' async_fire_mqtt_message(hass, 'test_light_rgb', '{"state":"ON", "white_value":155}') light_state = hass.states.get('light.test') - assert 155 == light_state.attributes.get('white_value') + assert light_state.attributes.get('white_value') == 155 async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): @@ -301,13 +297,13 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 95 == state.attributes.get('brightness') - assert (100, 100) == state.attributes.get('hs_color') - assert 'random' == state.attributes.get('effect') - assert 100 == state.attributes.get('color_temp') - assert 50 == state.attributes.get('white_value') - assert 191 == state.attributes.get(ATTR_SUPPORTED_FEATURES) + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 95 + assert state.attributes.get('hs_color') == (100, 100) + assert state.attributes.get('effect') == 'random' + assert state.attributes.get('color_temp') == 100 + assert state.attributes.get('white_value') == 50 + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 assert state.attributes.get(ATTR_ASSUMED_STATE) common.async_turn_on(hass, 'light.test') @@ -317,7 +313,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 'test_light_rgb/set', '{"state": "ON"}', 2, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON common.async_turn_off(hass, 'light.test') await hass.async_block_till_done() @@ -326,7 +322,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): 'test_light_rgb/set', '{"state": "OFF"}', 2, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF mqtt_mock.reset_mock() common.async_turn_on(hass, 'light.test', @@ -362,12 +358,12 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ], any_order=True) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 128, 0) == state.attributes['rgb_color'] - assert 50 == state.attributes['brightness'] - assert (30.118, 100) == state.attributes['hs_color'] - assert 80 == state.attributes['white_value'] - assert (0.611, 0.375) == state.attributes['xy_color'] + assert state.state == STATE_ON + assert state.attributes['rgb_color'] == (255, 128, 0) + assert state.attributes['brightness'] == 50 + assert state.attributes['hs_color'] == (30.118, 100) + assert state.attributes['white_value'] == 80 + assert state.attributes['xy_color'] == (0.611, 0.375) async def test_sending_hs_color(hass, mqtt_mock): @@ -384,7 +380,7 @@ async def test_sending_hs_color(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF mqtt_mock.reset_mock() common.async_turn_on(hass, 'light.test', @@ -430,7 +426,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) @@ -473,7 +469,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) @@ -519,7 +515,7 @@ async def test_sending_xy_color(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF common.async_turn_on(hass, 'light.test', brightness=50, xy_color=[0.123, 0.123]) @@ -566,8 +562,8 @@ async def test_flash_short_and_long(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state - assert 40 == state.attributes.get(ATTR_SUPPORTED_FEATURES) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 common.async_turn_on(hass, 'light.test', flash='short') await hass.async_block_till_done() @@ -577,7 +573,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): '{"state": "ON", "flash": 5}'), 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON common.async_turn_on(hass, 'light.test', flash='long') await hass.async_block_till_done() @@ -587,7 +583,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): '{"state": "ON", "flash": 15}'), 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async def test_transition(hass, mqtt_mock): @@ -603,8 +599,8 @@ async def test_transition(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state - assert 40 == state.attributes.get(ATTR_SUPPORTED_FEATURES) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 common.async_turn_on(hass, 'light.test', transition=15) await hass.async_block_till_done() @@ -614,7 +610,7 @@ async def test_transition(hass, mqtt_mock): '{"state": "ON", "transition": 15}'), 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON common.async_turn_off(hass, 'light.test', transition=30) await hass.async_block_till_done() @@ -624,7 +620,7 @@ async def test_transition(hass, mqtt_mock): '{"state": "OFF", "transition": 30}'), 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_brightness_scale(hass, mqtt_mock): @@ -642,7 +638,7 @@ async def test_brightness_scale(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('brightness') is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -650,16 +646,16 @@ async def test_brightness_scale(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_bright_scale', '{"state":"ON"}') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('brightness') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 255 # Turn on the light with brightness async_fire_mqtt_message(hass, 'test_light_bright_scale', '{"state":"ON", "brightness": 99}') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('brightness') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 255 async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): @@ -679,8 +675,8 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state - assert 185 == state.attributes.get(ATTR_SUPPORTED_FEATURES) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 185 assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('white_value') is None @@ -694,10 +690,10 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): '"white_value": 255}') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 255, 255) == state.attributes.get('rgb_color') - assert 255 == state.attributes.get('brightness') - assert 255 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('rgb_color') == (255, 255, 255) + assert state.attributes.get('brightness') == 255 + assert state.attributes.get('white_value') == 255 # Bad color values async_fire_mqtt_message(hass, 'test_light_rgb', @@ -706,8 +702,8 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): # Color should not have changed state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 255, 255) == state.attributes.get('rgb_color') + assert state.state == STATE_ON + assert state.attributes.get('rgb_color') == (255, 255, 255) # Bad brightness values async_fire_mqtt_message(hass, 'test_light_rgb', @@ -716,8 +712,8 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): # Brightness should not have changed state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('brightness') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 255 # Bad white value async_fire_mqtt_message(hass, 'test_light_rgb', @@ -726,8 +722,8 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): # White value should not have changed state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('white_value') == 255 async def test_default_availability_payload(hass, mqtt_mock): @@ -744,17 +740,17 @@ async def test_default_availability_payload(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_custom_availability_payload(hass, mqtt_mock): @@ -773,17 +769,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -801,7 +797,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('light.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -863,7 +859,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('light.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', @@ -873,12 +869,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('light.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('light.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 658357b8063..eef91675110 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -112,7 +112,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -122,7 +122,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test_light_rgb', 'on') state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -165,7 +165,7 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('effect') is None @@ -178,50 +178,48 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( 'on,255,145,123,255-128-64,') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert (255, 128, 63) == state.attributes.get('rgb_color') - assert 255 == state.attributes.get('brightness') - assert 145 == state.attributes.get('color_temp') - assert 123 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('rgb_color') == (255, 128, 63) + assert state.attributes.get('brightness') == 255 + assert state.attributes.get('color_temp') == 145 + assert state.attributes.get('white_value') == 123 assert state.attributes.get('effect') is None # turn the light off async_fire_mqtt_message(hass, 'test_light_rgb', 'off') state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF # lower the brightness async_fire_mqtt_message(hass, 'test_light_rgb', 'on,100') light_state = hass.states.get('light.test') - assert 100 == light_state.attributes['brightness'] + assert light_state.attributes['brightness'] == 100 # change the color temp async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,195') light_state = hass.states.get('light.test') - assert 195 == light_state.attributes['color_temp'] + assert light_state.attributes['color_temp'] == 195 # change the color async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,,41-42-43') light_state = hass.states.get('light.test') - assert (243, 249, 255) == \ - light_state.attributes.get('rgb_color') + assert light_state.attributes.get('rgb_color') == (243, 249, 255) # change the white value async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,134') light_state = hass.states.get('light.test') - assert 134 == light_state.attributes['white_value'] + assert light_state.attributes['white_value'] == 134 # change the effect - async_fire_mqtt_message(hass, 'test_light_rgb', - 'on,,,,41-42-43,rainbow') + async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,,41-42-43,rainbow') light_state = hass.states.get('light.test') - assert 'rainbow' == light_state.attributes.get('effect') + assert light_state.attributes.get('effect') == 'rainbow' async def test_optimistic(hass, mqtt_mock): @@ -256,12 +254,12 @@ async def test_optimistic(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 95 == state.attributes.get('brightness') - assert (100, 100) == state.attributes.get('hs_color') - assert 'random' == state.attributes.get('effect') - assert 100 == state.attributes.get('color_temp') - assert 50 == state.attributes.get('white_value') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 95 + assert state.attributes.get('hs_color') == (100, 100) + assert state.attributes.get('effect') == 'random' + assert state.attributes.get('color_temp') == 100 + assert state.attributes.get('white_value') == 50 assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -281,7 +279,7 @@ async def test_flash(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_transition(hass, mqtt_mock): @@ -299,7 +297,7 @@ async def test_transition(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_invalid_values(hass, mqtt_mock): @@ -336,7 +334,7 @@ async def test_invalid_values(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get('rgb_color') is None assert state.attributes.get('brightness') is None assert state.attributes.get('color_temp') is None @@ -349,54 +347,54 @@ async def test_invalid_values(hass, mqtt_mock): 'on,255,215,222,255-255-255,rainbow') state = hass.states.get('light.test') - assert STATE_ON == state.state - assert 255 == state.attributes.get('brightness') - assert 215 == state.attributes.get('color_temp') - assert (255, 255, 255) == state.attributes.get('rgb_color') - assert 222 == state.attributes.get('white_value') - assert 'rainbow' == state.attributes.get('effect') + assert state.state == STATE_ON + assert state.attributes.get('brightness') == 255 + assert state.attributes.get('color_temp') == 215 + assert state.attributes.get('rgb_color') == (255, 255, 255) + assert state.attributes.get('white_value') == 222 + assert state.attributes.get('effect') == 'rainbow' # bad state value async_fire_mqtt_message(hass, 'test_light_rgb', 'offf') # state should not have changed state = hass.states.get('light.test') - assert STATE_ON == state.state + assert state.state == STATE_ON # bad brightness values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,off,255-255-255') # brightness should not have changed state = hass.states.get('light.test') - assert 255 == state.attributes.get('brightness') + assert state.attributes.get('brightness') == 255 # bad color temp values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,off,255-255-255') # color temp should not have changed state = hass.states.get('light.test') - assert 215 == state.attributes.get('color_temp') + assert state.attributes.get('color_temp') == 215 # bad color values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,255,a-b-c') # color should not have changed state = hass.states.get('light.test') - assert (255, 255, 255) == state.attributes.get('rgb_color') + assert state.attributes.get('rgb_color') == (255, 255, 255) # bad white value values async_fire_mqtt_message(hass, 'test_light_rgb', 'on,,,off,255-255-255') # white value should not have changed state = hass.states.get('light.test') - assert 222 == state.attributes.get('white_value') + assert state.attributes.get('white_value') == 222 # bad effect value async_fire_mqtt_message(hass, 'test_light_rgb', 'on,255,a-b-c,white') # effect should not have changed state = hass.states.get('light.test') - assert 'rainbow' == state.attributes.get('effect') + assert state.attributes.get('effect') == 'rainbow' async def test_default_availability_payload(hass, mqtt_mock): @@ -414,17 +412,17 @@ async def test_default_availability_payload(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_custom_availability_payload(hass, mqtt_mock): @@ -444,17 +442,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): }) state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('light.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -474,7 +472,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('light.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -544,7 +542,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('light.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/light/bla/config', @@ -554,12 +552,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('light.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('light.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 56152870cc6..6328d2b7c1a 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -218,7 +218,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('lock.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -276,7 +276,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('lock.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/lock/bla/config', @@ -286,12 +286,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('lock.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('lock.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index db8f7620864..bcd70b82a24 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -30,9 +30,8 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', '100') state = hass.states.get('sensor.test') - assert '100' == state.state - assert 'fav unit' == \ - state.attributes.get('unit_of_measurement') + assert state.state == '100' + assert state.attributes.get('unit_of_measurement') == 'fav unit' async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): @@ -49,7 +48,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }) state = hass.states.get('sensor.test') - assert 'unknown' == state.state + assert state.state == 'unknown' now = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) with patch(('homeassistant.helpers.event.' @@ -60,7 +59,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): # Value was set correctly. state = hass.states.get('sensor.test') - assert '100' == state.state + assert state.state == '100' # Time jump +3s now = now + timedelta(seconds=3) @@ -69,7 +68,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): # Value is not yet expired state = hass.states.get('sensor.test') - assert '100' == state.state + assert state.state == '100' # Next message resets timer with patch(('homeassistant.helpers.event.' @@ -80,7 +79,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): # Value was updated correctly. state = hass.states.get('sensor.test') - assert '101' == state.state + assert state.state == '101' # Time jump +3s now = now + timedelta(seconds=3) @@ -89,7 +88,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): # Value is not yet expired state = hass.states.get('sensor.test') - assert '101' == state.state + assert state.state == '101' # Time jump +2s now = now + timedelta(seconds=2) @@ -98,7 +97,7 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): # Value is expired now state = hass.states.get('sensor.test') - assert 'unknown' == state.state + assert state.state == 'unknown' async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): @@ -116,7 +115,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') state = hass.states.get('sensor.test') - assert '100' == state.state + assert state.state == '100' async def test_force_update_disabled(hass, mqtt_mock): @@ -140,11 +139,11 @@ async def test_force_update_disabled(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', '100') await hass.async_block_till_done() - assert 1 == len(events) + assert len(events) == 1 async_fire_mqtt_message(hass, 'test-topic', '100') await hass.async_block_till_done() - assert 1 == len(events) + assert len(events) == 1 async def test_force_update_enabled(hass, mqtt_mock): @@ -169,11 +168,11 @@ async def test_force_update_enabled(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', '100') await hass.async_block_till_done() - assert 1 == len(events) + assert len(events) == 1 async_fire_mqtt_message(hass, 'test-topic', '100') await hass.async_block_till_done() - assert 2 == len(events) + assert len(events) == 2 async def test_default_availability_payload(hass, mqtt_mock): @@ -188,17 +187,17 @@ async def test_default_availability_payload(hass, mqtt_mock): }) state = hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') state = hass.states.get('sensor.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') state = hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_custom_availability_payload(hass, mqtt_mock): @@ -215,17 +214,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): }) state = hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') state = hass.states.get('sensor.test') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') state = hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_setting_sensor_attribute_via_legacy_mqtt_json_message( @@ -244,8 +243,7 @@ async def test_setting_sensor_attribute_via_legacy_mqtt_json_message( async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') state = hass.states.get('sensor.test') - assert '100' == \ - state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_legacy_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -302,9 +300,8 @@ async def test_update_with_legacy_json_attrs_and_template(hass, mqtt_mock): async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') state = hass.states.get('sensor.test') - assert '100' == \ - state.attributes.get('val') - assert '100' == state.state + assert state.attributes.get('val') == '100' + assert state.state == '100' async def test_invalid_device_class(hass, mqtt_mock): @@ -358,7 +355,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('sensor.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_setting_attribute_with_template(hass, mqtt_mock): @@ -377,8 +374,8 @@ async def test_setting_attribute_with_template(hass, mqtt_mock): {"Timer1": {"Arm": 0, "Time": "22:18"}})) state = hass.states.get('sensor.test') - assert 0 == state.attributes.get('Arm') - assert '22:18' == state.attributes.get('Time') + assert state.attributes.get('Arm') == 0 + assert state.attributes.get('Time') == '22:18' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -436,7 +433,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('sensor.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/sensor/bla/config', @@ -446,12 +443,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('sensor.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('sensor.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 180b7af5bef..95074e95eb3 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -33,24 +33,24 @@ async def test_subscribe_topics(hass, mqtt_mock, caplog): 'msg_callback': record_calls2}}) async_fire_mqtt_message(hass, 'test-topic1', 'test-payload1') - assert 1 == len(calls1) - assert 'test-topic1' == calls1[0][0].topic - assert 'test-payload1' == calls1[0][0].payload - assert 0 == len(calls2) + assert len(calls1) == 1 + assert calls1[0][0].topic == 'test-topic1' + assert calls1[0][0].payload == 'test-payload1' + assert len(calls2) == 0 async_fire_mqtt_message(hass, 'test-topic2', 'test-payload2') - assert 1 == len(calls1) - assert 1 == len(calls2) - assert 'test-topic2' == calls2[0][0].topic - assert 'test-payload2' == calls2[0][0].payload + assert len(calls1) == 1 + assert len(calls2) == 1 + assert calls2[0][0].topic == 'test-topic2' + assert calls2[0][0].payload == 'test-payload2' await async_unsubscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, 'test-topic1', 'test-payload') async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - assert 1 == len(calls1) - assert 1 == len(calls2) + assert len(calls1) == 1 + assert len(calls2) == 1 async def test_modify_topics(hass, mqtt_mock, caplog): @@ -78,12 +78,12 @@ async def test_modify_topics(hass, mqtt_mock, caplog): 'msg_callback': record_calls2}}) async_fire_mqtt_message(hass, 'test-topic1', 'test-payload') - assert 1 == len(calls1) - assert 0 == len(calls2) + assert len(calls1) == 1 + assert len(calls2) == 0 async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - assert 1 == len(calls1) - assert 1 == len(calls2) + assert len(calls1) == 1 + assert len(calls2) == 1 sub_state = await async_subscribe_topics( hass, sub_state, @@ -92,22 +92,22 @@ async def test_modify_topics(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'test-topic1', 'test-payload') async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - assert 1 == len(calls1) - assert 1 == len(calls2) + assert len(calls1) == 1 + assert len(calls2) == 1 async_fire_mqtt_message(hass, 'test-topic1_1', 'test-payload') - assert 2 == len(calls1) - assert 'test-topic1_1' == calls1[1][0].topic - assert 'test-payload' == calls1[1][0].payload - assert 1 == len(calls2) + assert len(calls1) == 2 + assert calls1[1][0].topic == 'test-topic1_1' + assert calls1[1][0].payload == 'test-payload' + assert len(calls2) == 1 await async_unsubscribe_topics(hass, sub_state) async_fire_mqtt_message(hass, 'test-topic1_1', 'test-payload') async_fire_mqtt_message(hass, 'test-topic2', 'test-payload') - assert 2 == len(calls1) - assert 1 == len(calls2) + assert len(calls1) == 2 + assert len(calls2) == 1 async def test_qos_encoding_default(hass, mqtt_mock, caplog): diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index dfd05424ca7..df6706b01cf 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -38,18 +38,18 @@ async def test_controlling_state_via_topic(hass, mock_publish): }) state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', '1') state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async_fire_mqtt_message(hass, 'state-topic', '0') state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): @@ -71,7 +71,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): }) state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON assert state.attributes.get(ATTR_ASSUMED_STATE) common.turn_on(hass, 'switch.test') @@ -81,7 +81,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): 'command-topic', 'beer on', 2, False) mock_publish.async_publish.reset_mock() state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON common.turn_off(hass, 'switch.test') await hass.async_block_till_done() @@ -90,7 +90,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): mock_publish.async_publish.assert_called_once_with( 'command-topic', 'beer off', 2, False) state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_controlling_state_via_topic_and_json_message( @@ -109,17 +109,17 @@ async def test_controlling_state_via_topic_and_json_message( }) state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async_fire_mqtt_message(hass, 'state-topic', '{"val":"beer on"}') state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async_fire_mqtt_message(hass, 'state-topic', '{"val":"beer off"}') state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_default_availability_payload(hass, mock_publish): @@ -137,28 +137,28 @@ async def test_default_availability_payload(hass, mock_publish): }) state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'online') state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'availability_topic', 'offline') state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'state-topic', '1') state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'online') state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async def test_custom_availability_payload(hass, mock_publish): @@ -178,28 +178,28 @@ async def test_custom_availability_payload(hass, mock_publish): }) state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'good') state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'availability_topic', 'nogood') state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'state-topic', '1') state = hass.states.get('switch.test') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability_topic', 'good') state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async def test_custom_state_payload(hass, mock_publish): @@ -218,18 +218,18 @@ async def test_custom_state_payload(hass, mock_publish): }) state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, 'state-topic', 'HIGH') state = hass.states.get('switch.test') - assert STATE_ON == state.state + assert state.state == STATE_ON async_fire_mqtt_message(hass, 'state-topic', 'LOW') state = hass.states.get('switch.test') - assert STATE_OFF == state.state + assert state.state == STATE_OFF async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): @@ -246,7 +246,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') state = hass.states.get('switch.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -304,7 +304,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') state = hass.states.get('switch.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/switch/bla/config', @@ -314,12 +314,12 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') state = hass.states.get('switch.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') state = hass.states.get('switch.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' async def test_unique_id(hass): From e3981b6498a2884898bf6a1a0699d2089cb94d6b Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Mon, 22 Apr 2019 12:09:55 -0700 Subject: [PATCH 074/346] Bump skybellpy to 0.4.0 (#23294) * Bump skybellpy to 0.4.0 * Bump skybellpy to 0.4.0 in requirements_all.txt --- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 6a22a698b4c..843fd3d13b0 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,7 +3,7 @@ "name": "Skybell", "documentation": "https://www.home-assistant.io/components/skybell", "requirements": [ - "skybellpy==0.3.0" + "skybellpy==0.4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 7bb24ef8bfb..0d0d821e665 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1585,7 +1585,7 @@ simplisafe-python==3.4.1 sisyphus-control==2.1 # homeassistant.components.skybell -skybellpy==0.3.0 +skybellpy==0.4.0 # homeassistant.components.slack slacker==0.12.0 From 8daba68dc1eae1fd0b67879867416a5c42cd6955 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Mon, 22 Apr 2019 14:10:55 -0500 Subject: [PATCH 075/346] Add support to play url (#23273) --- homeassistant/components/heos/media_player.py | 20 +++++++--- tests/components/heos/test_media_player.py | 38 +++++++++++++++++-- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 8821591df20..56e9647df50 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -6,10 +6,10 @@ from typing import Sequence from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( - DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, - SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP) + DOMAIN, MEDIA_TYPE_MUSIC, MEDIA_TYPE_URL, SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, + SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.typing import HomeAssistantType @@ -20,7 +20,8 @@ from .const import ( BASE_SUPPORTED_FEATURES = SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \ SUPPORT_VOLUME_STEP | SUPPORT_CLEAR_PLAYLIST | \ - SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOURCE + SUPPORT_SHUFFLE_SET | SUPPORT_SELECT_SOURCE | \ + SUPPORT_PLAY_MEDIA _LOGGER = logging.getLogger(__name__) @@ -153,6 +154,15 @@ class HeosMediaPlayer(MediaPlayerDevice): """Mute the volume.""" await self._player.set_mute(mute) + @log_command_error("play media") + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + if media_type == MEDIA_TYPE_URL: + await self._player.play_url(media_id) + else: + _LOGGER.error("Unable to play media: Unsupported media type '%s'", + media_type) + @log_command_error("select source") async def async_select_source(self, source): """Select input source.""" diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 0870f82b3ff..4cf871f5ed0 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the Heos Media Player platform.""" import asyncio -from pyheos import const, CommandError +from pyheos import CommandError, const from homeassistant.components.heos import media_player from homeassistant.components.heos.const import ( @@ -12,8 +12,9 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_DURATION, ATTR_MEDIA_POSITION, ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_TITLE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MEDIA_PLAYER_DOMAIN, MEDIA_TYPE_MUSIC, - SERVICE_CLEAR_PLAYLIST, SERVICE_SELECT_SOURCE, SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP) + MEDIA_TYPE_URL, SERVICE_CLEAR_PLAYLIST, SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOURCE, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -415,3 +416,34 @@ async def test_unload_config_entry(hass, config_entry, config, controller): await setup_platform(hass, config_entry, config) await config_entry.async_unload(hass) assert not hass.states.get('media_player.test_player') + + +async def test_play_media_url(hass, config_entry, config, controller, caplog): + """Test the play media service with type url.""" + await setup_platform(hass, config_entry, config) + player = controller.players[1] + url = "http://news/podcast.mp3" + # First pass completes successfully, second pass raises command error + for _ in range(2): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_URL, + ATTR_MEDIA_CONTENT_ID: url}, blocking=True) + player.play_url.assert_called_once_with(url) + player.play_url.reset_mock() + player.play_url.side_effect = CommandError(None, "Failure", 1) + assert "Unable to play media: Failure (1)" in caplog.text + + +async def test_play_media_invalid_type( + hass, config_entry, config, controller, caplog): + """Test the play media service with an invalid type.""" + await setup_platform(hass, config_entry, config) + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, + {ATTR_ENTITY_ID: 'media_player.test_player', + ATTR_MEDIA_CONTENT_TYPE: "Other", + ATTR_MEDIA_CONTENT_ID: ""}, blocking=True) + assert "Unable to play media: Unsupported media type 'Other'" \ + in caplog.text From 0c90bfb9369af09eaafcff7944d122d56eb96cfc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 22 Apr 2019 21:13:21 +0200 Subject: [PATCH 076/346] Fix ESPHome setup errors in beta (#23242) * Fix ESPHome setup errors in beta * Update requirements_all.txt --- homeassistant/components/esphome/__init__.py | 3 ++- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 121e210a0a0..e5feedd8421 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -149,7 +149,8 @@ class RuntimeEntryData: def _attr_obj_from_dict(cls, **kwargs): - return cls(**{key: kwargs[key] for key in attr.fields_dict(cls)}) + return cls(**{key: kwargs[key] for key in attr.fields_dict(cls) + if key in kwargs}) async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 734544b49c7..9d25ec6d034 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "documentation": "https://www.home-assistant.io/components/esphome", "requirements": [ - "aioesphomeapi==2.0.0" + "aioesphomeapi==2.0.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0d0d821e665..b1212d3c63a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ aiobotocore==0.10.2 aiodns==1.1.1 # homeassistant.components.esphome -aioesphomeapi==2.0.0 +aioesphomeapi==2.0.1 # homeassistant.components.freebox aiofreepybox==0.0.8 From e85af58e43711e7cd079f33e493d356790ec7fe4 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 22 Apr 2019 21:26:15 +0200 Subject: [PATCH 077/346] RFC: Upgrade philips_js component version and support channels and sources (#23061) * Drop unused constant * Don't default to localhost A philips tv will never run on localhost * Use library internal state * Add play media support for channels * Control update manually This allow us to delay update of state when we perform and action. * Bump version for support for api v1 again * Consider missing source and only channels as channels * Fix some flake8 tasks * Fix some pylint errors * Adjust requirements_all file * Switch to async_add_executor_job * Assume device turns of off a sucessfull standby call --- .../components/philips_js/manifest.json | 2 +- .../components/philips_js/media_player.py | 213 +++++++++++++----- requirements_all.txt | 2 +- 3 files changed, 154 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 18ddcf1f5ff..16a3dbd119d 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -3,7 +3,7 @@ "name": "Philips js", "documentation": "https://www.home-assistant.io/components/philips_js", "requirements": [ - "ha-philipsjs==0.0.5" + "ha-philipsjs==0.0.6" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 859ad26a3dd..0b0b1de4275 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -7,40 +7,48 @@ import voluptuous as vol from homeassistant.components.media_player import ( MediaPlayerDevice, PLATFORM_SCHEMA) from homeassistant.components.media_player.const import ( - SUPPORT_NEXT_TRACK, SUPPORT_PLAY, SUPPORT_PREVIOUS_TRACK, + SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + MEDIA_TYPE_CHANNEL, SUPPORT_PLAY_MEDIA) from homeassistant.const import ( CONF_API_VERSION, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import call_later, track_time_interval from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=30) - SUPPORT_PHILIPS_JS = SUPPORT_TURN_OFF | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE | \ - SUPPORT_SELECT_SOURCE - -SUPPORT_PHILIPS_JS_TV = SUPPORT_PHILIPS_JS | SUPPORT_NEXT_TRACK | \ - SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY + SUPPORT_SELECT_SOURCE | SUPPORT_NEXT_TRACK | \ + SUPPORT_PREVIOUS_TRACK | SUPPORT_PLAY_MEDIA CONF_ON_ACTION = 'turn_on_action' -DEFAULT_DEVICE = 'default' -DEFAULT_HOST = '127.0.0.1' DEFAULT_NAME = "Philips TV" DEFAULT_API_VERSION = '1' +DEFAULT_SCAN_INTERVAL = 30 + +DELAY_ACTION_DEFAULT = 2.0 +DELAY_ACTION_ON = 10.0 + +PREFIX_SEPARATOR = ': ' +PREFIX_SOURCE = 'Input' +PREFIX_CHANNEL = 'Channel' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): cv.string, vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, }) +def _inverted(data): + return {v: k for k, v in data.items()} + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Philips TV platform.""" import haphilipsjs @@ -63,18 +71,38 @@ class PhilipsTV(MediaPlayerDevice): """Initialize the Philips TV.""" self._tv = tv self._name = name - self._state = None - self._volume = None - self._muted = False - self._program_name = None - self._channel_name = None - self._source = None - self._source_list = [] - self._connfail = 0 - self._source_mapping = {} - self._watching_tv = None - self._channel_name = None + self._sources = {} + self._channels = {} self._on_script = on_script + self._supports = SUPPORT_PHILIPS_JS + if self._on_script: + self._supports |= SUPPORT_TURN_ON + self._update_task = None + + def _update_soon(self, delay): + """Reschedule update task.""" + if self._update_task: + self._update_task() + self._update_task = None + + self.schedule_update_ha_state( + force_refresh=False) + + def update_forced(event_time): + self.schedule_update_ha_state(force_refresh=True) + + def update_and_restart(event_time): + update_forced(event_time) + self._update_task = track_time_interval( + self.hass, update_forced, + timedelta(seconds=DEFAULT_SCAN_INTERVAL)) + + call_later(self.hass, delay, update_and_restart) + + async def async_added_to_hass(self): + """Start running updates once we are added to hass.""" + await self.hass.async_add_executor_job( + self._update_soon, 0) @property def name(self): @@ -84,110 +112,173 @@ class PhilipsTV(MediaPlayerDevice): @property def should_poll(self): """Device should be polled.""" - return True + return False @property def supported_features(self): """Flag media player features that are supported.""" - is_supporting_turn_on = SUPPORT_TURN_ON if self._on_script else 0 - if self._watching_tv: - return SUPPORT_PHILIPS_JS_TV | is_supporting_turn_on - return SUPPORT_PHILIPS_JS | is_supporting_turn_on + return self._supports @property def state(self): """Get the device state. An exception means OFF state.""" - return self._state + if self._tv.on: + return STATE_ON + return STATE_OFF @property def source(self): """Return the current input source.""" - return self._source + if self.media_content_type == MEDIA_TYPE_CHANNEL: + name = self._channels.get(self._tv.channel_id) + prefix = PREFIX_CHANNEL + else: + name = self._sources.get(self._tv.source_id) + prefix = PREFIX_SOURCE + + if name is None: + return None + return prefix + PREFIX_SEPARATOR + name @property def source_list(self): """List of available input sources.""" - return self._source_list + complete = [] + for source in self._sources.values(): + complete.append(PREFIX_SOURCE + PREFIX_SEPARATOR + source) + for channel in self._channels.values(): + complete.append(PREFIX_CHANNEL + PREFIX_SEPARATOR + channel) + return complete def select_source(self, source): """Set the input source.""" - if source in self._source_mapping: - self._tv.setSource(self._source_mapping.get(source)) + data = source.split(PREFIX_SEPARATOR, 1) + if data[0] == PREFIX_SOURCE: + source_id = _inverted(self._sources).get(data[1]) + if source_id: + self._tv.setSource(source_id) + elif data[0] == PREFIX_CHANNEL: + channel_id = _inverted(self._channels).get(data[1]) + if channel_id: + self._tv.setChannel(channel_id) + self._update_soon(DELAY_ACTION_DEFAULT) @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._volume + return self._tv.volume @property def is_volume_muted(self): """Boolean if volume is currently muted.""" - return self._muted + return self._tv.muted def turn_on(self): """Turn on the device.""" if self._on_script: self._on_script.run() + self._update_soon(DELAY_ACTION_ON) def turn_off(self): """Turn off the device.""" self._tv.sendKey('Standby') + self._tv.on = False + self._update_soon(DELAY_ACTION_DEFAULT) def volume_up(self): """Send volume up command.""" self._tv.sendKey('VolumeUp') + self._update_soon(DELAY_ACTION_DEFAULT) def volume_down(self): """Send volume down command.""" self._tv.sendKey('VolumeDown') + self._update_soon(DELAY_ACTION_DEFAULT) def mute_volume(self, mute): """Send mute command.""" - if self._muted != mute: - self._tv.sendKey('Mute') - self._muted = mute + self._tv.setVolume(self._tv.volume, mute) + self._update_soon(DELAY_ACTION_DEFAULT) def set_volume_level(self, volume): """Set volume level, range 0..1.""" - self._tv.setVolume(volume) + self._tv.setVolume(volume, self._tv.muted) + self._update_soon(DELAY_ACTION_DEFAULT) def media_previous_track(self): """Send rewind command.""" self._tv.sendKey('Previous') + self._update_soon(DELAY_ACTION_DEFAULT) def media_next_track(self): """Send fast forward command.""" self._tv.sendKey('Next') + self._update_soon(DELAY_ACTION_DEFAULT) + + @property + def media_channel(self): + """Get current channel if it's a channel.""" + if self.media_content_type == MEDIA_TYPE_CHANNEL: + return self._channels.get(self._tv.channel_id) + return None @property def media_title(self): """Title of current playing media.""" - if self._watching_tv and self._channel_name: - return '{} - {}'.format(self._source, self._channel_name) - return self._source + if self.media_content_type == MEDIA_TYPE_CHANNEL: + return self._channels.get(self._tv.channel_id) + return self._sources.get(self._tv.source_id) + + @property + def media_content_type(self): + """Return content type of playing media.""" + if (self._tv.source_id == 'tv' or self._tv.source_id == '11'): + return MEDIA_TYPE_CHANNEL + if (self._tv.source_id is None and self._tv.channels): + return MEDIA_TYPE_CHANNEL + return None + + @property + def media_content_id(self): + """Content type of current playing media.""" + if self.media_content_type == MEDIA_TYPE_CHANNEL: + return self._channels.get(self._tv.channel_id) + return None + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + 'channel_list': list(self._channels.values()) + } + + 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: + channel_id = _inverted(self._channels).get(media_id) + if channel_id: + self._tv.setChannel(channel_id) + self._update_soon(DELAY_ACTION_DEFAULT) + else: + _LOGGER.error("Unable to find channel <%s>", media_id) + else: + _LOGGER.error("Unsupported media type <%s>", media_type) def update(self): """Get the latest data and update device state.""" self._tv.update() - self._volume = self._tv.volume - self._muted = self._tv.muted - if self._tv.source_id: - self._source = self._tv.getSourceName(self._tv.source_id) - if self._tv.sources and not self._source_list: - 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: - self._state = STATE_ON - else: - self._state = STATE_OFF - - self._watching_tv = bool(self._tv.source_id == 'tv') - self._tv.getChannelId() self._tv.getChannels() - if self._tv.channels and self._tv.channel_id in self._tv.channels: - self._channel_name = self._tv.channels[self._tv.channel_id]['name'] - else: - self._channel_name = None + + self._sources = { + srcid: source['name'] or "Source {}".format(srcid) + for srcid, source in (self._tv.sources or {}).items() + } + + self._channels = { + chid: channel['name'] + for chid, channel in (self._tv.channels or {}).items() + } diff --git a/requirements_all.txt b/requirements_all.txt index b1212d3c63a..33ed716b336 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ gstreamer-player==1.1.2 ha-ffmpeg==2.0 # homeassistant.components.philips_js -ha-philipsjs==0.0.5 +ha-philipsjs==0.0.6 # homeassistant.components.habitica habitipy==0.2.0 From 9007e17c3e20c76a4446e6cc562daeae960f7293 Mon Sep 17 00:00:00 2001 From: Pawel Date: Mon, 22 Apr 2019 21:49:15 +0200 Subject: [PATCH 078/346] MQTT Vacuum State Device (#23171) * add StateVacuum MQTT --- .../components/mqtt/vacuum/__init__.py | 97 +++ .../{vacuum.py => vacuum/schema_legacy.py} | 97 +-- .../components/mqtt/vacuum/schema_state.py | 339 +++++++++ .../{test_vacuum.py => test_legacy_vacuum.py} | 349 +++++---- tests/components/mqtt/test_state_vacuum.py | 685 ++++++++++++++++++ 5 files changed, 1371 insertions(+), 196 deletions(-) create mode 100644 homeassistant/components/mqtt/vacuum/__init__.py rename homeassistant/components/mqtt/{vacuum.py => vacuum/schema_legacy.py} (87%) create mode 100644 homeassistant/components/mqtt/vacuum/schema_state.py rename tests/components/mqtt/{test_vacuum.py => test_legacy_vacuum.py} (68%) create mode 100644 tests/components/mqtt/test_state_vacuum.py diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py new file mode 100644 index 00000000000..f69e41985d6 --- /dev/null +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -0,0 +1,97 @@ +""" +Support for MQTT vacuums. + +For more details about this platform, please refer to the documentation at +https://www.home-assistant.io/components/vacuum.mqtt/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.vacuum import DOMAIN +from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH +from homeassistant.components.mqtt.discovery import ( + MQTT_DISCOVERY_NEW, clear_discovery_hash) +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +_LOGGER = logging.getLogger(__name__) + +CONF_SCHEMA = 'schema' +LEGACY = 'legacy' +STATE = 'state' + + +def validate_mqtt_vacuum(value): + """Validate MQTT vacuum schema.""" + from . import schema_legacy + from . import schema_state + + schemas = { + LEGACY: schema_legacy.PLATFORM_SCHEMA_LEGACY, + STATE: schema_state.PLATFORM_SCHEMA_STATE, + } + return schemas[value[CONF_SCHEMA]](value) + + +def services_to_strings(services, service_to_string): + """Convert SUPPORT_* service bitmask to list of service strings.""" + strings = [] + for service in service_to_string: + if service & services: + strings.append(service_to_string[service]) + return strings + + +def strings_to_services(strings, string_to_service): + """Convert service strings to SUPPORT_* service bitmask.""" + services = 0 + for string in strings: + services |= string_to_service[string] + return services + + +MQTT_VACUUM_SCHEMA = vol.Schema({ + vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( + vol.Lower, vol.Any(LEGACY, STATE)) +}) + +PLATFORM_SCHEMA = vol.All(MQTT_VACUUM_SCHEMA.extend({ +}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up MQTT vacuum through configuration.yaml.""" + await _async_setup_entity(config, async_add_entities, + discovery_info) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT vacuum dynamically through MQTT discovery.""" + async def async_discover(discovery_payload): + """Discover and add a MQTT vacuum.""" + try: + discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) + config = PLATFORM_SCHEMA(discovery_payload) + await _async_setup_entity(config, async_add_entities, config_entry, + discovery_hash) + except Exception: + if discovery_hash: + clear_discovery_hash(hass, discovery_hash) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(DOMAIN, 'mqtt'), async_discover) + + +async def _async_setup_entity(config, async_add_entities, config_entry, + discovery_hash=None): + """Set up the MQTT vacuum.""" + from . import schema_legacy + from . import schema_state + setup_entity = { + LEGACY: schema_legacy.async_setup_entity_legacy, + STATE: schema_state.async_setup_entity_state, + } + await setup_entity[config[CONF_SCHEMA]]( + config, async_add_entities, config_entry, discovery_hash) diff --git a/homeassistant/components/mqtt/vacuum.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py similarity index 87% rename from homeassistant/components/mqtt/vacuum.py rename to homeassistant/components/mqtt/vacuum/schema_legacy.py index 5895d52e9dc..6321d98fcd7 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,4 +1,4 @@ -"""Support for a generic MQTT vacuum.""" +"""Support for Legacy MQTT vacuum.""" import logging import json @@ -6,20 +6,20 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.vacuum import ( - DOMAIN, 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) from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.icon import icon_for_battery_level -from . import ( - ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, +from homeassistant.components.mqtt import ( + CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription) -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash + +from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -39,24 +39,6 @@ SERVICE_TO_STRING = { STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} - -def services_to_strings(services): - """Convert SUPPORT_* service bitmask to list of service strings.""" - strings = [] - for service in SERVICE_TO_STRING: - if service & services: - strings.append(SERVICE_TO_STRING[service]) - return strings - - -def strings_to_services(strings): - """Convert service strings to SUPPORT_* service bitmask.""" - services = 0 - for string in strings: - services |= STRING_TO_SERVICE[string] - return services - - DEFAULT_SERVICES = SUPPORT_TURN_ON | SUPPORT_TURN_OFF | SUPPORT_STOP |\ SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY |\ SUPPORT_CLEAN_SPOT @@ -96,9 +78,10 @@ DEFAULT_PAYLOAD_STOP = 'stop' DEFAULT_PAYLOAD_TURN_OFF = 'turn_off' DEFAULT_PAYLOAD_TURN_ON = 'turn_on' DEFAULT_RETAIN = False -DEFAULT_SERVICE_STRINGS = services_to_strings(DEFAULT_SERVICES) +DEFAULT_SERVICE_STRINGS = services_to_strings( + DEFAULT_SERVICES, SERVICE_TO_STRING) -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ +PLATFORM_SCHEMA_LEGACY = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, 'battery'): cv.template, vol.Inclusive(CONF_BATTERY_LEVEL_TOPIC, 'battery'): mqtt.valid_publish_topic, @@ -137,44 +120,19 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ vol.Optional(mqtt.CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( - mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema).extend(MQTT_VACUUM_SCHEMA.schema) -async def async_setup_platform(hass, config, async_add_entities, - discovery_info=None): - """Set up MQTT vacuum through configuration.yaml.""" - await _async_setup_entity(config, async_add_entities, - discovery_info) - - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up MQTT vacuum dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT vacuum.""" - try: - discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, config_entry, - discovery_hash) - except Exception: - if discovery_hash: - clear_discovery_hash(hass, discovery_hash) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(DOMAIN, 'mqtt'), async_discover) - - -async def _async_setup_entity(config, async_add_entities, config_entry, - discovery_hash=None): - """Set up the MQTT vacuum.""" +async def async_setup_entity_legacy(config, async_add_entities, + config_entry, discovery_hash): + """Set up a MQTT Vacuum Legacy.""" async_add_entities([MqttVacuum(config, config_entry, discovery_hash)]) # pylint: disable=too-many-ancestors class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, VacuumDevice): - """Representation of a MQTT-controlled vacuum.""" + """Representation of a MQTT-controlled legacy vacuum.""" def __init__(self, config, config_entry, discovery_info): """Initialize the vacuum.""" @@ -204,7 +162,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._name = config[CONF_NAME] supported_feature_strings = config[CONF_SUPPORTED_FEATURES] self._supported_features = strings_to_services( - supported_feature_strings + supported_feature_strings, STRING_TO_SERVICE ) self._fan_speed_list = config[CONF_FAN_SPEED_LIST] self._qos = config[mqtt.CONF_QOS] @@ -248,7 +206,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def discovery_update(self, discovery_payload): """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) + config = PLATFORM_SCHEMA_LEGACY(discovery_payload) self._setup_from_config(config) await self.attributes_discovery_update(config) await self.availability_discovery_update(config) @@ -374,7 +332,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def status(self): """Return a status string for the vacuum.""" if self.supported_features & SUPPORT_STATUS == 0: - return + return None return self._status @@ -382,7 +340,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, def fan_speed(self): """Return the status of the vacuum.""" if self.supported_features & SUPPORT_FAN_SPEED == 0: - return + return None return self._fan_speed @@ -429,7 +387,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_turn_off(self, **kwargs): """Turn the vacuum off.""" if self.supported_features & SUPPORT_TURN_OFF == 0: - return + return None mqtt.async_publish(self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_TURN_OFF], @@ -440,7 +398,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_stop(self, **kwargs): """Stop the vacuum.""" if self.supported_features & SUPPORT_STOP == 0: - return + return None mqtt.async_publish(self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_STOP], @@ -451,7 +409,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self.supported_features & SUPPORT_CLEAN_SPOT == 0: - return + return None mqtt.async_publish(self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_CLEAN_SPOT], @@ -462,7 +420,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_locate(self, **kwargs): """Locate the vacuum (usually by playing a song).""" if self.supported_features & SUPPORT_LOCATE == 0: - return + return None mqtt.async_publish(self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_LOCATE], @@ -473,7 +431,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_start_pause(self, **kwargs): """Start, pause or resume the cleaning task.""" if self.supported_features & SUPPORT_PAUSE == 0: - return + return None mqtt.async_publish(self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_START_PAUSE], @@ -484,7 +442,7 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_return_to_base(self, **kwargs): """Tell the vacuum to return to its dock.""" if self.supported_features & SUPPORT_RETURN_HOME == 0: - return + return None mqtt.async_publish(self.hass, self._command_topic, self._payloads[CONF_PAYLOAD_RETURN_TO_BASE], @@ -494,10 +452,9 @@ class MqttVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" - if self.supported_features & SUPPORT_FAN_SPEED == 0: - return - if not self._fan_speed_list or fan_speed not in self._fan_speed_list: - return + if ((self.supported_features & SUPPORT_FAN_SPEED == 0) or + fan_speed not in self._fan_speed_list): + return None mqtt.async_publish(self.hass, self._set_fan_speed_topic, fan_speed, self._qos, self._retain) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py new file mode 100644 index 00000000000..2e0921ad19d --- /dev/null +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -0,0 +1,339 @@ +"""Support for a State MQTT vacuum.""" +import logging +import json + +import voluptuous as vol + +from homeassistant.components import mqtt +from homeassistant.components.vacuum import ( + SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_START, + SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, + SUPPORT_STATUS, SUPPORT_STOP, STATE_CLEANING, STATE_DOCKED, STATE_PAUSED, + STATE_IDLE, STATE_RETURNING, STATE_ERROR, StateVacuumDevice) +from homeassistant.const import ( + ATTR_SUPPORTED_FEATURES, CONF_DEVICE, CONF_NAME, CONF_VALUE_TEMPLATE) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.mqtt import ( + CONF_UNIQUE_ID, MqttAttributes, MqttAvailability, + MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription, + CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, CONF_QOS) + +from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services + +_LOGGER = logging.getLogger(__name__) + +SERVICE_TO_STRING = { + SUPPORT_START: 'start', + SUPPORT_PAUSE: 'pause', + SUPPORT_STOP: 'stop', + SUPPORT_RETURN_HOME: 'return_home', + SUPPORT_FAN_SPEED: 'fan_speed', + SUPPORT_BATTERY: 'battery', + SUPPORT_STATUS: 'status', + SUPPORT_SEND_COMMAND: 'send_command', + SUPPORT_LOCATE: 'locate', + SUPPORT_CLEAN_SPOT: 'clean_spot', +} + +STRING_TO_SERVICE = {v: k for k, v in SERVICE_TO_STRING.items()} + + +DEFAULT_SERVICES = SUPPORT_START | SUPPORT_STOP |\ + SUPPORT_RETURN_HOME | SUPPORT_STATUS | SUPPORT_BATTERY |\ + SUPPORT_CLEAN_SPOT +ALL_SERVICES = DEFAULT_SERVICES | SUPPORT_PAUSE | SUPPORT_LOCATE |\ + SUPPORT_FAN_SPEED | SUPPORT_SEND_COMMAND + +BATTERY = 'battery_level' +FAN_SPEED = 'fan_speed' +STATE = "state" + +POSSIBLE_STATES = { + STATE_IDLE: STATE_IDLE, + STATE_DOCKED: STATE_DOCKED, + STATE_ERROR: STATE_ERROR, + STATE_PAUSED: STATE_PAUSED, + STATE_RETURNING: STATE_RETURNING, + STATE_CLEANING: STATE_CLEANING, +} + +CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES +CONF_PAYLOAD_TURN_ON = 'payload_turn_on' +CONF_PAYLOAD_TURN_OFF = 'payload_turn_off' +CONF_PAYLOAD_RETURN_TO_BASE = 'payload_return_to_base' +CONF_PAYLOAD_STOP = 'payload_stop' +CONF_PAYLOAD_CLEAN_SPOT = 'payload_clean_spot' +CONF_PAYLOAD_LOCATE = 'payload_locate' +CONF_PAYLOAD_START = 'payload_start' +CONF_PAYLOAD_PAUSE = 'payload_pause' +CONF_STATE_TEMPLATE = 'state_template' +CONF_SET_FAN_SPEED_TOPIC = 'set_fan_speed_topic' +CONF_FAN_SPEED_LIST = 'fan_speed_list' +CONF_SEND_COMMAND_TOPIC = 'send_command_topic' + +DEFAULT_NAME = 'MQTT State Vacuum' +DEFAULT_RETAIN = False +DEFAULT_SERVICE_STRINGS = services_to_strings( + DEFAULT_SERVICES, SERVICE_TO_STRING) +DEFAULT_PAYLOAD_RETURN_TO_BASE = 'return_to_base' +DEFAULT_PAYLOAD_STOP = 'stop' +DEFAULT_PAYLOAD_CLEAN_SPOT = 'clean_spot' +DEFAULT_PAYLOAD_LOCATE = 'locate' +DEFAULT_PAYLOAD_START = 'start' +DEFAULT_PAYLOAD_PAUSE = 'pause' + +PLATFORM_SCHEMA_STATE = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_FAN_SPEED_LIST, default=[]): + vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_CLEAN_SPOT, + default=DEFAULT_PAYLOAD_CLEAN_SPOT): cv.string, + vol.Optional(CONF_PAYLOAD_LOCATE, + default=DEFAULT_PAYLOAD_LOCATE): cv.string, + vol.Optional(CONF_PAYLOAD_RETURN_TO_BASE, + default=DEFAULT_PAYLOAD_RETURN_TO_BASE): cv.string, + vol.Optional(CONF_PAYLOAD_START, + default=DEFAULT_PAYLOAD_START): cv.string, + vol.Optional(CONF_PAYLOAD_PAUSE, + default=DEFAULT_PAYLOAD_PAUSE): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS): + vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, +}).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema).extend( + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema).extend(MQTT_VACUUM_SCHEMA.schema) + + +async def async_setup_entity_state(config, async_add_entities, + config_entry, discovery_hash): + """Set up a State MQTT Vacuum.""" + async_add_entities([MqttStateVacuum(config, config_entry, discovery_hash)]) + + +# pylint: disable=too-many-ancestors +class MqttStateVacuum(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, + MqttEntityDeviceInfo, StateVacuumDevice): + """Representation of a MQTT-controlled state vacuum.""" + + def __init__(self, config, config_entry, discovery_info): + """Initialize the vacuum.""" + self._state = None + self._state_attrs = {} + self._fan_speed_list = [] + self._sub_state = None + self._unique_id = config.get(CONF_UNIQUE_ID) + + # Load config + self._setup_from_config(config) + + device_config = config.get(CONF_DEVICE) + + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_info, + self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + + def _setup_from_config(self, config): + self._config = config + self._name = config[CONF_NAME] + supported_feature_strings = config[CONF_SUPPORTED_FEATURES] + self._supported_features = strings_to_services( + supported_feature_strings, STRING_TO_SERVICE + ) + self._fan_speed_list = config[CONF_FAN_SPEED_LIST] + self._command_topic = config.get(mqtt.CONF_COMMAND_TOPIC) + self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC) + self._send_command_topic = config.get(CONF_SEND_COMMAND_TOPIC) + + self._payloads = { + key: config.get(key) for key in ( + CONF_PAYLOAD_START, + CONF_PAYLOAD_PAUSE, + CONF_PAYLOAD_STOP, + CONF_PAYLOAD_RETURN_TO_BASE, + CONF_PAYLOAD_CLEAN_SPOT, + CONF_PAYLOAD_LOCATE + ) + } + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA_STATE(discovery_payload) + self._setup_from_config(config) + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) + await self._subscribe_topics() + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + await subscription.async_unsubscribe_topics(self.hass, self._sub_state) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + template = self._config.get(CONF_VALUE_TEMPLATE) + if template is not None: + template.hass = self.hass + topics = {} + + @callback + def state_message_received(msg): + """Handle state MQTT message.""" + payload = msg.payload + if template is not None: + payload = template.async_render_with_possible_json_value( + payload) + else: + payload = json.loads(payload) + if STATE in payload and payload[STATE] in POSSIBLE_STATES: + self._state = POSSIBLE_STATES[payload[STATE]] + del payload[STATE] + self._state_attrs.update(payload) + self.async_write_ha_state() + + if self._config.get(CONF_STATE_TOPIC): + topics['state_position_topic'] = { + 'topic': self._config.get(CONF_STATE_TOPIC), + 'msg_callback': state_message_received, + 'qos': self._config[CONF_QOS]} + self._sub_state = await subscription.async_subscribe_topics( + self.hass, self._sub_state, topics) + + @property + def name(self): + """Return the name of the vacuum.""" + return self._name + + @property + def state(self): + """Return state of vacuum.""" + return self._state + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def fan_speed(self): + """Return fan speed of the vacuum.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return None + + return self._state_attrs.get(FAN_SPEED, 0) + + @property + def fan_speed_list(self): + """Return fan speed list of the vacuum.""" + if self.supported_features & SUPPORT_FAN_SPEED == 0: + return None + return self._fan_speed_list + + @property + def battery_level(self): + """Return battery level of the vacuum.""" + if self.supported_features & SUPPORT_BATTERY == 0: + return None + return max(0, min(100, self._state_attrs.get(BATTERY, 0))) + + @property + def supported_features(self): + """Flag supported features.""" + return self._supported_features + + async def async_start(self): + """Start the vacuum.""" + if self.supported_features & SUPPORT_START == 0: + return None + mqtt.async_publish(self.hass, self._command_topic, + self._config[CONF_PAYLOAD_START], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_pause(self): + """Pause the vacuum.""" + if self.supported_features & SUPPORT_PAUSE == 0: + return None + mqtt.async_publish(self.hass, self._command_topic, + self._config[CONF_PAYLOAD_PAUSE], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_stop(self, **kwargs): + """Stop the vacuum.""" + if self.supported_features & SUPPORT_STOP == 0: + return None + mqtt.async_publish(self.hass, self._command_topic, + self._config[CONF_PAYLOAD_STOP], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_set_fan_speed(self, fan_speed, **kwargs): + """Set fan speed.""" + if ((self.supported_features & SUPPORT_FAN_SPEED == 0) or + (fan_speed not in self._fan_speed_list)): + return None + mqtt.async_publish(self.hass, self._set_fan_speed_topic, + fan_speed, + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_return_to_base(self, **kwargs): + """Tell the vacuum to return to its dock.""" + if self.supported_features & SUPPORT_RETURN_HOME == 0: + return None + mqtt.async_publish(self.hass, self._command_topic, + self._config[CONF_PAYLOAD_RETURN_TO_BASE], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_clean_spot(self, **kwargs): + """Perform a spot clean-up.""" + if self.supported_features & SUPPORT_CLEAN_SPOT == 0: + return None + mqtt.async_publish(self.hass, self._command_topic, + self._config[CONF_PAYLOAD_CLEAN_SPOT], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_locate(self, **kwargs): + """Locate the vacuum (usually by playing a song).""" + if self.supported_features & SUPPORT_LOCATE == 0: + return None + mqtt.async_publish(self.hass, self._command_topic, + self._config[CONF_PAYLOAD_LOCATE], + self._config[CONF_QOS], + self._config[CONF_RETAIN]) + + async def async_send_command(self, command, params=None, **kwargs): + """Send a command to a vacuum cleaner.""" + if self.supported_features & SUPPORT_SEND_COMMAND == 0: + return None + if params: + message = {"command": command} + message.update(params) + message = json.dumps(message) + else: + message = command + mqtt.async_publish(self.hass, self._send_command_topic, + message, + self._config[CONF_QOS], + self._config[CONF_RETAIN]) diff --git a/tests/components/mqtt/test_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py similarity index 68% rename from tests/components/mqtt/test_vacuum.py rename to tests/components/mqtt/test_legacy_vacuum.py index 78ca45a792f..5a7bf6c2d8b 100644 --- a/tests/components/mqtt/test_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -1,12 +1,14 @@ -"""The tests for the Mqtt vacuum platform.""" -import copy +"""The tests for the Legacy Mqtt vacuum platform.""" +from copy import deepcopy import json -import pytest from homeassistant.components import mqtt, vacuum -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, vacuum as mqttvacuum) +from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.discovery import async_start +from homeassistant.components.mqtt.vacuum import ( + schema_legacy as mqttvacuum, services_to_strings) +from homeassistant.components.mqtt.vacuum.schema_legacy import ( + ALL_SERVICES, SERVICE_TO_STRING) from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, ATTR_BATTERY_LEVEL, ATTR_FAN_SPEED, ATTR_STATUS) from homeassistant.const import ( @@ -17,7 +19,7 @@ from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component) from tests.components.vacuum import common -default_config = { +DEFAULT_CONFIG = { CONF_PLATFORM: 'mqtt', CONF_NAME: 'mqtttest', CONF_COMMAND_TOPIC: 'vacuum/command', @@ -40,115 +42,205 @@ default_config = { } -@pytest.fixture -def mock_publish(hass): - """Initialize components.""" - yield hass.loop.run_until_complete(async_mock_mqtt_component(hass)) - - -async def test_default_supported_features(hass, mock_publish): +async def test_default_supported_features(hass, mqtt_mock): """Test that the correct supported features.""" assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: DEFAULT_CONFIG, }) entity = hass.states.get('vacuum.mqtttest') entity_features = \ entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) - assert sorted(mqttvacuum.services_to_strings(entity_features)) == \ + assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == \ sorted(['turn_on', 'turn_off', 'stop', 'return_home', 'battery', 'status', 'clean_spot']) -async def test_all_commands(hass, mock_publish): +async def test_all_commands(hass, mqtt_mock): """Test simple commands to the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) common.turn_on(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'turn_on', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.turn_off(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'turn_off', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.stop(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'stop', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.clean_spot(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'clean_spot', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.locate(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'locate', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.start_pause(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'start_pause', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.return_to_base(hass, 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'return_to_base', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.set_fan_speed(hass, 'high', 'vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/set_fan_speed', 'high', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - mock_publish.async_publish.assert_called_once_with( + mqtt_mock.async_publish.assert_called_once_with( 'vacuum/send_command', '44 FE 93', 0, False) - mock_publish.async_publish.reset_mock() + mqtt_mock.async_publish.reset_mock() common.send_command(hass, '44 FE 93', {"key": "value"}, entity_id='vacuum.mqtttest') await hass.async_block_till_done() await hass.async_block_till_done() - assert json.loads(mock_publish.async_publish.mock_calls[-1][1][1]) == { + assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { + "command": "44 FE 93", + "key": "value" + } + + common.send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { "command": "44 FE 93", "key": "value" } -async def test_status(hass, mock_publish): - """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) +async def test_commands_without_supported_features(hass, mqtt_mock): + """Test commands which are not supported by the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + services = mqttvacuum.STRING_TO_SERVICE["status"] + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + services_to_strings( + services, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, + }) + + common.turn_on(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.turn_off(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.stop(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.clean_spot(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.locate(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.start_pause(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.return_to_base(hass, 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.set_fan_speed(hass, 'high', 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + +async def test_attributes_without_supported_features(hass, mqtt_mock): + """Test attributes which are not supported by the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + services = mqttvacuum.STRING_TO_SERVICE["turn_on"] + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + services_to_strings( + services, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + state = hass.states.get('vacuum.mqtttest') + assert STATE_OFF == state.state + assert state.attributes.get(ATTR_BATTERY_LEVEL) is None + assert state.attributes.get(ATTR_BATTERY_ICON) is None + + +async def test_status(hass, mqtt_mock): + """Test status updates from the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, }) message = """{ @@ -162,11 +254,10 @@ async def test_status(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_ON == state.state - assert 'mdi:battery-50' == \ - state.attributes.get(ATTR_BATTERY_ICON) - assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 'max' == state.attributes.get(ATTR_FAN_SPEED) + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 + assert state.attributes.get(ATTR_FAN_SPEED) == 'max' message = """{ "battery_level": 61, @@ -180,20 +271,20 @@ async def test_status(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state - assert 'mdi:battery-charging-60' == \ - state.attributes.get(ATTR_BATTERY_ICON) - assert 61 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert 'min' == state.attributes.get(ATTR_FAN_SPEED) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-charging-60' + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 + assert state.attributes.get(ATTR_FAN_SPEED) == 'min' -async def test_status_battery(hass, mock_publish): +async def test_status_battery(hass, mqtt_mock): """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) message = """{ @@ -203,17 +294,17 @@ async def test_status_battery(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert 'mdi:battery-50' == \ - state.attributes.get(ATTR_BATTERY_ICON) + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' -async def test_status_cleaning(hass, mock_publish): +async def test_status_cleaning(hass, mqtt_mock): """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) message = """{ @@ -223,16 +314,17 @@ async def test_status_cleaning(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_ON == state.state + assert state.state == STATE_ON -async def test_status_docked(hass, mock_publish): +async def test_status_docked(hass, mqtt_mock): """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) message = """{ @@ -242,16 +334,17 @@ async def test_status_docked(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state + assert state.state == STATE_OFF -async def test_status_charging(hass, mock_publish): +async def test_status_charging(hass, mqtt_mock): """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) message = """{ @@ -261,17 +354,17 @@ async def test_status_charging(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert 'mdi:battery-outline' == \ - state.attributes.get(ATTR_BATTERY_ICON) + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-outline' -async def test_status_fan_speed(hass, mock_publish): +async def test_status_fan_speed(hass, mqtt_mock): """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) message = """{ @@ -281,16 +374,17 @@ async def test_status_fan_speed(hass, mock_publish): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert 'max' == state.attributes.get(ATTR_FAN_SPEED) + assert state.attributes.get(ATTR_FAN_SPEED) == 'max' -async def test_status_error(hass, mock_publish): +async def test_status_error(hass, mqtt_mock): """Test status updates from the vacuum.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) message = """{ @@ -299,7 +393,7 @@ async def test_status_error(hass, mock_publish): async_fire_mqtt_message(hass, 'vacuum/state', message) await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert 'Error: Error1' == state.attributes.get(ATTR_STATUS) + assert state.attributes.get(ATTR_STATUS) == 'Error: Error1' message = """{ "error": "" @@ -307,49 +401,50 @@ async def test_status_error(hass, mock_publish): async_fire_mqtt_message(hass, 'vacuum/state', message) await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert 'Stopped' == state.attributes.get(ATTR_STATUS) + assert state.attributes.get(ATTR_STATUS) == 'Stopped' -async def test_battery_template(hass, mock_publish): +async def test_battery_template(hass, mqtt_mock): """Test that you can use non-default templates for battery_level.""" - default_config.update({ + config = deepcopy(DEFAULT_CONFIG) + config.update({ mqttvacuum.CONF_SUPPORTED_FEATURES: - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES), + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING), mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE: "{{ value }}" }) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) async_fire_mqtt_message(hass, 'retroroomba/battery_level', '54') await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert 54 == state.attributes.get(ATTR_BATTERY_LEVEL) - assert state.attributes.get(ATTR_BATTERY_ICON) == \ - 'mdi:battery-50' + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' -async def test_status_invalid_json(hass, mock_publish): +async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" - default_config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ - mqttvacuum.services_to_strings(mqttvacuum.ALL_SERVICES) + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings(ALL_SERVICES, SERVICE_TO_STRING) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) async_fire_mqtt_message(hass, 'vacuum/state', '{"asdfasas false}') await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state - assert "Stopped" == state.attributes.get(ATTR_STATUS) + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_missing_battery_template(hass, mock_publish): +async def test_missing_battery_template(hass, mqtt_mock): """Test to make sure missing template is not allowed.""" - config = copy.deepcopy(default_config) + config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -360,9 +455,9 @@ async def test_missing_battery_template(hass, mock_publish): assert state is None -async def test_missing_charging_template(hass, mock_publish): +async def test_missing_charging_template(hass, mqtt_mock): """Test to make sure missing template is not allowed.""" - config = copy.deepcopy(default_config) + config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -373,9 +468,9 @@ async def test_missing_charging_template(hass, mock_publish): assert state is None -async def test_missing_cleaning_template(hass, mock_publish): +async def test_missing_cleaning_template(hass, mqtt_mock): """Test to make sure missing template is not allowed.""" - config = copy.deepcopy(default_config) + config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -386,9 +481,9 @@ async def test_missing_cleaning_template(hass, mock_publish): assert state is None -async def test_missing_docked_template(hass, mock_publish): +async def test_missing_docked_template(hass, mqtt_mock): """Test to make sure missing template is not allowed.""" - config = copy.deepcopy(default_config) + config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -399,9 +494,9 @@ async def test_missing_docked_template(hass, mock_publish): assert state is None -async def test_missing_error_template(hass, mock_publish): +async def test_missing_error_template(hass, mqtt_mock): """Test to make sure missing template is not allowed.""" - config = copy.deepcopy(default_config) + config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -412,9 +507,9 @@ async def test_missing_error_template(hass, mock_publish): assert state is None -async def test_missing_fan_speed_template(hass, mock_publish): +async def test_missing_fan_speed_template(hass, mqtt_mock): """Test to make sure missing template is not allowed.""" - config = copy.deepcopy(default_config) + config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -425,14 +520,15 @@ async def test_missing_fan_speed_template(hass, mock_publish): assert state is None -async def test_default_availability_payload(hass, mock_publish): +async def test_default_availability_payload(hass, mqtt_mock): """Test availability by default payload with defined topic.""" - default_config.update({ + config = deepcopy(DEFAULT_CONFIG) + config.update({ 'availability_topic': 'availability-topic' }) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) state = hass.states.get('vacuum.mqtttest') @@ -453,16 +549,17 @@ async def test_default_availability_payload(hass, mock_publish): assert STATE_UNAVAILABLE == state.state -async def test_custom_availability_payload(hass, mock_publish): +async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - default_config.update({ + config = deepcopy(DEFAULT_CONFIG) + config.update({ 'availability_topic': 'availability-topic', 'payload_available': 'good', 'payload_not_available': 'nogood' }) assert await async_setup_component(hass, vacuum.DOMAIN, { - vacuum.DOMAIN: default_config, + vacuum.DOMAIN: config, }) state = hass.states.get('vacuum.mqtttest') @@ -483,7 +580,7 @@ async def test_custom_availability_payload(hass, mock_publish): assert STATE_UNAVAILABLE == state.state -async def test_discovery_removal_vacuum(hass, mock_publish): +async def test_discovery_removal_vacuum(hass, mqtt_mock): """Test removal of discovered vacuum.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -543,7 +640,7 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): assert state is None -async def test_discovery_update_vacuum(hass, mock_publish): +async def test_discovery_update_vacuum(hass, mqtt_mock): """Test update of discovered vacuum.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, 'homeassistant', {}, entry) @@ -592,7 +689,7 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): await hass.async_block_till_done() state = hass.states.get('vacuum.test') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): @@ -614,7 +711,7 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): assert 'JSON result was not a dictionary' in caplog.text -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): """Test attributes get extracted from a JSON result.""" assert await async_setup_component(hass, vacuum.DOMAIN, { vacuum.DOMAIN: { @@ -654,7 +751,7 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Change json_attributes_topic async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', @@ -667,17 +764,17 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.beer') - assert '100' == state.attributes.get('val') + assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get('vacuum.beer') - assert '75' == state.attributes.get('val') + assert state.attributes.get('val') == '75' -async def test_unique_id(hass, mock_publish): +async def test_unique_id(hass, mqtt_mock): """Test unique id option only creates one vacuum per unique_id.""" await async_mock_mqtt_component(hass) assert await async_setup_component(hass, vacuum.DOMAIN, { @@ -702,7 +799,7 @@ async def test_unique_id(hass, mock_publish): # all vacuums group is 1, unique id created is 1 -async def test_entity_device_info_with_identifier(hass, mock_publish): +async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT vacuum device registry integration.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) entry.add_to_hass(hass) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py new file mode 100644 index 00000000000..0c871fdcfd0 --- /dev/null +++ b/tests/components/mqtt/test_state_vacuum.py @@ -0,0 +1,685 @@ +"""The tests for the State vacuum Mqtt platform.""" +from copy import deepcopy +import json + +from homeassistant.components import mqtt, vacuum +from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC +from homeassistant.components.mqtt.discovery import async_start +from homeassistant.components.mqtt.vacuum import ( + CONF_SCHEMA, schema_state as mqttvacuum, services_to_strings) +from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING +from homeassistant.components.vacuum import ( + ATTR_BATTERY_ICON, ATTR_BATTERY_LEVEL, ATTR_FAN_SPEED, ATTR_FAN_SPEED_LIST, + DOMAIN, SERVICE_CLEAN_SPOT, SERVICE_LOCATE, SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, SERVICE_START, SERVICE_STOP, STATE_CLEANING, + STATE_DOCKED) +from homeassistant.const import ( + CONF_NAME, CONF_PLATFORM, STATE_UNAVAILABLE, STATE_UNKNOWN) +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component) +from tests.components.vacuum import common + +COMMAND_TOPIC = 'vacuum/command' +SEND_COMMAND_TOPIC = 'vacuum/send_command' +STATE_TOPIC = 'vacuum/state' + +DEFAULT_CONFIG = { + CONF_PLATFORM: 'mqtt', + CONF_SCHEMA: 'state', + CONF_NAME: 'mqtttest', + CONF_COMMAND_TOPIC: COMMAND_TOPIC, + mqttvacuum.CONF_SEND_COMMAND_TOPIC: SEND_COMMAND_TOPIC, + CONF_STATE_TOPIC: STATE_TOPIC, + mqttvacuum.CONF_SET_FAN_SPEED_TOPIC: 'vacuum/set_fan_speed', + mqttvacuum.CONF_FAN_SPEED_LIST: ['min', 'medium', 'high', 'max'], +} + + +async def test_default_supported_features(hass, mqtt_mock): + """Test that the correct supported features.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: DEFAULT_CONFIG, + }) + entity = hass.states.get('vacuum.mqtttest') + entity_features = \ + entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) + assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == \ + sorted(['start', 'stop', + 'return_home', 'battery', 'status', + 'clean_spot']) + + +async def test_all_commands(hass, mqtt_mock): + """Test simple commands send to the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + services_to_strings( + mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + await hass.services.async_call( + DOMAIN, SERVICE_START, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + COMMAND_TOPIC, 'start', 0, False) + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_STOP, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + COMMAND_TOPIC, 'stop', 0, False) + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + COMMAND_TOPIC, 'pause', 0, False) + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + COMMAND_TOPIC, 'locate', 0, False) + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + COMMAND_TOPIC, 'clean_spot', 0, False) + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + COMMAND_TOPIC, 'return_to_base', 0, False) + mqtt_mock.async_publish.reset_mock() + + common.set_fan_speed(hass, 'medium', 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'vacuum/set_fan_speed', 'medium', 0, False) + mqtt_mock.async_publish.reset_mock() + + common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_called_once_with( + 'vacuum/send_command', '44 FE 93', 0, False) + mqtt_mock.async_publish.reset_mock() + + common.send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { + "command": "44 FE 93", + "key": "value" + } + + +async def test_commands_without_supported_features(hass, mqtt_mock): + """Test commands which are not supported by the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + services = mqttvacuum.STRING_TO_SERVICE["status"] + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + services_to_strings( + services, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + await hass.services.async_call( + DOMAIN, SERVICE_START, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_STOP, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.set_fan_speed(hass, 'medium', 'vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + mqtt_mock.async_publish.reset_mock() + + common.send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.assert_not_called() + + +async def test_status(hass, mqtt_mock): + """Test status updates from the vacuum.""" + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + services_to_strings(mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + message = """{ + "battery_level": 54, + "state": "cleaning", + "fan_speed": "max" + }""" + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_CLEANING + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' + assert state.attributes.get(ATTR_FAN_SPEED) == 'max' + + message = """{ + "battery_level": 61, + "state": "docked", + "fan_speed": "min" + }""" + + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_DOCKED + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-charging-60' + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 + assert state.attributes.get(ATTR_FAN_SPEED) == 'min' + assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ['min', 'medium', + 'high', 'max'] + + +async def test_no_fan_vacuum(hass, mqtt_mock): + """Test status updates from the vacuum when fan is not supported.""" + config = deepcopy(DEFAULT_CONFIG) + del config[mqttvacuum.CONF_FAN_SPEED_LIST] + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + services_to_strings(mqttvacuum.DEFAULT_SERVICES, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + message = """{ + "battery_level": 54, + "state": "cleaning" + }""" + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_CLEANING + assert state.attributes.get(ATTR_FAN_SPEED) is None + assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' + + message = """{ + "battery_level": 54, + "state": "cleaning", + "fan_speed": "max" + }""" + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + + assert state.state == STATE_CLEANING + assert state.attributes.get(ATTR_FAN_SPEED) is None + assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None + + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' + + message = """{ + "battery_level": 61, + "state": "docked" + }""" + + async_fire_mqtt_message(hass, 'vacuum/state', message) + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_DOCKED + assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-charging-60' + assert state.attributes.get(ATTR_BATTERY_LEVEL) == 61 + + +async def test_status_invalid_json(hass, mqtt_mock): + """Test to make sure nothing breaks if the vacuum sends bad JSON.""" + config = deepcopy(DEFAULT_CONFIG) + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = \ + mqttvacuum.services_to_strings( + mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + async_fire_mqtt_message(hass, 'vacuum/state', '{"asdfasas false}') + await hass.async_block_till_done() + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_UNKNOWN + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + config = deepcopy(DEFAULT_CONFIG) + config.update({ + 'availability_topic': 'availability-topic' + }) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, 'availability-topic', 'online') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_UNAVAILABLE + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + config = deepcopy(DEFAULT_CONFIG) + config.update({ + 'availability_topic': 'availability-topic', + 'payload_available': 'good', + 'payload_not_available': 'nogood' + }) + + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: config, + }) + + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.mqtttest') + assert state.state == STATE_UNAVAILABLE + + +async def test_discovery_removal_vacuum(hass, mqtt_mock): + """Test removal of discovered vacuum.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "component": "state" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', '') + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is None + + +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic#",' + ' "component": "state" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic",' + ' "component": "state" }' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is None + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.milk') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('vacuum.beer') + assert state is None + + +async def test_discovery_update_vacuum(hass, mqtt_mock): + """Test update of discovered vacuum.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + '"component": "state" }' + ) + data2 = ( + '{ "name": "Milk",' + ' "command_topic": "test_topic",' + ' "component": "state"}' + ) + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Beer' + + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + state = hass.states.get('vacuum.beer') + assert state is not None + assert state.name == 'Milk' + state = hass.states.get('vacuum.milk') + assert state is None + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') + await hass.async_block_till_done() + state = hass.states.get('vacuum.test') + + assert state.attributes.get('val') == '100' + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') + await hass.async_block_till_done() + state = hass.states.get('vacuum.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'test-topic', + 'json_attributes_topic': 'attr-topic' + } + }) + + async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') + await hass.async_block_till_done() + + state = hass.states.get('vacuum.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + await async_start(hass, 'homeassistant', {}, entry) + data1 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic1" }' + ) + data2 = ( + '{ "name": "Beer",' + ' "command_topic": "test_topic",' + ' "json_attributes_topic": "attr-topic2" }' + ) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.beer') + assert state.attributes.get('val') == '100' + + # Change json_attributes_topic + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data2) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Verify we are no longer subscribing to the old topic + async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.beer') + assert state.attributes.get('val') == '100' + + # Verify we are subscribing to the new topic + async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') + await hass.async_block_till_done() + await hass.async_block_till_done() + state = hass.states.get('vacuum.beer') + assert state.attributes.get('val') == '75' + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one vacuum per unique_id.""" + await async_mock_mqtt_component(hass) + assert await async_setup_component(hass, vacuum.DOMAIN, { + vacuum.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + async_fire_mqtt_message(hass, 'test-topic', 'payload') + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 2 + # all vacuums group is 1, unique id created is 1 + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT vacuum device registry integration.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps({ + 'platform': 'mqtt', + 'name': 'Test 1', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + }) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.identifiers == {('mqtt', 'helloworld')} + assert device.connections == {('mac', "02:5b:26:a8:dc:12")} + assert device.manufacturer == 'Whatever' + assert device.name == 'Beer' + assert device.model == 'Glass' + assert device.sw_version == '0.1-beta' + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, 'homeassistant', {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'test-command-topic', + 'device': { + 'identifiers': ['helloworld'], + 'connections': [ + ["mac", "02:5b:26:a8:dc:12"], + ], + 'manufacturer': 'Whatever', + 'name': 'Beer', + 'model': 'Glass', + 'sw_version': '0.1-beta', + }, + 'unique_id': 'veryunique' + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Beer' + + config['device']['name'] = 'Milk' + data = json.dumps(config) + async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', + data) + await hass.async_block_till_done() + await hass.async_block_till_done() + + device = registry.async_get_device({('mqtt', 'helloworld')}, set()) + assert device is not None + assert device.name == 'Milk' From d0f9595ad9b3866008e472ce88af288fba4ba399 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 22 Apr 2019 22:44:46 +0200 Subject: [PATCH 079/346] Add connection control for netgear_lte (#22946) --- .../components/netgear_lte/__init__.py | 58 ++++++++++++++++--- .../components/netgear_lte/services.yaml | 20 +++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 5491fffe969..0d349f8756e 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -35,11 +35,18 @@ DATA_KEY = 'netgear_lte' EVENT_SMS = 'netgear_lte_sms' SERVICE_DELETE_SMS = 'delete_sms' +SERVICE_SET_OPTION = 'set_option' +SERVICE_CONNECT_LTE = 'connect_lte' ATTR_HOST = 'host' ATTR_SMS_ID = 'sms_id' ATTR_FROM = 'from' ATTR_MESSAGE = 'message' +ATTR_FAILOVER = 'failover' +ATTR_AUTOCONNECT = 'autoconnect' + +FAILOVER_MODES = ['auto', 'wire', 'mobile'] +AUTOCONNECT_MODES = ['never', 'home', 'always'] NOTIFY_SCHEMA = vol.Schema({ @@ -74,10 +81,22 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) DELETE_SMS_SCHEMA = vol.Schema({ - vol.Required(ATTR_HOST): cv.string, + vol.Optional(ATTR_HOST): cv.string, vol.Required(ATTR_SMS_ID): vol.All(cv.ensure_list, [cv.positive_int]), }) +SET_OPTION_SCHEMA = vol.Schema( + vol.All(cv.has_at_least_one_key(ATTR_FAILOVER, ATTR_AUTOCONNECT), { + vol.Optional(ATTR_HOST): cv.string, + vol.Optional(ATTR_FAILOVER): vol.In(FAILOVER_MODES), + vol.Optional(ATTR_AUTOCONNECT): vol.In(AUTOCONNECT_MODES), + }) +) + +CONNECT_LTE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_HOST): cv.string, +}) + @attr.s class ModemData: @@ -116,7 +135,11 @@ class LTEData: def get_modem_data(self, config): """Get modem_data for the host in config.""" - return self.modem_data.get(config[CONF_HOST]) + if config[CONF_HOST] is not None: + return self.modem_data.get(config[CONF_HOST]) + if len(self.modem_data) != 1: + return None + return next(iter(self.modem_data.values())) async def async_setup(hass, config): @@ -126,24 +149,43 @@ async def async_setup(hass, config): hass, cookie_jar=aiohttp.CookieJar(unsafe=True)) hass.data[DATA_KEY] = LTEData(websession) - async def delete_sms_handler(service): + async def service_handler(service): """Apply a service.""" - host = service.data[ATTR_HOST] + host = service.data.get(ATTR_HOST) conf = {CONF_HOST: host} modem_data = hass.data[DATA_KEY].get_modem_data(conf) if not modem_data: _LOGGER.error( - "%s: host %s unavailable", SERVICE_DELETE_SMS, host) + "%s: host %s unavailable", service.service, host) return - for sms_id in service.data[ATTR_SMS_ID]: - await modem_data.modem.delete_sms(sms_id) + if service.service == SERVICE_DELETE_SMS: + for sms_id in service.data[ATTR_SMS_ID]: + await modem_data.modem.delete_sms(sms_id) + elif service.service == SERVICE_SET_OPTION: + failover = service.data.get(ATTR_FAILOVER) + if failover: + await modem_data.modem.set_failover_mode(failover) + + autoconnect = service.data.get(ATTR_AUTOCONNECT) + if autoconnect: + await modem_data.modem.set_autoconnect_mode(autoconnect) + elif service.service == SERVICE_CONNECT_LTE: + await modem_data.modem.connect_lte() hass.services.async_register( - DOMAIN, SERVICE_DELETE_SMS, delete_sms_handler, + DOMAIN, SERVICE_DELETE_SMS, service_handler, schema=DELETE_SMS_SCHEMA) + hass.services.async_register( + DOMAIN, SERVICE_SET_OPTION, service_handler, + schema=SET_OPTION_SCHEMA) + + hass.services.async_register( + DOMAIN, SERVICE_CONNECT_LTE, service_handler, + schema=CONNECT_LTE_SCHEMA) + netgear_lte_config = config[DOMAIN] # Set up each modem diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index 8f61e7a44b5..4ba3afb07b4 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -7,3 +7,23 @@ delete_sms: sms_id: description: Integer or list of integers with inbox IDs of messages to delete. example: 7 + +set_option: + description: Set options on the modem. + fields: + host: + description: The modem to set options on. + example: 192.168.5.1 + failover: + description: Failover mode, auto/wire/mobile. + example: auto + autoconnect: + description: Auto-connect mode, never/home/always. + example: home + +connect_lte: + description: Ask the modem to establish the LTE connection. + fields: + host: + description: The modem that should connect. + example: 192.168.5.1 From 845d81bdae20243d03e88f47f5dd5ef193debe9b Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Tue, 23 Apr 2019 05:28:40 +0100 Subject: [PATCH 080/346] Correct calculation and units of light level values. (#23309) --- homeassistant/components/hue/sensor.py | 9 +++++++-- tests/components/hue/test_sensor_base.py | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 555c16a0be7..30a439f92e9 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -27,12 +27,17 @@ class HueLightLevel(GenericHueGaugeSensorEntity): """The light level sensor entity for a Hue motion sensor device.""" device_class = DEVICE_CLASS_ILLUMINANCE - unit_of_measurement = "Lux" + unit_of_measurement = "lx" @property def state(self): """Return the state of the device.""" - return self.sensor.lightlevel + # https://developers.meethue.com/develop/hue-api/supported-devices/#clip_zll_lightlevel + # Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm + # scale used because the human eye adjusts to light levels and small + # changes at low lux levels are more noticeable than at high lux + # levels. + return 10 ** ((self.sensor.lightlevel - 1) / 10000) @property def device_state_attributes(self): diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index 38eb3d8c55b..6259921dcfb 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -48,7 +48,7 @@ PRESENCE_SENSOR_1_PRESENT = { } LIGHT_LEVEL_SENSOR_1 = { "state": { - "lightlevel": 0, + "lightlevel": 1, "dark": True, "daylight": True, "lastupdated": "2019-01-01T01:00:00" @@ -141,7 +141,7 @@ PRESENCE_SENSOR_2_NOT_PRESENT = { } LIGHT_LEVEL_SENSOR_2 = { "state": { - "lightlevel": 100, + "lightlevel": 10001, "dark": True, "daylight": True, "lastupdated": "2019-01-01T01:00:00" @@ -234,7 +234,7 @@ PRESENCE_SENSOR_3_PRESENT = { } LIGHT_LEVEL_SENSOR_3 = { "state": { - "lightlevel": 0, + "lightlevel": 1, "dark": True, "daylight": True, "lastupdated": "2019-01-01T01:00:00" @@ -399,7 +399,7 @@ async def test_sensors(hass, mock_bridge): assert presence_sensor_1 is not None assert presence_sensor_1.state == 'on' assert light_level_sensor_1 is not None - assert light_level_sensor_1.state == '0' + assert light_level_sensor_1.state == '1.0' assert light_level_sensor_1.name == 'Living room sensor light level' assert temperature_sensor_1 is not None assert temperature_sensor_1.state == '17.75' @@ -414,7 +414,7 @@ async def test_sensors(hass, mock_bridge): assert presence_sensor_2 is not None assert presence_sensor_2.state == 'off' assert light_level_sensor_2 is not None - assert light_level_sensor_2.state == '100' + assert light_level_sensor_2.state == '10.0' assert light_level_sensor_2.name == 'Kitchen sensor light level' assert temperature_sensor_2 is not None assert temperature_sensor_2.state == '18.75' From ee88433fb10fe7e8ea95ca08c6622de46a8b34fd Mon Sep 17 00:00:00 2001 From: VDRainer <26381449+VDRainer@users.noreply.github.com> Date: Tue, 23 Apr 2019 06:29:34 +0200 Subject: [PATCH 081/346] Create services.yaml for input_datetime (#23303) * Create services.yaml for input_datetime * HA error while parsing a flow mapping --- homeassistant/components/input_datetime/services.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 homeassistant/components/input_datetime/services.yaml diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml new file mode 100644 index 00000000000..9534ad3f696 --- /dev/null +++ b/homeassistant/components/input_datetime/services.yaml @@ -0,0 +1,9 @@ +set_datetime: + description: This can be used to dynamically set the date and/or time. + fields: + entity_id: {description: Entity id of the input datetime to set the new value., + example: input_datetime.test_date_time} + date: {description: The target date the entity should be set to., + example: '"date": "2019-04-22"'} + time: {description: The target time the entity should be set to., + example: '"time": "05:30:00"'} From baeb3cddc6cb5a99a13d2280b9498b25cc68762b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 23 Apr 2019 05:32:39 +0100 Subject: [PATCH 082/346] Set placeholders in homekit config flow title (#23311) --- .../homekit_controller/config_flow.py | 5 + .../homekit_controller/strings.json | 1 + .../homekit_controller/test_config_flow.py | 103 +++++++++++------- 3 files changed, 68 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index a6c5ac8b36d..310f187556d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -130,6 +130,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): status_flags = int(properties['sf']) paired = not status_flags & 0x01 + # pylint: disable=unsupported-assignment-operation + self.context['title_placeholders'] = { + 'name': discovery_info['name'], + } + # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index b1601a1f33e..075bf6ca6cd 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "HomeKit Accessory", + "flow_title": "HomeKit Accessory: {name}", "step": { "user": { "title": "Pair with HomeKit Accessory", diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index da4176e1edc..2dd42737477 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -12,9 +12,17 @@ from tests.components.homekit_controller.common import ( ) +def _setup_flow_handler(hass): + flow = config_flow.HomekitControllerFlowHandler() + flow.hass = hass + flow.context = {} + return flow + + async def test_discovery_works(hass): """Test a device being discovered.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -25,12 +33,12 @@ async def test_discovery_works(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} pairing = mock.Mock(pairing_data={ 'AccessoryPairingID': '00:00:00:00:00:00', @@ -66,6 +74,7 @@ async def test_discovery_works(hass): async def test_discovery_works_upper_case(hass): """Test a device being discovered.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -76,12 +85,12 @@ async def test_discovery_works_upper_case(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} pairing = mock.Mock(pairing_data={ 'AccessoryPairingID': '00:00:00:00:00:00', @@ -117,6 +126,7 @@ async def test_discovery_works_upper_case(hass): async def test_discovery_works_missing_csharp(hass): """Test a device being discovered that has missing mdns attrs.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -126,12 +136,12 @@ async def test_discovery_works_missing_csharp(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} pairing = mock.Mock(pairing_data={ 'AccessoryPairingID': '00:00:00:00:00:00', @@ -167,6 +177,7 @@ async def test_discovery_works_missing_csharp(hass): async def test_pair_already_paired_1(hass): """Already paired.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -177,17 +188,18 @@ async def test_pair_already_paired_1(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_paired' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} async def test_discovery_ignored_model(hass): """Already paired.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -198,12 +210,12 @@ async def test_discovery_ignored_model(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'ignored_model' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} async def test_discovery_invalid_config_entry(hass): @@ -216,6 +228,7 @@ async def test_discovery_invalid_config_entry(hass): assert len(hass.config_entries.async_entries()) == 1 discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -226,12 +239,12 @@ async def test_discovery_invalid_config_entry(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} # Discovery of a HKID that is in a pairable state but for which there is # already a config entry - in that case the stale config entry is @@ -243,6 +256,7 @@ async def test_discovery_invalid_config_entry(hass): async def test_discovery_already_configured(hass): """Already configured.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -259,12 +273,12 @@ async def test_discovery_already_configured(hass): conn.config_num = 1 hass.data[KNOWN_DEVICES]['00:00:00:00:00:00'] = conn - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} assert conn.async_config_num_changed.call_count == 0 @@ -272,6 +286,7 @@ async def test_discovery_already_configured(hass): async def test_discovery_already_configured_config_change(hass): """Already configured.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -288,12 +303,12 @@ async def test_discovery_already_configured_config_change(hass): conn.config_num = 1 hass.data[KNOWN_DEVICES]['00:00:00:00:00:00'] = conn - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'abort' assert result['reason'] == 'already_configured' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} assert conn.async_refresh_entity_map.call_args == mock.call(2) @@ -301,6 +316,7 @@ async def test_discovery_already_configured_config_change(hass): async def test_pair_unable_to_pair(hass): """Pairing completed without exception, but didn't create a pairing.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -311,12 +327,12 @@ async def test_pair_unable_to_pair(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} controller = mock.Mock() controller.pairings = {} @@ -334,6 +350,7 @@ async def test_pair_unable_to_pair(hass): async def test_pair_authentication_error(hass): """Pairing code is incorrect.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -344,12 +361,12 @@ async def test_pair_authentication_error(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} controller = mock.Mock() controller.pairings = {} @@ -369,6 +386,7 @@ async def test_pair_authentication_error(hass): async def test_pair_unknown_error(hass): """Pairing failed for an unknown rason.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -379,12 +397,12 @@ async def test_pair_unknown_error(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} controller = mock.Mock() controller.pairings = {} @@ -404,6 +422,7 @@ async def test_pair_unknown_error(hass): async def test_pair_already_paired(hass): """Device is already paired.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -414,12 +433,12 @@ async def test_pair_already_paired(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_step_discovery(discovery_info) assert result['type'] == 'form' assert result['step_id'] == 'pair' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} controller = mock.Mock() controller.pairings = {} @@ -439,6 +458,7 @@ async def test_pair_already_paired(hass): async def test_import_works(hass): """Test a device being discovered.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -468,8 +488,7 @@ async def test_import_works(hass): }] }] - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" @@ -486,6 +505,7 @@ async def test_import_works(hass): async def test_import_already_configured(hass): """Test importing a device from .homekit that is already a ConfigEntry.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -502,12 +522,11 @@ async def test_import_already_configured(hass): config_entry = MockConfigEntry( domain='homekit_controller', - data=import_info + data=import_info, ) config_entry.add_to_hass(hass) - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) result = await flow.async_import_legacy_pairing( discovery_info['properties'], import_info) @@ -518,6 +537,7 @@ async def test_import_already_configured(hass): async def test_user_works(hass): """Test user initiated disovers devices.""" discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -550,8 +570,7 @@ async def test_user_works(hass): discovery_info, ] - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) with mock.patch('homekit.Controller') as controller_cls: controller_cls.return_value = controller @@ -577,8 +596,7 @@ async def test_user_works(hass): async def test_user_no_devices(hass): """Test user initiated pairing where no devices discovered.""" - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) with mock.patch('homekit.Controller') as controller_cls: controller_cls.return_value.discover.return_value = [] @@ -590,10 +608,10 @@ async def test_user_no_devices(hass): async def test_user_no_unpaired_devices(hass): """Test user initiated pairing where no unpaired devices discovered.""" - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -638,6 +656,7 @@ async def test_parse_new_homekit_json(hass): mock_open = mock.mock_open(read_data=json.dumps(read_data)) discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -648,8 +667,7 @@ async def test_parse_new_homekit_json(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" @@ -662,6 +680,7 @@ async def test_parse_new_homekit_json(hass): assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} async def test_parse_old_homekit_json(hass): @@ -694,6 +713,7 @@ async def test_parse_old_homekit_json(hass): mock_open = mock.mock_open(read_data=json.dumps(read_data)) discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -704,8 +724,7 @@ async def test_parse_old_homekit_json(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" @@ -719,6 +738,7 @@ async def test_parse_old_homekit_json(hass): assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} async def test_parse_overlapping_homekit_json(hass): @@ -762,6 +782,7 @@ async def test_parse_overlapping_homekit_json(hass): side_effects = [mock_open_1.return_value, mock_open_2.return_value] discovery_info = { + 'name': 'TestDevice', 'host': '127.0.0.1', 'port': 8080, 'properties': { @@ -772,8 +793,7 @@ async def test_parse_overlapping_homekit_json(hass): } } - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass + flow = _setup_flow_handler(hass) pairing_cls_imp = "homekit.controller.ip_implementation.IpPairing" @@ -789,3 +809,4 @@ async def test_parse_overlapping_homekit_json(hass): assert result['type'] == 'create_entry' assert result['title'] == 'TestDevice' assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00' + assert flow.context == {'title_placeholders': {'name': 'TestDevice'}} From 2a720efbd481c8fc89022bf1396d2df5d4c5c610 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 23 Apr 2019 06:47:12 +0200 Subject: [PATCH 083/346] Fix hass.io panel_custom/frontend (#23313) * Fix hass.io panel_custom/frontend * Update manifest.json --- homeassistant/components/hassio/addon_panel.py | 2 +- homeassistant/components/hassio/manifest.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index d19ca23799a..7291a87e954 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -79,7 +79,7 @@ def _register_panel(hass, addon, data): Return coroutine. """ - return hass.components.frontend.async_register_built_in_panel( + return hass.components.panel_custom.async_register_panel( frontend_url_path=addon, webcomponent_name='hassio-main', sidebar_title=data[ATTR_TITLE], diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 24782e45799..23095064d55 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -5,7 +5,6 @@ "requirements": [], "dependencies": [ "http", - "frontend", "panel_custom" ], "codeowners": [ From 72bbe2203e0496c41e027aa34b061347fecf0091 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 22 Apr 2019 22:06:58 -0700 Subject: [PATCH 084/346] Dont cache integrations that are not found (#23316) --- homeassistant/loader.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ed2ea83afb0..fb2c1bae894 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -161,11 +161,13 @@ async def async_get_integration(hass: 'HomeAssistant', domain: str)\ await int_or_evt.wait() int_or_evt = cache.get(domain, _UNDEF) - if int_or_evt is _UNDEF: - pass - elif int_or_evt is None: - raise IntegrationNotFound(domain) - else: + # When we have waited and it's _UNDEF, it doesn't exist + # We don't cache that it doesn't exist, or else people can't fix it + # and then restart, because their config will never be valid. + if int_or_evt is _UNDEF: + raise IntegrationNotFound(domain) + + if int_or_evt is not _UNDEF: return cast(Integration, int_or_evt) event = cache[domain] = asyncio.Event() @@ -197,7 +199,12 @@ async def async_get_integration(hass: 'HomeAssistant', domain: str)\ return integration integration = Integration.resolve_legacy(hass, domain) - cache[domain] = integration + if integration is not None: + cache[domain] = integration + else: + # Remove event from cache. + cache.pop(domain) + event.set() if not integration: From ddb5ff3b71340889bcde6f546380478ed97aafa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 23 Apr 2019 07:07:56 +0200 Subject: [PATCH 085/346] Show correct version for stable (#23291) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 5684a3c64d1..16d11e913f7 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/components/version", "requirements": [ - "pyhaversion==2.2.0" + "pyhaversion==2.2.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 33ed716b336..fa5ee0cc221 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1079,7 +1079,7 @@ pygtfs==0.1.5 pygtt==1.1.2 # homeassistant.components.version -pyhaversion==2.2.0 +pyhaversion==2.2.1 # homeassistant.components.heos pyheos==0.4.0 From 00d26b304984aa953bf5cc6fe2ca21953d86c9b0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 22 Apr 2019 23:34:37 -0700 Subject: [PATCH 086/346] Random hassfest fixes (#23314) --- script/hassfest/__main__.py | 12 +++++++----- script/hassfest/dependencies.py | 2 +- script/hassfest/services.py | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index b555f98d883..bca419126db 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -35,10 +35,9 @@ def main(): integrations = Integration.load_dir( pathlib.Path('homeassistant/components') ) - manifest.validate(integrations, config) - dependencies.validate(integrations, config) - codeowners.validate(integrations, config) - services.validate(integrations, config) + + for plugin in PLUGINS: + plugin.validate(integrations, config) # When we generate, all errors that are fixable will be ignored, # as generating them will be fixed. @@ -59,7 +58,10 @@ def main(): print("Invalid integrations:", len(invalid_itg)) if not invalid_itg and not general_errors: - codeowners.generate(integrations, config) + for plugin in PLUGINS: + if hasattr(plugin, 'generate'): + plugin.generate(integrations, config) + return 0 print() diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 25553be1124..f0f14ad21a4 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -68,5 +68,5 @@ def validate(integrations: Dict[str, Integration], config): if dep not in integrations: integration.add_error( 'dependencies', - "Dependency {} does not exist" + "Dependency {} does not exist".format(dep) ) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 4be366b3d55..8750f9a6982 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -59,7 +59,7 @@ def validate_services(integration: Integration): """Validate services.""" # Find if integration uses services has_services = grep_dir(integration.path, "**/*.py", - r"hass\.(services|async_register)") + r"hass\.services\.(register|async_register)") if not has_services: return From 5b0ee473b6ec7d7f636e814a6c0c661537317748 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Apr 2019 03:46:23 -0700 Subject: [PATCH 087/346] Add get_states faster (#23315) --- homeassistant/auth/permissions/__init__.py | 13 ++++++++++++ homeassistant/auth/permissions/util.py | 14 +++++++++++++ .../components/websocket_api/commands.py | 13 +++++++----- tests/auth/permissions/test_util.py | 21 +++++++++++++++++++ 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 tests/auth/permissions/test_util.py diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 63e76dd2496..0079f11447b 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -11,6 +11,7 @@ from .models import PermissionLookup from .types import PolicyType from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .merge import merge_policies # noqa +from .util import test_all POLICY_SCHEMA = vol.Schema({ @@ -29,6 +30,10 @@ class AbstractPermissions: """Return a function that can test entity access.""" raise NotImplementedError + def access_all_entities(self, key: str) -> bool: + """Check if we have a certain access to all entities.""" + raise NotImplementedError + def check_entity(self, entity_id: str, key: str) -> bool: """Check if we can access entity.""" entity_func = self._cached_entity_func @@ -48,6 +53,10 @@ class PolicyPermissions(AbstractPermissions): self._policy = policy self._perm_lookup = perm_lookup + def access_all_entities(self, key: str) -> bool: + """Check if we have a certain access to all entities.""" + return test_all(self._policy.get(CAT_ENTITIES), key) + def _entity_func(self) -> Callable[[str, str], bool]: """Return a function that can test entity access.""" return compile_entities(self._policy.get(CAT_ENTITIES), @@ -65,6 +74,10 @@ class _OwnerPermissions(AbstractPermissions): # pylint: disable=no-self-use + def access_all_entities(self, key: str) -> bool: + """Check if we have a certain access to all entities.""" + return True + def _entity_func(self) -> Callable[[str, str], bool]: """Return a function that can test entity access.""" return lambda entity_id, key: True diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index d2d259fb32e..0d334c4a3ba 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -3,6 +3,7 @@ from functools import wraps from typing import Callable, Dict, List, Optional, Union, cast # noqa: F401 +from .const import SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType @@ -96,3 +97,16 @@ def _gen_dict_test_func( return schema.get(key) return test_value + + +def test_all(policy: CategoryType, key: str) -> bool: + """Test if a policy has an ALL access for a specific key.""" + if not isinstance(policy, dict): + return bool(policy) + + all_policy = policy.get(SUBCAT_ALL) + + if not isinstance(all_policy, dict): + return bool(all_policy) + + return all_policy.get(key, False) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index d9834758c80..84178beef8b 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -142,11 +142,14 @@ def handle_get_states(hass, connection, msg): Async friendly. """ - entity_perm = connection.user.permissions.check_entity - states = [ - state for state in hass.states.async_all() - if entity_perm(state.entity_id, 'read') - ] + if connection.user.permissions.access_all_entities('read'): + states = hass.states.async_all() + else: + entity_perm = connection.user.permissions.check_entity + states = [ + state for state in hass.states.async_all() + if entity_perm(state.entity_id, 'read') + ] connection.send_message(messages.result_message( msg['id'], states)) diff --git a/tests/auth/permissions/test_util.py b/tests/auth/permissions/test_util.py new file mode 100644 index 00000000000..1a339208f4d --- /dev/null +++ b/tests/auth/permissions/test_util.py @@ -0,0 +1,21 @@ +"""Test the permission utils.""" + +from homeassistant.auth.permissions import util + + +def test_test_all(): + """Test if we can test the all group.""" + for val in ( + None, + {}, + {'all': None}, + {'all': {}}, + ): + assert util.test_all(val, 'read') is False + + for val in ( + True, + {'all': True}, + {'all': {'read': True}}, + ): + assert util.test_all(val, 'read') is True From 2871a650f69257fa6e8e4254527007f964e2f806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 23 Apr 2019 14:46:11 +0200 Subject: [PATCH 088/346] Handle traccar connection errors (#23289) * Handle connection errors * Fix lint issue E127 * Remove periods from logs * Merge connection checks * Fail with bad credentials * Move stuff around for async_init * Fix E128 linting issue * Simplify --- .../components/traccar/device_tracker.py | 26 ++++++++++++++----- .../components/traccar/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 39d1c2dd370..b3e2b2833c2 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -109,26 +109,38 @@ class TraccarScanner: self._scan_interval = scan_interval self._async_see = async_see self._api = api + self.connected = False self._hass = hass async def async_init(self): """Further initialize connection to Traccar.""" await self._api.test_connection() - if self._api.authenticated: - await self._async_update() - async_track_time_interval(self._hass, - self._async_update, - self._scan_interval) + if self._api.connected and not self._api.authenticated: + _LOGGER.error("Authentication for Traccar failed") + return False - return self._api.authenticated + await self._async_update() + async_track_time_interval(self._hass, + self._async_update, + self._scan_interval) + return True async def _async_update(self, now=None): """Update info from Traccar.""" - _LOGGER.debug('Updating device data.') + if not self.connected: + _LOGGER.debug('Testing connection to Traccar') + await self._api.test_connection() + self.connected = self._api.connected + if self.connected: + _LOGGER.info("Connection to Traccar restored") + else: + return + _LOGGER.debug('Updating device data') await self._api.get_device_info(self._custom_attributes) self._hass.async_create_task(self.import_device_data()) if self._event_types: self._hass.async_create_task(self.import_events()) + self.connected = self._api.connected async def import_device_data(self): """Import device data from Traccar.""" diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 5c859fefb71..0f9aa6e8464 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,7 +3,7 @@ "name": "Traccar", "documentation": "https://www.home-assistant.io/components/traccar", "requirements": [ - "pytraccar==0.7.0", + "pytraccar==0.8.0", "stringcase==1.2.0" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index fa5ee0cc221..10e8e52dced 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ pytile==2.0.6 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.7.0 +pytraccar==0.8.0 # homeassistant.components.trackr pytrackr==0.0.5 From c040f7abc07b77ade1b1f0907b117639b37e024c Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Tue, 23 Apr 2019 19:14:02 +0200 Subject: [PATCH 089/346] Upgrade attrs to 19.1.0 (#23323) --- 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 3bef086d70a..25d6c587277 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,7 +1,7 @@ aiohttp==3.5.4 astral==1.10.1 async_timeout==3.0.1 -attrs==18.2.0 +attrs==19.1.0 bcrypt==3.1.6 certifi>=2018.04.16 jinja2>=2.10 diff --git a/requirements_all.txt b/requirements_all.txt index 10e8e52dced..24371f3d0f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ aiohttp==3.5.4 astral==1.10.1 async_timeout==3.0.1 -attrs==18.2.0 +attrs==19.1.0 bcrypt==3.1.6 certifi>=2018.04.16 jinja2>=2.10 diff --git a/setup.py b/setup.py index 6f67f93d3e2..4f1e3a6eb71 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ REQUIRES = [ 'aiohttp==3.5.4', 'astral==1.10.1', 'async_timeout==3.0.1', - 'attrs==18.2.0', + 'attrs==19.1.0', 'bcrypt==3.1.6', 'certifi>=2018.04.16', 'jinja2>=2.10', From b252d8e2cd89285a0dcb82a77c05894bc7b29fbc Mon Sep 17 00:00:00 2001 From: dreed47 Date: Tue, 23 Apr 2019 14:44:13 -0400 Subject: [PATCH 090/346] Zestimate - Added check for the existence of data in response (#23310) --- homeassistant/components/zestimate/sensor.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index e66aad701b7..0a1f14324f6 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -113,12 +113,16 @@ class ZestimateDataSensor(Entity): return data = data_dict['response'][NAME] details = {} - details[ATTR_AMOUNT] = data['amount']['#text'] - details[ATTR_CURRENCY] = data['amount']['@currency'] - details[ATTR_LAST_UPDATED] = data['last-updated'] - details[ATTR_CHANGE] = int(data['valueChange']['#text']) - details[ATTR_VAL_HI] = int(data['valuationRange']['high']['#text']) - details[ATTR_VAL_LOW] = int(data['valuationRange']['low']['#text']) + if 'amount' in data and data['amount'] is not None: + details[ATTR_AMOUNT] = data['amount']['#text'] + details[ATTR_CURRENCY] = data['amount']['@currency'] + if 'last-updated' in data and data['last-updated'] is not None: + details[ATTR_LAST_UPDATED] = data['last-updated'] + if 'valueChange' in data and data['valueChange'] is not None: + details[ATTR_CHANGE] = int(data['valueChange']['#text']) + if 'valuationRange' in data and data['valuationRange'] is not None: + details[ATTR_VAL_HI] = int(data['valuationRange']['high']['#text']) + details[ATTR_VAL_LOW] = int(data['valuationRange']['low']['#text']) self.address = data_dict['response']['address']['street'] self.data = details if self.data is not None: From d505f1c5f23dbf52eccf8ebd9a35b3af63d291e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Apr 2019 13:13:00 -0700 Subject: [PATCH 091/346] Always set latest pin (#23328) --- homeassistant/components/cloud/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index aedd71bd9ac..5bbd7bb48fa 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -106,6 +106,10 @@ class CloudClient(Interface): entity_config=google_conf.get(CONF_ENTITY_CONFIG), ) + # Set it to the latest. + self._google_config.secure_devices_pin = \ + self._prefs.google_secure_devices_pin + return self._google_config @property From 68d3e624e651b08e22bb55ce5347191030959aae Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Tue, 23 Apr 2019 16:32:36 -0700 Subject: [PATCH 092/346] Fix ps4 not able to use different PSN accounts (#22799) * Remove skipping of creds step. * Check for device added per account * typo * lint * Pylint * Fix test * Fix test * Typo * Add auto location * blank space * Add new identifier handling + fix select source * Add cred_timeout error * add credential timeout error * Fix Tests * patch decorator * Update test_config_flow.py * add test * Revert * Rename vars * fix tests * Add attr location * Bump 0.6.0 * Bump 0.6.0 * Bump 0.6.0 * Update handling exception * Update remove method * Update tests * Refactoring * Pylint * revert * chmod * 0.6.1 * 0.6.1 * 0.6.1 * Remove func * Add migration * Version 3 * Remove redefinition * Add format unique id * Add format unique id * pylint * pylint * 0.7.1 * 0.7.1 * 0.7.1 * Changes with media_art call * Add library exception * 0.7.2 * 0.7.2 * 0.7.2 * Version and entry_version update * Revert list comprehension * Corrected exception handling * Update media_player.py * Update media_player.py * white space --- CODEOWNERS | 1 + homeassistant/components/ps4/__init__.py | 51 ++++++++++++-- homeassistant/components/ps4/config_flow.py | 70 ++++++++++++-------- homeassistant/components/ps4/manifest.json | 6 +- homeassistant/components/ps4/media_player.py | 39 ++++++++--- homeassistant/components/ps4/strings.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ps4/test_config_flow.py | 32 +++++++++ 9 files changed, 157 insertions(+), 47 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c2cd1f4553a..b96aae298a5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -170,6 +170,7 @@ homeassistant/components/pi_hole/* @fabaff homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike homeassistant/components/pollen/* @bachya +homeassistant/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/qnap/* @colinodell diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 22c21fcffbe..16c09d7ce2d 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -1,7 +1,9 @@ """Support for PlayStation 4 consoles.""" import logging -from homeassistant.const import CONF_REGION +from homeassistant.core import split_entity_id +from homeassistant.const import CONF_REGION, CONF_TOKEN +from homeassistant.helpers import entity_registry from homeassistant.util import location from .config_flow import PlayStation4FlowHandler # noqa: pylint: disable=unused-import @@ -37,9 +39,14 @@ async def async_migrate_entry(hass, entry): data = entry.data version = entry.version - reason = {1: "Region codes have changed"} # From 0.89 + _LOGGER.debug("Migrating PS4 entry from Version %s", version) - # Migrate Version 1 -> Version 2 + reason = { + 1: "Region codes have changed", + 2: "Format for Unique ID for entity registry has changed" + } + + # Migrate Version 1 -> Version 2: New region codes. if version == 1: loc = await hass.async_add_executor_job(location.detect_location_info) if loc: @@ -47,11 +54,41 @@ async def async_migrate_entry(hass, entry): if country in COUNTRIES: for device in data['devices']: device[CONF_REGION] = country - entry.version = 2 + version = entry.version = 2 config_entries.async_update_entry(entry, data=data) _LOGGER.info( "PlayStation 4 Config Updated: \ Region changed to: %s", country) + + # Migrate Version 2 -> Version 3: Update identifier format. + if version == 2: + # Prevent changing entity_id. Updates entity registry. + registry = await entity_registry.async_get_registry(hass) + + for entity_id, e_entry in registry.entities.items(): + if e_entry.config_entry_id == entry.entry_id: + unique_id = e_entry.unique_id + + # Remove old entity entry. + registry.async_remove(entity_id) + await hass.async_block_till_done() + + # Format old unique_id. + unique_id = format_unique_id(entry.data[CONF_TOKEN], unique_id) + + # Create new entry with old entity_id. + new_id = split_entity_id(entity_id)[1] + registry.async_get_or_create( + 'media_player', DOMAIN, unique_id, + suggested_object_id=new_id, + config_entry_id=e_entry.config_entry_id, + device_id=e_entry.device_id + ) + entry.version = 3 + _LOGGER.info( + "PlayStation 4 identifier for entity: %s \ + has changed", entity_id) + config_entries.async_update_entry(entry) return True msg = """{} for the PlayStation 4 Integration. @@ -64,3 +101,9 @@ async def async_migrate_entry(hass, entry): notification_id='config_entry_migration' ) return False + + +def format_unique_id(creds, mac_address): + """Use last 4 Chars of credential as suffix. Unique ID per PSN user.""" + suffix = creds[-4:] + return "{}_{}".format(mac_address, suffix) diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index 1b184a3774f..ff028682739 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) +from homeassistant.util import location from .const import DEFAULT_NAME, DOMAIN @@ -25,7 +26,7 @@ PORT_MSG = {UDP_PORT: 'port_987_bind_error', TCP_PORT: 'port_997_bind_error'} class PlayStation4FlowHandler(config_entries.ConfigFlow): """Handle a PlayStation 4 config flow.""" - VERSION = 2 + VERSION = 3 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): @@ -39,6 +40,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): self.region = None self.pin = None self.m_device = None + self.location = None self.device_list = [] async def async_step_user(self, user_input=None): @@ -50,23 +52,25 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): if failed in ports: reason = PORT_MSG[failed] return self.async_abort(reason=reason) - # Skip Creds Step if a device is configured. - if self.hass.config_entries.async_entries(DOMAIN): - return await self.async_step_mode() return await self.async_step_creds() async def async_step_creds(self, user_input=None): """Return PS4 credentials from 2nd Screen App.""" + from pyps4_homeassistant.errors import CredentialTimeout + errors = {} if user_input is not None: - self.creds = await self.hass.async_add_executor_job( - self.helper.get_creds) - - if self.creds is not None: - return await self.async_step_mode() - return self.async_abort(reason='credential_error') + try: + self.creds = await self.hass.async_add_executor_job( + self.helper.get_creds) + if self.creds is not None: + return await self.async_step_mode() + return self.async_abort(reason='credential_error') + except CredentialTimeout: + errors['base'] = 'credential_timeout' return self.async_show_form( - step_id='creds') + step_id='creds', + errors=errors) async def async_step_mode(self, user_input=None): """Prompt for mode.""" @@ -99,6 +103,7 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): """Prompt user input. Create or edit entry.""" from pyps4_homeassistant.media_art import COUNTRIES regions = sorted(COUNTRIES.keys()) + default_region = None errors = {} if user_input is None: @@ -112,26 +117,23 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): self.device_list = [device['host-ip'] for device in devices] - # If entry exists check that devices found aren't configured. - if self.hass.config_entries.async_entries(DOMAIN): - creds = {} - for entry in self.hass.config_entries.async_entries(DOMAIN): - # Retrieve creds from entry - creds['data'] = entry.data[CONF_TOKEN] - # Retrieve device data from entry - conf_devices = entry.data['devices'] - for c_device in conf_devices: - if c_device['host'] in self.device_list: - # Remove configured device from search list. - self.device_list.remove(c_device['host']) + # Check that devices found aren't configured per account. + entries = self.hass.config_entries.async_entries(DOMAIN) + if entries: + # Retrieve device data from all entries if creds match. + conf_devices = [device for entry in entries + if self.creds == entry.data[CONF_TOKEN] + for device in entry.data['devices']] + + # Remove configured device from search list. + for c_device in conf_devices: + if c_device['host'] in self.device_list: + # Remove configured device from search list. + self.device_list.remove(c_device['host']) + # If list is empty then all devices are configured. if not self.device_list: return self.async_abort(reason='devices_configured') - # Add existing creds for linking. Should be only 1. - if not creds: - # Abort if creds is missing. - return self.async_abort(reason='credential_error') - self.creds = creds['data'] # Login to PS4 with user data. if user_input is not None: @@ -163,11 +165,21 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): }, ) + # Try to find region automatically. + if not self.location: + self.location = await self.hass.async_add_executor_job( + location.detect_location_info) + if self.location: + country = self.location.country_name + if country in COUNTRIES: + default_region = country + # Show User Input form. link_schema = OrderedDict() link_schema[vol.Required(CONF_IP_ADDRESS)] = vol.In( list(self.device_list)) - link_schema[vol.Required(CONF_REGION)] = vol.In(list(regions)) + link_schema[vol.Required( + CONF_REGION, default=default_region)] = vol.In(list(regions)) link_schema[vol.Required(CONF_CODE)] = vol.All( vol.Strip, vol.Length(min=8, max=8), vol.Coerce(int)) link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 605dd3f530c..fcfcad95c12 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,8 +3,10 @@ "name": "Ps4", "documentation": "https://www.home-assistant.io/components/ps4", "requirements": [ - "pyps4-homeassistant==0.5.2" + "pyps4-homeassistant==0.7.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@ktnrg45" + ] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 3382cd6fe43..a53110b6f0e 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -9,6 +9,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.const import ( MEDIA_TYPE_GAME, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON) +from homeassistant.components.ps4 import format_unique_id from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_REGION, CONF_TOKEN, STATE_IDLE, STATE_OFF, STATE_PLAYING) @@ -87,7 +88,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = device[CONF_NAME] ps4 = pyps4.Ps4(host, creds) device_list.append(PS4Device( - name, host, region, ps4, games_file)) + name, host, region, ps4, creds, games_file)) add_entities(device_list, True) @@ -102,21 +103,24 @@ class PS4Data(): class PS4Device(MediaPlayerDevice): """Representation of a PS4.""" - def __init__(self, name, host, region, ps4, games_file): + def __init__(self, name, host, region, ps4, creds, games_file): """Initialize the ps4 device.""" self._ps4 = ps4 self._host = host self._name = name self._region = region + self._creds = creds self._state = None self._games_filename = games_file self._media_content_id = None self._media_title = None self._media_image = None + self._media_type = None self._source = None self._games = {} self._source_list = [] self._retry = 0 + self._disconnected = False self._info = None self._unique_id = None self._power_on = False @@ -145,6 +149,7 @@ class PS4Device(MediaPlayerDevice): status = None if status is not None: self._retry = 0 + self._disconnected = False if status.get('status') == 'Ok': # Check if only 1 device in Hass. if len(self.hass.data[PS4_DATA].devices) == 1: @@ -187,7 +192,9 @@ class PS4Device(MediaPlayerDevice): """Set states for state unknown.""" self.reset_title() self._state = None - _LOGGER.warning("PS4 could not be reached") + if self._disconnected is False: + _LOGGER.warning("PS4 could not be reached") + self._disconnected = True self._retry = 0 def reset_title(self): @@ -198,19 +205,24 @@ class PS4Device(MediaPlayerDevice): def get_title_data(self, title_id, name): """Get PS Store Data.""" + from pyps4_homeassistant.errors import PSDataIncomplete app_name = None art = None try: - app_name, art = self._ps4.get_ps_store_data( + title = self._ps4.get_ps_store_data( name, title_id, self._region) - except TypeError: + except PSDataIncomplete: _LOGGER.error( "Could not find data in region: %s for PS ID: %s", self._region, title_id) + else: + app_name = title.name + art = title.cover_art finally: self._media_title = app_name or name self._source = self._media_title self._media_image = art + self._media_type = MEDIA_TYPE_GAME self.update_list() def update_list(self): @@ -257,7 +269,7 @@ class PS4Device(MediaPlayerDevice): self.save_games(games) def get_device_info(self, status): - """Return device info for registry.""" + """Set device info for registry.""" _sw_version = status['system-version'] _sw_version = _sw_version[1:4] sw_version = "{}.{}".format(_sw_version[0], _sw_version[1:]) @@ -270,12 +282,14 @@ class PS4Device(MediaPlayerDevice): 'manufacturer': 'Sony Interactive Entertainment Inc.', 'sw_version': sw_version } - self._unique_id = status['host-id'] + + self._unique_id = format_unique_id(self._creds, status['host-id']) async def async_will_remove_from_hass(self): """Remove Entity from Hass.""" # Close TCP Socket - await self.hass.async_add_executor_job(self._ps4.close) + if self._ps4.connected: + await self.hass.async_add_executor_job(self._ps4.close) self.hass.data[PS4_DATA].devices.remove(self) @property @@ -321,7 +335,7 @@ class PS4Device(MediaPlayerDevice): @property def media_content_type(self): """Content type of current playing media.""" - return MEDIA_TYPE_GAME + return self._media_type @property def media_image_url(self): @@ -370,13 +384,18 @@ class PS4Device(MediaPlayerDevice): def select_source(self, source): """Select input source.""" for title_id, game in self._games.items(): - if source == game: + if source.lower().encode(encoding='utf-8') == \ + game.lower().encode(encoding='utf-8') \ + or source == title_id: _LOGGER.debug( "Starting PS4 game %s (%s) using source %s", game, title_id, source) self._ps4.start_title( title_id, running_id=self._media_content_id) return + _LOGGER.warning( + "Could not start title. '%s' is not in source list", source) + return def send_command(self, command): """Send Button Command.""" diff --git a/homeassistant/components/ps4/strings.json b/homeassistant/components/ps4/strings.json index ea69d8c7a8c..77443b1ee9a 100644 --- a/homeassistant/components/ps4/strings.json +++ b/homeassistant/components/ps4/strings.json @@ -26,6 +26,7 @@ } }, "error": { + "credential_timeout": "Credential service timed out. Press submit to restart.", "not_ready": "PlayStation 4 is not on or connected to network.", "login_failed": "Failed to pair to PlayStation 4. Verify PIN is correct.", "no_ipaddress": "Enter the IP Address of the PlayStation 4 you would like to configure." diff --git a/requirements_all.txt b/requirements_all.txt index 24371f3d0f6..e53402d49a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1237,7 +1237,7 @@ pypoint==1.1.1 pypollencom==2.2.3 # homeassistant.components.ps4 -pyps4-homeassistant==0.5.2 +pyps4-homeassistant==0.7.2 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0051c8edaf..4f805039041 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ pyopenuv==1.0.9 pyotp==2.2.7 # homeassistant.components.ps4 -pyps4-homeassistant==0.5.2 +pyps4-homeassistant==0.7.2 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 06fe1ef65da..5db3fc2dd81 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -7,6 +7,7 @@ from homeassistant.components.ps4.const import ( DEFAULT_NAME, DEFAULT_REGION) from homeassistant.const import ( CONF_CODE, CONF_HOST, CONF_IP_ADDRESS, CONF_NAME, CONF_REGION, CONF_TOKEN) +from homeassistant.util import location from tests.common import MockConfigEntry @@ -47,11 +48,17 @@ MOCK_TCP_PORT = int(997) MOCK_AUTO = {"Config Mode": 'Auto Discover'} MOCK_MANUAL = {"Config Mode": 'Manual Entry', CONF_IP_ADDRESS: MOCK_HOST} +MOCK_LOCATION = location.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True) + async def test_full_flow_implementation(hass): """Test registering an implementation and flow works.""" flow = ps4.PlayStation4FlowHandler() flow.hass = hass + flow.location = MOCK_LOCATION manager = hass.config_entries # User Step Started, results in Step Creds @@ -105,6 +112,7 @@ async def test_multiple_flow_implementation(hass): """Test multiple device flows.""" flow = ps4.PlayStation4FlowHandler() flow.hass = hass + flow.location = MOCK_LOCATION manager = hass.config_entries # User Step Started, results in Step Creds @@ -165,6 +173,13 @@ async def test_multiple_flow_implementation(hass): {'host-ip': MOCK_HOST_ADDITIONAL}]): result = await flow.async_step_user() assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'creds' + + # Step Creds results with form in Step Mode. + with patch('pyps4_homeassistant.Helper.get_creds', + return_value=MOCK_CREDS): + result = await flow.async_step_creds({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM assert result['step_id'] == 'mode' # Step Mode with User Input which is not manual, results in Step Link. @@ -229,6 +244,7 @@ async def test_duplicate_abort(hass): MockConfigEntry(domain=ps4.DOMAIN, data=MOCK_DATA).add_to_hass(hass) flow = ps4.PlayStation4FlowHandler() flow.hass = hass + flow.creds = MOCK_CREDS with patch('pyps4_homeassistant.Helper.has_devices', return_value=[{'host-ip': MOCK_HOST}]): @@ -284,6 +300,7 @@ async def test_manual_mode(hass): """Test host specified in manual mode is passed to Step Link.""" flow = ps4.PlayStation4FlowHandler() flow.hass = hass + flow.location = MOCK_LOCATION # Step Mode with User Input: manual, results in Step Link. with patch('pyps4_homeassistant.Helper.has_devices', @@ -305,10 +322,24 @@ async def test_credential_abort(hass): assert result['reason'] == 'credential_error' +async def test_credential_timeout(hass): + """Test that Credential Timeout shows error.""" + from pyps4_homeassistant.errors import CredentialTimeout + flow = ps4.PlayStation4FlowHandler() + flow.hass = hass + + with patch('pyps4_homeassistant.Helper.get_creds', + side_effect=CredentialTimeout): + result = await flow.async_step_creds({}) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors'] == {'base': 'credential_timeout'} + + async def test_wrong_pin_error(hass): """Test that incorrect pin throws an error.""" flow = ps4.PlayStation4FlowHandler() flow.hass = hass + flow.location = MOCK_LOCATION with patch('pyps4_homeassistant.Helper.link', return_value=(True, False)), \ @@ -324,6 +355,7 @@ async def test_device_connection_error(hass): """Test that device not connected or on throws an error.""" flow = ps4.PlayStation4FlowHandler() flow.hass = hass + flow.location = MOCK_LOCATION with patch('pyps4_homeassistant.Helper.link', return_value=(False, True)), \ From 16d8e92b06022297515e45d0b9c9859e579e1504 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Wed, 24 Apr 2019 01:47:31 +0200 Subject: [PATCH 093/346] Reorg Homematic IP Cloud imports and minor fixes (#23330) * reorg HmiP Imports after introduction of manifests * add type to some functions * fix usage of dimLevel (HomematicipDimmer,HomematicipNotificationLight) * align naming to HomematicipMultiSwitch: channel_index -> channel for (HomematicipNotificationLight) * fix lint * Fix is_on for dimmers * fix lint --- .../homematicip_cloud/alarm_control_panel.py | 9 ++-- .../homematicip_cloud/binary_sensor.py | 27 +++++------- .../components/homematicip_cloud/climate.py | 4 +- .../components/homematicip_cloud/cover.py | 4 +- .../components/homematicip_cloud/device.py | 3 +- .../components/homematicip_cloud/hap.py | 20 +++------ .../components/homematicip_cloud/light.py | 41 +++++++++++-------- .../components/homematicip_cloud/sensor.py | 27 ++++++------ .../components/homematicip_cloud/switch.py | 17 +++----- .../components/homematicip_cloud/weather.py | 7 ++-- 10 files changed, 70 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index cb35833c231..1326e46d7d3 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -1,6 +1,9 @@ """Support for HomematicIP Cloud alarm control panel.""" import logging +from homematicip.aio.group import AsyncSecurityZoneGroup +from homematicip.base.enums import WindowState + from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, @@ -18,9 +21,7 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up the HomematicIP alarm control panel from a config entry.""" - from homematicip.aio.group import AsyncSecurityZoneGroup - + """Set up the HomematicIP alrm control panel from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for group in home.groups: @@ -43,8 +44,6 @@ class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): @property def state(self): """Return the state of the device.""" - from homematicip.base.enums import WindowState - if self._device.active: if (self._device.sabotage or self._device.motionDetected or self._device.windowState == WindowState.OPEN or diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 48e9520a952..1396493a527 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -1,6 +1,14 @@ """Support for HomematicIP Cloud binary sensor.""" import logging +from homematicip.aio.device import ( + AsyncDevice, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, + AsyncMotionDetectorPushButton, AsyncRotaryHandleSensor, + AsyncShutterContact, AsyncSmokeDetector, AsyncWaterSensor, + AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) +from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup +from homematicip.base.enums import SmokeDetectorAlarmType, WindowState + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -26,15 +34,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP Cloud binary sensor from a config entry.""" - from homematicip.aio.device import ( - AsyncDevice, AsyncShutterContact, AsyncMotionDetectorIndoor, - AsyncMotionDetectorOutdoor, AsyncSmokeDetector, AsyncWaterSensor, - AsyncRotaryHandleSensor, AsyncMotionDetectorPushButton, - AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) - - from homematicip.aio.group import ( - AsyncSecurityGroup, AsyncSecurityZoneGroup) - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: @@ -79,8 +78,6 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self): """Return true if the shutter contact is on/open.""" - from homematicip.base.enums import WindowState - if self._device.sabotage: return True if self._device.windowState is None: @@ -115,7 +112,6 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self): """Return true if smoke is detected.""" - from homematicip.base.enums import SmokeDetectorAlarmType return (self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF) @@ -246,7 +242,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, attr[ATTR_MOTIONDETECTED] = True if self._device.presenceDetected: attr[ATTR_PRESENCEDETECTED] = True - from homematicip.base.enums import WindowState + if self._device.windowState is not None and \ self._device.windowState != WindowState.CLOSED: attr[ATTR_WINDOWSTATE] = str(self._device.windowState) @@ -262,7 +258,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, self._device.unreach or \ self._device.sabotage: return True - from homematicip.base.enums import WindowState + if self._device.windowState is not None and \ self._device.windowState != WindowState.CLOSED: return True @@ -288,7 +284,7 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, attr[ATTR_MOISTUREDETECTED] = True if self._device.waterlevelDetected: attr[ATTR_WATERLEVELDETECTED] = True - from homematicip.base.enums import SmokeDetectorAlarmType + if self._device.smokeDetectorAlarmType is not None and \ self._device.smokeDetectorAlarmType != \ SmokeDetectorAlarmType.IDLE_OFF: @@ -301,7 +297,6 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, def is_on(self): """Return true if safety issue detected.""" parent_is_on = super().is_on - from homematicip.base.enums import SmokeDetectorAlarmType if parent_is_on or \ self._device.powerMainsFailure or \ self._device.moistureDetected or \ diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 5055858e9c7..8a2ad8738df 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,6 +1,8 @@ """Support for HomematicIP Cloud climate devices.""" import logging +from homematicip.group import HeatingGroup + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE) @@ -26,8 +28,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP climate from a config entry.""" - from homematicip.group import HeatingGroup - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.groups: diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index e572e3d9754..381bcf1980e 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,6 +1,8 @@ """Support for HomematicIP Cloud cover devices.""" import logging +from homematicip.aio.device import AsyncFullFlushShutter + from homeassistant.components.cover import ATTR_POSITION, CoverDevice from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -19,8 +21,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP cover from a config entry.""" - from homematicip.aio.device import AsyncFullFlushShutter - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 0b815d0ec7e..f6da8b27cf7 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,6 +1,8 @@ """Generic device for the HomematicIP Cloud component.""" import logging +from homematicip.aio.device import AsyncDevice + from homeassistant.components import homematicip_cloud from homeassistant.helpers.entity import Entity @@ -29,7 +31,6 @@ class HomematicipGenericDevice(Entity): @property def device_info(self): """Return device specific attributes.""" - from homematicip.aio.device import AsyncDevice # Only physical devices should be HA devices. if isinstance(self._device, AsyncDevice): return { diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 64721c0a96c..99e98b5a1d2 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -2,6 +2,10 @@ import asyncio import logging +from homematicip.aio.auth import AsyncAuth +from homematicip.aio.home import AsyncHome +from homematicip.base.base_connection import HmipConnectionError + from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -36,8 +40,6 @@ class HomematicipAuth: async def async_checkbutton(self): """Check blue butten has been pressed.""" - from homematicip.base.base_connection import HmipConnectionError - try: return await self.auth.isRequestAcknowledged() except HmipConnectionError: @@ -45,8 +47,6 @@ class HomematicipAuth: async def async_register(self): """Register client at HomematicIP.""" - from homematicip.base.base_connection import HmipConnectionError - try: authtoken = await self.auth.requestAuthToken() await self.auth.confirmAuthToken(authtoken) @@ -56,9 +56,6 @@ class HomematicipAuth: async def get_auth(self, hass, hapid, pin): """Create a HomematicIP access point object.""" - from homematicip.aio.auth import AsyncAuth - from homematicip.base.base_connection import HmipConnectionError - auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: await auth.init(hapid) @@ -138,8 +135,6 @@ class HomematicipHAP: 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: @@ -162,8 +157,6 @@ class HomematicipHAP: async def async_connect(self): """Start WebSocket connection.""" - from homematicip.base.base_connection import HmipConnectionError - tries = 0 while True: retry_delay = 2 ** min(tries, 8) @@ -203,11 +196,8 @@ class HomematicipHAP: self.config_entry, component) return True - async def get_hap(self, hass, hapid, authtoken, name): + async def get_hap(self, hass, hapid, authtoken, name) -> AsyncHome: """Create a HomematicIP access point object.""" - from homematicip.aio.home import AsyncHome - from homematicip.base.base_connection import HmipConnectionError - home = AsyncHome(hass.loop, async_get_clientsession(hass)) home.name = name diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index b67e4114db2..e783214a447 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,6 +1,8 @@ """Support for HomematicIP Cloud lights.""" import logging +from homematicip.base.enums import RGBColorState + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) @@ -90,12 +92,15 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): @property def is_on(self): """Return true if device is on.""" - return self._device.dimLevel != 0 + return self._device.dimLevel is not None and \ + self._device.dimLevel > 0.0 @property def brightness(self): """Return the brightness of this light between 0..255.""" - return int(self._device.dimLevel*255) + if self._device.dimLevel: + return int(self._device.dimLevel*255) + return 0 @property def supported_features(self): @@ -117,15 +122,14 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home, device, channel_index): + def __init__(self, home, device, channel): """Initialize the dimmer light device.""" - self._channel_index = channel_index - if self._channel_index == 2: + self.channel = channel + if self.channel == 2: super().__init__(home, device, 'Top') else: super().__init__(home, device, 'Bottom') - from homematicip.base.enums import RGBColorState self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], RGBColorState.RED: [0.0, 100.0], @@ -137,23 +141,26 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): } @property - def _channel(self): - return self._device.functionalChannels[self._channel_index] + def _func_channel(self): + return self._device.functionalChannels[self.channel] @property def is_on(self): """Return true if device is on.""" - return self._channel.dimLevel > 0.0 + return self._func_channel.dimLevel is not None and \ + self._func_channel.dimLevel > 0.0 @property def brightness(self): """Return the brightness of this light between 0..255.""" - return int(self._channel.dimLevel * 255) + if self._func_channel.dimLevel: + return int(self._func_channel.dimLevel * 255) + return 0 @property def hs_color(self): """Return the hue and saturation color value [float, float].""" - simple_rgb_color = self._channel.simpleRGBColorState + simple_rgb_color = self._func_channel.simpleRGBColorState return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) @property @@ -161,7 +168,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Return the state attributes of the generic device.""" attr = super().device_state_attributes if self.is_on: - attr[ATTR_COLOR_NAME] = self._channel.simpleRGBColorState + attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState return attr @property @@ -201,27 +208,25 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): dim_level = brightness / 255.0 await self._device.set_rgb_dim_level( - self._channel_index, + self.channel, simple_rgb_color, dim_level) async def async_turn_off(self, **kwargs): """Turn the light off.""" - simple_rgb_color = self._channel.simpleRGBColorState + simple_rgb_color = self._func_channel.simpleRGBColorState await self._device.set_rgb_dim_level( - self._channel_index, + self.channel, simple_rgb_color, 0.0) -def _convert_color(color): +def _convert_color(color) -> RGBColorState: """ Convert the given color to the reduced RGBColorState color. RGBColorStat contains only 8 colors including white and black, so a conversion is required. """ - from homematicip.base.enums import RGBColorState - if color is None: return RGBColorState.WHITE diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 316bf1f4cd8..4816eacd08f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -1,6 +1,17 @@ """Support for HomematicIP Cloud sensors.""" import logging +from homematicip.aio.device import ( + AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring, + AsyncHeatingThermostat, AsyncHeatingThermostatCompact, AsyncLightSensor, + AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, + AsyncMotionDetectorPushButton, AsyncPlugableSwitchMeasuring, + AsyncTemperatureHumiditySensorDisplay, + AsyncTemperatureHumiditySensorOutdoor, + AsyncTemperatureHumiditySensorWithoutDisplay, AsyncWeatherSensor, + AsyncWeatherSensorPlus, AsyncWeatherSensorPro) +from homematicip.base.enums import ValveState + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, POWER_WATT, TEMP_CELSIUS) @@ -22,16 +33,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP Cloud sensors from a config entry.""" - from homematicip.aio.device import ( - AsyncHeatingThermostat, AsyncHeatingThermostatCompact, - AsyncTemperatureHumiditySensorWithoutDisplay, - AsyncTemperatureHumiditySensorDisplay, AsyncMotionDetectorIndoor, - AsyncMotionDetectorOutdoor, AsyncTemperatureHumiditySensorOutdoor, - AsyncMotionDetectorPushButton, AsyncLightSensor, - AsyncPlugableSwitchMeasuring, AsyncBrandSwitchMeasuring, - AsyncFullFlushSwitchMeasuring, AsyncWeatherSensor, - AsyncWeatherSensorPlus, AsyncWeatherSensorPro) - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] for device in home.devices: @@ -119,8 +120,6 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): @property def icon(self): """Return the icon.""" - from homematicip.base.enums import ValveState - if super().icon: return super().icon if self._device.valveState != ValveState.ADAPTION_DONE: @@ -130,8 +129,6 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): @property def state(self): """Return the state of the radiator valve.""" - from homematicip.base.enums import ValveState - if self._device.valveState != ValveState.ADAPTION_DONE: return self._device.valveState return round(self._device.valvePosition*100) @@ -299,7 +296,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): return 'mm' -def _get_wind_direction(wind_direction_degree): +def _get_wind_direction(wind_direction_degree) -> str: """Convert wind direction degree to named direction.""" if 11.25 <= wind_direction_degree < 33.75: return 'NNE' diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index b96e0c4cf4d..9a0d48ac253 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -1,6 +1,12 @@ """Support for HomematicIP Cloud switches.""" import logging +from homematicip.aio.device import ( + AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring, AsyncMultiIOBox, + AsyncOpenCollector8Module, AsyncPlugableSwitch, + AsyncPlugableSwitchMeasuring) +from homematicip.aio.group import AsyncSwitchingGroup + from homeassistant.components.switch import SwitchDevice from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -17,17 +23,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP switch from a config entry.""" - from homematicip.aio.device import ( - AsyncPlugableSwitch, - AsyncPlugableSwitchMeasuring, - AsyncBrandSwitchMeasuring, - AsyncFullFlushSwitchMeasuring, - AsyncOpenCollector8Module, - AsyncMultiIOBox, - ) - - from homematicip.aio.group import AsyncSwitchingGroup - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 74b302b18fc..9c7d843b448 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -2,6 +2,9 @@ """Support for HomematicIP Cloud weather devices.""" import logging +from homematicip.aio.device import ( + AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + from homeassistant.components.weather import WeatherEntity from homeassistant.const import TEMP_CELSIUS @@ -18,10 +21,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP weather sensor from a config entry.""" - from homematicip.aio.device import ( - AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro, - ) - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: From c61b6cf616ed200404fd491d511ba017b587f49f Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Tue, 23 Apr 2019 17:47:09 -0700 Subject: [PATCH 094/346] Support unicode in configuration migration (#23335) --- homeassistant/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 1ed2bb6db59..a7267441cdb 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -392,13 +392,13 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: config_path = find_config_file(hass.config.config_dir) assert config_path is not None - with open(config_path, 'rt') as config_file: + with open(config_path, 'rt', encoding='utf-8') as config_file: config_raw = config_file.read() if TTS_PRE_92 in config_raw: _LOGGER.info("Migrating google tts to google_translate tts") config_raw = config_raw.replace(TTS_PRE_92, TTS_92) - with open(config_path, 'wt') as config_file: + with open(config_path, 'wt', encoding='utf-8') as config_file: config_file.write(config_raw) with open(version_path, 'wt') as outp: From aa26f904204ffb0441bd8e55525a69f6abb6109e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 23 Apr 2019 19:19:23 -0700 Subject: [PATCH 095/346] Add sensor and binary senseor to default expose (#23332) --- homeassistant/components/google_assistant/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 07506611109..815b2bd1bd2 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -34,6 +34,7 @@ DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ 'climate', 'cover', 'fan', 'group', 'input_boolean', 'light', 'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock', + 'binary_sensor', 'sensor' ] PREFIX_TYPES = 'action.devices.types.' From 662375bdd7aa090feff4bd3c1982f325a3722fb6 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 24 Apr 2019 04:20:20 +0200 Subject: [PATCH 096/346] Changes due to manifest.json. Awaiting coroutines instead of creating tasks (#23321) --- homeassistant/components/lcn/__init__.py | 6 ++---- homeassistant/components/lcn/binary_sensor.py | 19 ++++++++----------- homeassistant/components/lcn/cover.py | 9 ++++----- homeassistant/components/lcn/light.py | 14 ++++++-------- homeassistant/components/lcn/sensor.py | 14 ++++++-------- homeassistant/components/lcn/switch.py | 14 ++++++-------- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 418b6ffa89d..7e7fb1430cc 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -1,6 +1,8 @@ """Support for LCN devices.""" import logging +import pypck +from pypck.connection import PchkConnectionManager import voluptuous as vol from homeassistant.const import ( @@ -149,9 +151,6 @@ def get_connection(connections, connection_id=None): async def async_setup(hass, config): """Set up the LCN component.""" - import pypck - from pypck.connection import PchkConnectionManager - hass.data[DATA_LCN] = {} conf_connections = config[DOMAIN][CONF_CONNECTIONS] @@ -201,7 +200,6 @@ class LcnDevice(Entity): def __init__(self, config, address_connection): """Initialize the LCN device.""" - import pypck self.pypck = pypck self.config = config self.address_connection = address_connection diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index ec37d3e5128..a59494023bb 100755 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -1,4 +1,6 @@ """Support for LCN binary sensors.""" +import pypck + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_ADDRESS @@ -13,8 +15,6 @@ async def async_setup_platform(hass, hass_config, async_add_entities, if discovery_info is None: return - import pypck - devices = [] for config in discovery_info: address, connection_id = config[CONF_ADDRESS] @@ -50,9 +50,8 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.setpoint_variable)) + await self.address_connection.activate_status_request_handler( + self.setpoint_variable) @property def is_on(self): @@ -84,9 +83,8 @@ class LcnBinarySensor(LcnDevice, BinarySensorDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.bin_sensor_port)) + await self.address_connection.activate_status_request_handler( + self.bin_sensor_port) @property def is_on(self): @@ -115,9 +113,8 @@ class LcnLockKeysSensor(LcnDevice, BinarySensorDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.source)) + await self.address_connection.activate_status_request_handler( + self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index 7123f2d5d0a..d07fa09c189 100755 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -1,4 +1,6 @@ """Support for LCN covers.""" +import pypck + from homeassistant.components.cover import CoverDevice from homeassistant.const import CONF_ADDRESS @@ -12,8 +14,6 @@ async def async_setup_platform(hass, hass_config, async_add_entities, if discovery_info is None: return - import pypck - devices = [] for config in discovery_info: address, connection_id = config[CONF_ADDRESS] @@ -43,9 +43,8 @@ class LcnCover(LcnDevice, CoverDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.motor)) + await self.address_connection.activate_status_request_handler( + self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index 653873ba78a..49cdff5de49 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -1,4 +1,6 @@ """Support for LCN lights.""" +import pypck + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, Light) @@ -16,8 +18,6 @@ async def async_setup_platform( if discovery_info is None: return - import pypck - devices = [] for config in discovery_info: address, connection_id = config[CONF_ADDRESS] @@ -56,9 +56,8 @@ class LcnOutputLight(LcnDevice, Light): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.output)) + await self.address_connection.activate_status_request_handler( + self.output) @property def supported_features(self): @@ -138,9 +137,8 @@ class LcnRelayLight(LcnDevice, Light): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.output)) + await self.address_connection.activate_status_request_handler( + self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 48ac8c7266c..38b17c80793 100755 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -1,4 +1,6 @@ """Support for LCN sensors.""" +import pypck + from homeassistant.const import CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT from . import LcnDevice, get_connection @@ -13,8 +15,6 @@ async def async_setup_platform(hass, hass_config, async_add_entities, if discovery_info is None: return - import pypck - devices = [] for config in discovery_info: address, connection_id = config[CONF_ADDRESS] @@ -50,9 +50,8 @@ class LcnVariableSensor(LcnDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.variable)) + await self.address_connection.activate_status_request_handler( + self.variable) @property def state(self): @@ -91,9 +90,8 @@ class LcnLedLogicSensor(LcnDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.source)) + await self.address_connection.activate_status_request_handler( + self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 48ae579fbcd..e5a8484e271 100755 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -1,4 +1,6 @@ """Support for LCN switches.""" +import pypck + from homeassistant.components.switch import SwitchDevice from homeassistant.const import CONF_ADDRESS @@ -12,8 +14,6 @@ async def async_setup_platform(hass, hass_config, async_add_entities, if discovery_info is None: return - import pypck - devices = [] for config in discovery_info: address, connection_id = config[CONF_ADDRESS] @@ -46,9 +46,8 @@ class LcnOutputSwitch(LcnDevice, SwitchDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.output)) + await self.address_connection.activate_status_request_handler( + self.output) @property def is_on(self): @@ -91,9 +90,8 @@ class LcnRelaySwitch(LcnDevice, SwitchDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - self.hass.async_create_task( - self.address_connection.activate_status_request_handler( - self.output)) + await self.address_connection.activate_status_request_handler( + self.output) @property def is_on(self): From 95bbea20a87dc321b252a2be19cee06c5dcbfcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Wed, 24 Apr 2019 04:23:52 +0200 Subject: [PATCH 097/346] Fix Switchbot restore state (#23325) * switchbot library * req * req * issue #23039 --- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/switch.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 0143855db37..21ac6ad833e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -3,7 +3,7 @@ "name": "Switchbot", "documentation": "https://www.home-assistant.io/components/switchbot", "requirements": [ - "PySwitchbot==0.5" + "PySwitchbot==0.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index b8a2a905dcb..c29dfea6737 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -46,7 +46,7 @@ class SwitchBot(SwitchDevice, RestoreEntity): state = await self.async_get_last_state() if not state: return - self._state = state.state + self._state = state.state == 'on' def turn_on(self, **kwargs) -> None: """Turn device on.""" diff --git a/requirements_all.txt b/requirements_all.txt index e53402d49a3..eb6e3cd7bee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -64,7 +64,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.1.3 # homeassistant.components.switchbot -# PySwitchbot==0.5 +# PySwitchbot==0.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 6681605c3421d1254a9cfe99b9fb975cd6c35c29 Mon Sep 17 00:00:00 2001 From: Kyle Pinette Date: Tue, 23 Apr 2019 22:24:43 -0400 Subject: [PATCH 098/346] Added override for kwikset 888. (#23327) --- homeassistant/components/zwave/lock.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index f33933a2772..e7e15d2303c 100755 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -35,6 +35,8 @@ DEVICE_MAPPINGS = { (0x0090, 0x440): WORKAROUND_DEVICE_STATE, (0x0090, 0x446): WORKAROUND_DEVICE_STATE, (0x0090, 0x238): WORKAROUND_DEVICE_STATE, + # Kwikset 888ZW500-15S Smartcode 888 + (0x0090, 0x541): WORKAROUND_DEVICE_STATE, # Yale Locks # Yale YRD210, YRD220, YRL220 (0x0129, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, From 7c55b9f08704e79d94cfeb03bff8819151d4b377 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 24 Apr 2019 04:25:20 +0200 Subject: [PATCH 099/346] Expose door cover/binary_sensor as door type (#23307) * Expose door cover/binary_sensor as door type More logical to ask "What doors are open" than "What sensors are open" * Add test for binary_sensor device_classes * Cosmetic flake8 * Add test for device class for cover --- .../components/google_assistant/const.py | 4 +- .../google_assistant/test_smart_home.py | 85 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 815b2bd1bd2..1bab27bdd12 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -50,6 +50,7 @@ TYPE_BLINDS = PREFIX_TYPES + 'BLINDS' TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' TYPE_OUTLET = PREFIX_TYPES + 'OUTLET' TYPE_SENSOR = PREFIX_TYPES + 'SENSOR' +TYPE_DOOR = PREFIX_TYPES + 'DOOR' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' @@ -94,9 +95,10 @@ DOMAIN_TO_GOOGLE_TYPES = { DEVICE_CLASS_TO_GOOGLE_TYPES = { (cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE, + (cover.DOMAIN, cover.DEVICE_CLASS_DOOR): TYPE_DOOR, (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, - (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_DOOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 8ea6f26553d..375f647da22 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -13,6 +13,8 @@ from homeassistant.components.climate.const import ( from homeassistant.components.google_assistant import ( const, trait, helpers, smart_home as sh, EVENT_COMMAND_RECEIVED, EVENT_QUERY_RECEIVED, EVENT_SYNC_RECEIVED) +from homeassistant.components.demo.binary_sensor import DemoBinarySensor +from homeassistant.components.demo.cover import DemoCover from homeassistant.components.demo.light import DemoLight from homeassistant.components.demo.switch import DemoSwitch @@ -598,6 +600,89 @@ async def test_device_class_switch(hass, device_class, google_type): } +@pytest.mark.parametrize("device_class,google_type", [ + ('door', 'action.devices.types.DOOR'), + ('garage_door', 'action.devices.types.SENSOR'), + ('lock', 'action.devices.types.SENSOR'), + ('opening', 'action.devices.types.SENSOR'), + ('window', 'action.devices.types.SENSOR'), +]) +async def test_device_class_binary_sensor(hass, device_class, google_type): + """Test that a binary entity syncs to the correct device type.""" + sensor = DemoBinarySensor( + 'Demo Sensor', + state=False, + device_class=device_class + ) + sensor.hass = hass + sensor.entity_id = 'binary_sensor.demo_sensor' + await sensor.async_update_ha_state() + + result = await sh.async_handle_message( + hass, BASIC_CONFIG, 'test-agent', + { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [{ + 'attributes': {'queryOnlyOpenClose': True}, + 'id': 'binary_sensor.demo_sensor', + 'name': {'name': 'Demo Sensor'}, + 'traits': ['action.devices.traits.OpenClose'], + 'type': google_type, + 'willReportState': False + }] + } + } + + +@pytest.mark.parametrize("device_class,google_type", [ + ('non_existing_class', 'action.devices.types.BLINDS'), + ('door', 'action.devices.types.DOOR'), +]) +async def test_device_class_cover(hass, device_class, google_type): + """Test that a binary entity syncs to the correct device type.""" + sensor = DemoCover( + hass, + 'Demo Sensor', + device_class=device_class + ) + sensor.hass = hass + sensor.entity_id = 'cover.demo_sensor' + await sensor.async_update_ha_state() + + result = await sh.async_handle_message( + hass, BASIC_CONFIG, 'test-agent', + { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [{ + 'attributes': {}, + 'id': 'cover.demo_sensor', + 'name': {'name': 'Demo Sensor'}, + 'traits': ['action.devices.traits.OpenClose'], + 'type': google_type, + 'willReportState': False + }] + } + } + + async def test_query_disconnect(hass): """Test a disconnect message.""" result = await sh.async_handle_message( From 3d04856cbd79259883779197a52c5075a0137c20 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 24 Apr 2019 11:56:43 +0200 Subject: [PATCH 100/346] Upgrade youtube_dl to 2019.04.17 (#23342) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 431e711951a..9007cb5c7be 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.04.07" + "youtube_dl==2019.04.17" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index eb6e3cd7bee..7c9d471894c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1830,7 +1830,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.04.07 +youtube_dl==2019.04.17 # homeassistant.components.zengge zengge==0.2 From 2863ac1068c9e64be87462ff2499a008be0914ad Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Wed, 24 Apr 2019 13:27:45 +0200 Subject: [PATCH 101/346] Fix Homematic IP Cloud remaining light imports (#23339) * Fix missing impor reorg * Add brackets * Removed trailing whitespaces --- homeassistant/components/homematicip_cloud/light.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index e783214a447..f4f73104f7c 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -1,6 +1,10 @@ """Support for HomematicIP Cloud lights.""" import logging +from homematicip.aio.device import ( + AsyncBrandSwitchMeasuring, AsyncDimmer, AsyncPluggableDimmer, + AsyncBrandDimmer, AsyncFullFlushDimmer, + AsyncBrandSwitchNotificationLight) from homematicip.base.enums import RGBColorState from homeassistant.components.light import ( @@ -23,10 +27,6 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the HomematicIP Cloud lights from a config entry.""" - from homematicip.aio.device import AsyncBrandSwitchMeasuring, AsyncDimmer,\ - AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer,\ - AsyncBrandSwitchNotificationLight - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: From e11e6e1b044b51b0296c45dc854cd1363969e5b5 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 24 Apr 2019 18:08:41 +0200 Subject: [PATCH 102/346] Volume trait for google assistant (#23237) * Add action.devices.traits.Volume * Drop media player from brightness trait * Factor out commands into separate functions * Drop support for explicit mute --- .../components/google_assistant/trait.py | 94 +++++++++++++++---- tests/components/google_assistant/__init__.py | 6 +- .../google_assistant/test_google_assistant.py | 4 +- .../components/google_assistant/test_trait.py | 92 ++++++++++++------ 4 files changed, 145 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index bad186a4edb..ac2f65af058 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -60,6 +60,7 @@ TRAIT_LOCKUNLOCK = PREFIX_TRAITS + 'LockUnlock' TRAIT_FANSPEED = PREFIX_TRAITS + 'FanSpeed' TRAIT_MODES = PREFIX_TRAITS + 'Modes' TRAIT_OPENCLOSE = PREFIX_TRAITS + 'OpenClose' +TRAIT_VOLUME = PREFIX_TRAITS + 'Volume' PREFIX_COMMANDS = 'action.devices.commands.' COMMAND_ONOFF = PREFIX_COMMANDS + 'OnOff' @@ -79,6 +80,8 @@ COMMAND_LOCKUNLOCK = PREFIX_COMMANDS + 'LockUnlock' COMMAND_FANSPEED = PREFIX_COMMANDS + 'SetFanSpeed' COMMAND_MODES = PREFIX_COMMANDS + 'SetModes' COMMAND_OPENCLOSE = PREFIX_COMMANDS + 'OpenClose' +COMMAND_SET_VOLUME = PREFIX_COMMANDS + 'setVolume' +COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + 'volumeRelative' TRAITS = [] @@ -141,8 +144,6 @@ class BrightnessTrait(_Trait): """Test if state is supported.""" if domain == light.DOMAIN: return features & light.SUPPORT_BRIGHTNESS - if domain == media_player.DOMAIN: - return features & media_player.SUPPORT_VOLUME_SET return False @@ -160,13 +161,6 @@ class BrightnessTrait(_Trait): if brightness is not None: response['brightness'] = int(100 * (brightness / 255)) - elif domain == media_player.DOMAIN: - level = self.state.attributes.get( - media_player.ATTR_MEDIA_VOLUME_LEVEL) - if level is not None: - # Convert 0.0-1.0 to 0-255 - response['brightness'] = int(level * 100) - return response async def execute(self, command, data, params, challenge): @@ -179,13 +173,6 @@ class BrightnessTrait(_Trait): ATTR_ENTITY_ID: self.state.entity_id, light.ATTR_BRIGHTNESS_PCT: params['brightness'] }, blocking=True, context=data.context) - elif domain == media_player.DOMAIN: - await self.hass.services.async_call( - media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: - params['brightness'] / 100 - }, blocking=True, context=data.context) @register_trait @@ -1132,6 +1119,81 @@ class OpenCloseTrait(_Trait): 'Setting a position is not supported') +@register_trait +class VolumeTrait(_Trait): + """Trait to control brightness of a device. + + https://developers.google.com/actions/smarthome/traits/volume + """ + + name = TRAIT_VOLUME + commands = [ + COMMAND_SET_VOLUME, + COMMAND_VOLUME_RELATIVE, + ] + + @staticmethod + def supported(domain, features, device_class): + """Test if state is supported.""" + if domain == media_player.DOMAIN: + return features & media_player.SUPPORT_VOLUME_SET + + return False + + def sync_attributes(self): + """Return brightness attributes for a sync request.""" + return {} + + def query_attributes(self): + """Return brightness query attributes.""" + response = {} + + level = self.state.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL) + muted = self.state.attributes.get( + media_player.ATTR_MEDIA_VOLUME_MUTED) + if level is not None: + # Convert 0.0-1.0 to 0-100 + response['currentVolume'] = int(level * 100) + response['isMuted'] = bool(muted) + + return response + + async def _execute_set_volume(self, data, params): + level = params['volumeLevel'] + + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_SET, { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: + level / 100 + }, blocking=True, context=data.context) + + async def _execute_volume_relative(self, data, params): + # This could also support up/down commands using relativeSteps + relative = params['volumeRelativeLevel'] + current = self.state.attributes.get( + media_player.ATTR_MEDIA_VOLUME_LEVEL) + + await self.hass.services.async_call( + media_player.DOMAIN, media_player.SERVICE_VOLUME_SET, { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: + current + relative / 100 + }, blocking=True, context=data.context) + + async def execute(self, command, data, params, challenge): + """Execute a brightness command.""" + if command == COMMAND_SET_VOLUME: + await self._execute_set_volume(data, params) + elif command == COMMAND_VOLUME_RELATIVE: + await self._execute_volume_relative(data, params) + else: + raise SmartHomeError( + ERR_NOT_SUPPORTED, 'Command not supported') + + def _verify_pin_challenge(data, challenge): """Verify a pin challenge.""" if not data.config.secure_devices_pin: diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index d75b51df65b..f3732c12213 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -143,7 +143,7 @@ DEMO_DEVICES = [{ }, 'traits': [ - 'action.devices.traits.OnOff', 'action.devices.traits.Brightness', + 'action.devices.traits.OnOff', 'action.devices.traits.Volume', 'action.devices.traits.Modes' ], 'type': @@ -158,7 +158,7 @@ DEMO_DEVICES = [{ }, 'traits': [ - 'action.devices.traits.OnOff', 'action.devices.traits.Brightness', + 'action.devices.traits.OnOff', 'action.devices.traits.Volume', 'action.devices.traits.Modes' ], 'type': @@ -180,7 +180,7 @@ DEMO_DEVICES = [{ 'name': 'Walkman' }, 'traits': - ['action.devices.traits.OnOff', 'action.devices.traits.Brightness'], + ['action.devices.traits.OnOff', 'action.devices.traits.Volume'], 'type': 'action.devices.types.SWITCH', 'willReportState': diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 19e1858d4f5..4e2c04e5cf4 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -319,9 +319,9 @@ def test_execute_request(hass_fixture, assistant_client, auth_header): }], "execution": [{ "command": - "action.devices.commands.BrightnessAbsolute", + "action.devices.commands.setVolume", "params": { - "brightness": 70 + "volumeLevel": 70 } }] }, { diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 8b7f0788f34..96ca8d82f5e 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -92,36 +92,6 @@ async def test_brightness_light(hass): } -async def test_brightness_media_player(hass): - """Test brightness trait support for media player domain.""" - assert helpers.get_google_type(media_player.DOMAIN, None) is not None - assert trait.BrightnessTrait.supported(media_player.DOMAIN, - media_player.SUPPORT_VOLUME_SET, - None) - - trt = trait.BrightnessTrait(hass, State( - 'media_player.bla', media_player.STATE_PLAYING, { - media_player.ATTR_MEDIA_VOLUME_LEVEL: .3 - }), BASIC_CONFIG) - - assert trt.sync_attributes() == {} - - assert trt.query_attributes() == { - 'brightness': 30 - } - - calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET) - await trt.execute( - trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA, - {'brightness': 60}, {}) - assert len(calls) == 1 - assert calls[0].data == { - ATTR_ENTITY_ID: 'media_player.bla', - media_player.ATTR_MEDIA_VOLUME_LEVEL: .6 - } - - async def test_camera_stream(hass): """Test camera stream trait support for camera domain.""" hass.config.api = Mock(base_url='http://1.1.1.1:8123') @@ -1276,3 +1246,65 @@ async def test_openclose_binary_sensor(hass, device_class): assert trt.query_attributes() == { 'openPercent': 0 } + + +async def test_volume_media_player(hass): + """Test volume trait support for media player domain.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None + assert trait.VolumeTrait.supported(media_player.DOMAIN, + media_player.SUPPORT_VOLUME_SET | + media_player.SUPPORT_VOLUME_MUTE, + None) + + trt = trait.VolumeTrait(hass, State( + 'media_player.bla', media_player.STATE_PLAYING, { + media_player.ATTR_MEDIA_VOLUME_LEVEL: .3, + media_player.ATTR_MEDIA_VOLUME_MUTED: False, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == { + 'currentVolume': 30, + 'isMuted': False + } + + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET) + await trt.execute( + trait.COMMAND_SET_VOLUME, BASIC_DATA, + {'volumeLevel': 60}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'media_player.bla', + media_player.ATTR_MEDIA_VOLUME_LEVEL: .6 + } + + +async def test_volume_media_player_relative(hass): + """Test volume trait support for media player domain.""" + trt = trait.VolumeTrait(hass, State( + 'media_player.bla', media_player.STATE_PLAYING, { + media_player.ATTR_MEDIA_VOLUME_LEVEL: .3, + media_player.ATTR_MEDIA_VOLUME_MUTED: False, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == { + 'currentVolume': 30, + 'isMuted': False + } + + calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET) + + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, + {'volumeRelativeLevel': 20, + 'relativeSteps': 2}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'media_player.bla', + media_player.ATTR_MEDIA_VOLUME_LEVEL: .5 + } From 82ff5cbe0f1067e229cb2dbcf7e0a12fe91ddb90 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 24 Apr 2019 18:52:29 +0200 Subject: [PATCH 103/346] Upgrade ruamel.yaml to 0.15.94 (#23344) --- 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 25d6c587277..a4a08af1236 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ python-slugify==3.0.2 pytz>=2019.01 pyyaml>=3.13,<4 requests==2.21.0 -ruamel.yaml==0.15.91 +ruamel.yaml==0.15.94 voluptuous==0.11.5 voluptuous-serialize==2.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7c9d471894c..a90e15a6616 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ python-slugify==3.0.2 pytz>=2019.01 pyyaml>=3.13,<4 requests==2.21.0 -ruamel.yaml==0.15.91 +ruamel.yaml==0.15.94 voluptuous==0.11.5 voluptuous-serialize==2.1.0 diff --git a/setup.py b/setup.py index 4f1e3a6eb71..4de6fa2f042 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ REQUIRES = [ 'pytz>=2019.01', 'pyyaml>=3.13,<4', 'requests==2.21.0', - 'ruamel.yaml==0.15.91', + 'ruamel.yaml==0.15.94', 'voluptuous==0.11.5', 'voluptuous-serialize==2.1.0', ] From f4e736465124fd3b513a887ba791632e0152f8d4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 24 Apr 2019 18:54:51 +0200 Subject: [PATCH 104/346] Netatmo 5min fetch interval (#23341) --- homeassistant/components/netatmo/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index cf64363ba50..f56ffbfffd2 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -58,8 +58,8 @@ ATTR_FACE_URL = 'face_url' ATTR_SNAPSHOT_URL = 'snapshot_url' ATTR_VIGNETTE_URL = 'vignette_url' -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) -MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=10) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) +MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ From e850ccb82cd6f4250343f2b12d922235915f9715 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Thu, 25 Apr 2019 00:55:37 +0800 Subject: [PATCH 105/346] Fixed test (#23343) --- tests/components/mobile_app/test_entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py index 3d8e575f686..e98307468d1 100644 --- a/tests/components/mobile_app/test_entity.py +++ b/tests/components/mobile_app/test_entity.py @@ -36,6 +36,7 @@ async def test_sensor(hass, create_registrations, webhook_client): # noqa: F401 json = await reg_resp.json() assert json == {'success': True} + await hass.async_block_till_done() entity = hass.states.get('sensor.battery_state') assert entity is not None From 843bad83fa4de5b02be22fc9eab0246546305dd1 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Wed, 24 Apr 2019 09:55:48 -0700 Subject: [PATCH 106/346] Remove ghost folder (#23350) --- homeassistant/components/aws_lambda/manifest.json | 10 ---------- homeassistant/components/aws_sns/manifest.json | 10 ---------- homeassistant/components/aws_sqs/manifest.json | 10 ---------- requirements_all.txt | 3 --- 4 files changed, 33 deletions(-) delete mode 100644 homeassistant/components/aws_lambda/manifest.json delete mode 100644 homeassistant/components/aws_sns/manifest.json delete mode 100644 homeassistant/components/aws_sqs/manifest.json diff --git a/homeassistant/components/aws_lambda/manifest.json b/homeassistant/components/aws_lambda/manifest.json deleted file mode 100644 index 40c8c7b0629..00000000000 --- a/homeassistant/components/aws_lambda/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "aws_lambda", - "name": "Aws lambda", - "documentation": "https://www.home-assistant.io/components/aws_lambda", - "requirements": [ - "boto3==1.9.16" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/aws_sns/manifest.json b/homeassistant/components/aws_sns/manifest.json deleted file mode 100644 index f6c3438025d..00000000000 --- a/homeassistant/components/aws_sns/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "aws_sns", - "name": "Aws sns", - "documentation": "https://www.home-assistant.io/components/aws_sns", - "requirements": [ - "boto3==1.9.16" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/aws_sqs/manifest.json b/homeassistant/components/aws_sqs/manifest.json deleted file mode 100644 index fcfc8cfb297..00000000000 --- a/homeassistant/components/aws_sqs/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "aws_sqs", - "name": "Aws sqs", - "documentation": "https://www.home-assistant.io/components/aws_sqs", - "requirements": [ - "boto3==1.9.16" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/requirements_all.txt b/requirements_all.txt index a90e15a6616..70c547e26e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -245,9 +245,6 @@ blockchain==1.4.4 bomradarloop==0.1.2 # homeassistant.components.amazon_polly -# homeassistant.components.aws_lambda -# homeassistant.components.aws_sns -# homeassistant.components.aws_sqs # homeassistant.components.route53 boto3==1.9.16 From 62fcb1895ebf777fa6b540880c321ef8e01b77dc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 24 Apr 2019 18:56:22 +0200 Subject: [PATCH 107/346] Device type garage for binary sensor garage_door (#23345) * Switch binary sensor to garage for garage_door * Add test for cover garage device type --- homeassistant/components/google_assistant/const.py | 2 +- tests/components/google_assistant/test_smart_home.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 1bab27bdd12..b6f57546cec 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -100,7 +100,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_DOOR): TYPE_DOOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_GARAGE_DOOR): - TYPE_SENSOR, + TYPE_GARAGE, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 375f647da22..ce750b74e23 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -602,7 +602,7 @@ async def test_device_class_switch(hass, device_class, google_type): @pytest.mark.parametrize("device_class,google_type", [ ('door', 'action.devices.types.DOOR'), - ('garage_door', 'action.devices.types.SENSOR'), + ('garage_door', 'action.devices.types.GARAGE'), ('lock', 'action.devices.types.SENSOR'), ('opening', 'action.devices.types.SENSOR'), ('window', 'action.devices.types.SENSOR'), @@ -646,6 +646,7 @@ async def test_device_class_binary_sensor(hass, device_class, google_type): @pytest.mark.parametrize("device_class,google_type", [ ('non_existing_class', 'action.devices.types.BLINDS'), ('door', 'action.devices.types.DOOR'), + ('garage', 'action.devices.types.GARAGE'), ]) async def test_device_class_cover(hass, device_class, google_type): """Test that a binary entity syncs to the correct device type.""" From d53a00d054116906221bc6a37a11336814340dec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Apr 2019 11:15:56 -0700 Subject: [PATCH 108/346] Updated frontend to 20190424.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ae91178e4c4..608687610e4 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190419.0" + "home-assistant-frontend==20190424.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 70c547e26e0..188c7075d10 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190419.0 +home-assistant-frontend==20190424.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f805039041..c09da4b3bb8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -139,7 +139,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190419.0 +home-assistant-frontend==20190424.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From d218ba98e7d8fcda54e5777aa16e893464e011ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Apr 2019 13:37:08 -0700 Subject: [PATCH 109/346] Fix config test when current version is 92 (#23356) --- tests/test_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 3cbcec0214e..e9ca2a6c806 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -318,7 +318,8 @@ class TestConfig(unittest.TestCase): ha_version = '0.92.0' mock_open = mock.mock_open() - with mock.patch('homeassistant.config.open', mock_open, create=True): + with mock.patch('homeassistant.config.open', mock_open, create=True), \ + mock.patch.object(config_util, '__version__', '0.91.0'): opened_file = mock_open.return_value # pylint: disable=no-member opened_file.readline.return_value = ha_version @@ -326,7 +327,7 @@ class TestConfig(unittest.TestCase): config_util.process_ha_config_upgrade(self.hass) assert opened_file.write.call_count == 1 - assert opened_file.write.call_args == mock.call(__version__) + assert opened_file.write.call_args == mock.call('0.91.0') def test_config_upgrade_same_version(self): """Test no update of version on no upgrade.""" From ef5ca63bf0d65e9429f28bd976c76bb48aab6087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Wed, 24 Apr 2019 23:39:31 +0200 Subject: [PATCH 110/346] Fix non-syncthru supporting printers (#21482) * Fix non syncthru-syncthru supporting printers * Formatting * Update requirements_all * Update syncthru.py * Fix component to be async (as is the used SyncThru implementation) * Add async syntax * Omit loop passing * Don't await async_add_platform * Generate new all requirements * Explain, why exception is caught in setuExplain, why exception is caught in setupp * Handle failing initial setup correctly * Formatting * Formatting * Fix requested changes * Update requirements and add nielstron as codeowner * Run codeowners script * Make notification about missing syncthru support a warning * Revert pure formatting * Fix logging --- CODEOWNERS | 1 + .../components/syncthru/manifest.json | 4 +- homeassistant/components/syncthru/sensor.py | 122 +++++++++++------- requirements_all.txt | 2 +- 4 files changed, 79 insertions(+), 50 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b96aae298a5..20fd91b75d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,6 +209,7 @@ homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen homeassistant/components/switcher_kis/* @tomerfi homeassistant/components/switchmate/* @danielhiversen +homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff homeassistant/components/sytadin/* @gautric diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 1aadeb54909..8fc3b2476cb 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,8 +3,8 @@ "name": "Syncthru", "documentation": "https://www.home-assistant.io/components/syncthru", "requirements": [ - "pysyncthru==0.3.1" + "pysyncthru==0.4.2" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@nielstron"] } diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 33f57fa0371..fe95d7c7e20 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_RESOURCE, CONF_HOST, CONF_NAME, CONF_MONITORED_CONDITIONS) +from homeassistant.helpers import aiohttp_client from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -12,40 +13,33 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA _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' ] +DRUM_COLORS = COLORS +TONER_COLORS = COLORS +TRAYS = range(1, 6) +OUTPUT_TRAYS = range(0, 6) +DEFAULT_MONITORED_CONDITIONS = [] +DEFAULT_MONITORED_CONDITIONS.extend( + ['toner_{}'.format(key) for key in TONER_COLORS] +) +DEFAULT_MONITORED_CONDITIONS.extend( + ['drum_{}'.format(key) for key in DRUM_COLORS] +) +DEFAULT_MONITORED_CONDITIONS.extend( + ['trays_{}'.format(key) for key in TRAYS] +) +DEFAULT_MONITORED_CONDITIONS.extend( + ['output_trays_{}'.format(key) for key in OUTPUT_TRAYS] +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, - vol.Optional( - CONF_NAME, - default=DEFAULT_NAME - ): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional( CONF_MONITORED_CONDITIONS, default=DEFAULT_MONITORED_CONDITIONS @@ -53,48 +47,70 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, + config, + async_add_entities, + discovery_info=None): """Set up the SyncThru component.""" - from pysyncthru import SyncThru, test_syncthru + from pysyncthru import SyncThru if discovery_info is not None: + _LOGGER.info("Discovered a new Samsung Printer at %s", + discovery_info.get(CONF_HOST)) 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 + # Main device, always added 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 + session = aiohttp_client.async_get_clientsession(hass) + + printer = SyncThru(host, session) + # Test if the discovered device actually is a syncthru printer + # and fetch the available toner/drum/etc + try: + # No error is thrown when the device is off + # (only after user added it manually) + # therefore additional catches are inside the Sensor below + await printer.update() + supp_toner = printer.toner_status(filter_supported=True) + supp_drum = printer.drum_status(filter_supported=True) + supp_tray = printer.input_tray_status(filter_supported=True) + supp_output_tray = printer.output_tray_status() + except ValueError: + # if an exception is thrown, printer does not support syncthru + # and should not be set up + # If the printer was discovered automatically, no warning or error + # should be issued and printer should not be set up + if discovery_info is not None: + _LOGGER.info("Samsung printer at %s does not support SyncThru", + host) + return + # Otherwise, emulate printer that supports everything + supp_toner = TONER_COLORS + supp_drum = DRUM_COLORS + supp_tray = TRAYS + supp_output_tray = OUTPUT_TRAYS - printer.update() devices = [SyncThruMainSensor(printer, name)] - for key in printer.toner_status(filter_supported=True): + for key in supp_toner: if 'toner_{}'.format(key) in monitored: devices.append(SyncThruTonerSensor(printer, name, key)) - for key in printer.drum_status(filter_supported=True): + for key in supp_drum: if 'drum_{}'.format(key) in monitored: devices.append(SyncThruDrumSensor(printer, name, key)) - for key in printer.input_tray_status(filter_supported=True): + for key in supp_tray: if 'tray_{}'.format(key) in monitored: devices.append(SyncThruInputTraySensor(printer, name, key)) - for key in printer.output_tray_status(): + for key in supp_output_tray: if 'output_tray_{}'.format(key) in monitored: devices.append(SyncThruOutputTraySensor(printer, name, key)) - add_entities(devices, True) + async_add_entities(devices, True) class SyncThruSensor(Entity): @@ -143,16 +159,28 @@ class SyncThruSensor(Entity): class SyncThruMainSensor(SyncThruSensor): - """Implementation of the main sensor, monitoring the general state.""" + """Implementation of the main sensor, conducting the actual polling.""" def __init__(self, syncthru, name): """Initialize the sensor.""" super().__init__(syncthru, name) self._id_suffix = '_main' + self._active = True - def update(self): + async def async_update(self): """Get the latest data from SyncThru and update the state.""" - self.syncthru.update() + if not self._active: + return + try: + await self.syncthru.update() + except ValueError: + # if an exception is thrown, printer does not support syncthru + _LOGGER.warning( + "Configured printer at %s does not support SyncThru. " + "Consider changing your configuration", + self.syncthru.url + ) + self._active = False self._state = self.syncthru.device_status() diff --git a/requirements_all.txt b/requirements_all.txt index 188c7075d10..e768fa1f071 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1300,7 +1300,7 @@ pystride==0.1.7 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.3.1 +pysyncthru==0.4.2 # homeassistant.components.tautulli pytautulli==0.5.0 From fef1dc8c5441da090c754a580ad36d3b5a313a9a Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Wed, 24 Apr 2019 14:47:22 -0700 Subject: [PATCH 111/346] Bump ecovacs lib 2 (#23354) * Bump Ecovacs dependency (sucks) Update to new version of sucks, which switches to a custom-built SleekXMPP that turns off certificate validation. This is to fix issues caused by Ecovacs serving invalid certificates. * Update requirements file --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index d36768fb1b0..4495cb3c2f9 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -3,7 +3,7 @@ "name": "Ecovacs", "documentation": "https://www.home-assistant.io/components/ecovacs", "requirements": [ - "sucks==0.9.3" + "sucks==0.9.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e768fa1f071..2007491a903 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1656,7 +1656,7 @@ steamodd==4.21 stringcase==1.2.0 # homeassistant.components.ecovacs -sucks==0.9.3 +sucks==0.9.4 # homeassistant.components.onvif suds-passworddigest-homeassistant==0.1.2a0.dev0 From 96735e41af21e923eebafddb949af9042a79f73f Mon Sep 17 00:00:00 2001 From: Beat <508289+bdurrer@users.noreply.github.com> Date: Thu, 25 Apr 2019 00:30:46 +0200 Subject: [PATCH 112/346] Add support for a wider variety of EnOcean devices (#22052) * Implement EnOcean temperature and humidity sensors. * Bump EnOcean version to 0.50 * Refactor components for more generic device handling * Move radio packet data interpretation to specific devices * Update CODEOWNERS * Implement code review changes --- CODEOWNERS | 1 + homeassistant/components/enocean/__init__.py | 107 ++++------ .../components/enocean/binary_sensor.py | 70 ++++--- homeassistant/components/enocean/light.py | 35 ++-- .../components/enocean/manifest.json | 4 +- homeassistant/components/enocean/sensor.py | 189 +++++++++++++++--- homeassistant/components/enocean/switch.py | 46 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + 10 files changed, 305 insertions(+), 153 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 20fd91b75d3..c3126205810 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -66,6 +66,7 @@ homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/emby/* @mezz64 homeassistant/components/enigma2/* @fbradyirl +homeassistant/components/enocean/* @bdurrer homeassistant/components/ephember/* @ttroy50 homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index 2dcf6a3a0ac..9d51821082a 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -4,13 +4,13 @@ import logging import voluptuous as vol from homeassistant.const import CONF_DEVICE +from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DOMAIN = 'enocean' - -ENOCEAN_DONGLE = None +DATA_ENOCEAN = 'enocean' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -18,14 +18,15 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) +SIGNAL_RECEIVE_MESSAGE = 'enocean.receive_message' +SIGNAL_SEND_MESSAGE = 'enocean.send_message' + def setup(hass, config): """Set up the EnOcean component.""" - global ENOCEAN_DONGLE - serial_dev = config[DOMAIN].get(CONF_DEVICE) - - ENOCEAN_DONGLE = EnOceanDongle(hass, serial_dev) + dongle = EnOceanDongle(hass, serial_dev) + hass.data[DATA_ENOCEAN] = dongle return True @@ -39,87 +40,53 @@ class EnOceanDongle: self.__communicator = SerialCommunicator( port=ser, callback=self.callback) self.__communicator.start() - self.__devices = [] + self.hass = hass + self.hass.helpers.dispatcher.dispatcher_connect( + SIGNAL_SEND_MESSAGE, self._send_message_callback) - def register_device(self, dev): - """Register another device.""" - self.__devices.append(dev) - - def send_command(self, command): - """Send a command from the EnOcean dongle.""" + def _send_message_callback(self, command): + """Send a command through the EnOcean dongle.""" self.__communicator.send(command) - # pylint: disable=no-self-use - def _combine_hex(self, data): - """Combine list of integer values to one big integer.""" - output = 0x00 - for i, j in enumerate(reversed(data)): - output |= (j << i * 8) - return output - - def callback(self, temp): + def callback(self, packet): """Handle EnOcean device's callback. This is the callback function called by python-enocan whenever there is an incoming packet. """ from enocean.protocol.packet import RadioPacket - if isinstance(temp, RadioPacket): - _LOGGER.debug("Received radio packet: %s", temp) - rxtype = None - value = None - channel = 0 - if temp.data[6] == 0x30: - rxtype = "wallswitch" - value = 1 - elif temp.data[6] == 0x20: - rxtype = "wallswitch" - value = 0 - elif temp.data[4] == 0x0c: - rxtype = "power" - value = temp.data[3] + (temp.data[2] << 8) - elif temp.data[2] & 0x60 == 0x60: - rxtype = "switch_status" - channel = temp.data[2] & 0x1F - if temp.data[3] == 0xe4: - value = 1 - elif temp.data[3] == 0x80: - value = 0 - elif temp.data[0] == 0xa5 and temp.data[1] == 0x02: - rxtype = "dimmerstatus" - value = temp.data[2] - for device in self.__devices: - if rxtype == "wallswitch" and device.stype == "listener": - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value, temp.data[1]) - if rxtype == "power" and device.stype == "powersensor": - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value) - if rxtype == "power" and device.stype == "switch": - if temp.sender_int == self._combine_hex(device.dev_id): - if value > 10: - device.value_changed(1) - if rxtype == "switch_status" and device.stype == "switch" and \ - channel == device.channel: - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value) - if rxtype == "dimmerstatus" and device.stype == "dimmer": - if temp.sender_int == self._combine_hex(device.dev_id): - device.value_changed(value) + if isinstance(packet, RadioPacket): + _LOGGER.debug("Received radio packet: %s", packet) + self.hass.helpers.dispatcher.dispatcher_send( + SIGNAL_RECEIVE_MESSAGE, packet) -class EnOceanDevice(): +class EnOceanDevice(Entity): """Parent class for all devices associated with the EnOcean component.""" - def __init__(self): + def __init__(self, dev_id, dev_name="EnOcean device"): """Initialize the device.""" - ENOCEAN_DONGLE.register_device(self) - self.stype = "" - self.sensorid = [0x00, 0x00, 0x00, 0x00] + self.dev_id = dev_id + self.dev_name = dev_name + + async def async_added_to_hass(self): + """Register callbacks.""" + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RECEIVE_MESSAGE, self._message_received_callback) + + def _message_received_callback(self, packet): + """Handle incoming packets.""" + from enocean.utils import combine_hex + if packet.sender_int == combine_hex(self.dev_id): + self.value_changed(packet) + + def value_changed(self, packet): + """Update the internal state of the device when a packet arrives.""" # pylint: disable=no-self-use def send_command(self, data, optional, packet_type): """Send a command via the EnOcean dongle.""" from enocean.protocol.packet import Packet packet = Packet(packet_type, data=data, optional=optional) - ENOCEAN_DONGLE.send_command(packet) + self.hass.helpers.dispatcher.dispatcher_send( + SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 649bec024e3..5e0a3b31817 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -3,16 +3,17 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import ( - BinarySensorDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA) from homeassistant.components import enocean -from homeassistant.const import ( - CONF_NAME, CONF_ID, CONF_DEVICE_CLASS) +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'EnOcean binary sensor' +DEPENDENCIES = ['enocean'] +EVENT_BUTTON_PRESSED = 'button_pressed' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), @@ -24,61 +25,80 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Binary Sensor platform for EnOcean.""" dev_id = config.get(CONF_ID) - devname = config.get(CONF_NAME) + dev_name = config.get(CONF_NAME) device_class = config.get(CONF_DEVICE_CLASS) - add_entities([EnOceanBinarySensor(dev_id, devname, device_class)]) + add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)]) class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorDevice): - """Representation of EnOcean binary sensors such as wall switches.""" + """Representation of EnOcean binary sensors such as wall switches. - def __init__(self, dev_id, devname, device_class): + Supported EEPs (EnOcean Equipment Profiles): + - F6-02-01 (Light and Blind Control - Application Style 2) + - F6-02-02 (Light and Blind Control - Application Style 1) + """ + + def __init__(self, dev_id, dev_name, device_class): """Initialize the EnOcean binary sensor.""" - enocean.EnOceanDevice.__init__(self) - self.stype = 'listener' - self.dev_id = dev_id + super().__init__(dev_id, dev_name) + self._device_class = device_class self.which = -1 self.onoff = -1 - self.devname = devname - self._device_class = device_class @property def name(self): """Return the default name for the binary sensor.""" - return self.devname + return self.dev_name @property def device_class(self): """Return the class of this sensor.""" return self._device_class - def value_changed(self, value, value2): + def value_changed(self, packet): """Fire an event with the data that have changed. This method is called when there is an incoming packet associated with this platform. + + Example packet data: + - 2nd button pressed + ['0xf6', '0x10', '0x00', '0x2d', '0xcf', '0x45', '0x30'] + - button released + ['0xf6', '0x00', '0x00', '0x2d', '0xcf', '0x45', '0x20'] """ + # Energy Bow + pushed = None + + if packet.data[6] == 0x30: + pushed = 1 + elif packet.data[6] == 0x20: + pushed = 0 + self.schedule_update_ha_state() - if value2 == 0x70: + + action = packet.data[1] + if action == 0x70: self.which = 0 self.onoff = 0 - elif value2 == 0x50: + elif action == 0x50: self.which = 0 self.onoff = 1 - elif value2 == 0x30: + elif action == 0x30: self.which = 1 self.onoff = 0 - elif value2 == 0x10: + elif action == 0x10: self.which = 1 self.onoff = 1 - elif value2 == 0x37: + elif action == 0x37: self.which = 10 self.onoff = 0 - elif value2 == 0x15: + elif action == 0x15: self.which = 10 self.onoff = 1 - self.hass.bus.fire('button_pressed', {'id': self.dev_id, - 'pushed': value, - 'which': self.which, - 'onoff': self.onoff}) + self.hass.bus.fire(EVENT_BUTTON_PRESSED, + {'id': self.dev_id, + 'pushed': pushed, + 'which': self.which, + 'onoff': self.onoff}) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 9ec3f4ab27b..d40b2c01df6 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -4,10 +4,10 @@ import math import voluptuous as vol -from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, PLATFORM_SCHEMA) -from homeassistant.const import (CONF_NAME, CONF_ID) from homeassistant.components import enocean +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light) +from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,29 +28,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the EnOcean light platform.""" sender_id = config.get(CONF_SENDER_ID) - devname = config.get(CONF_NAME) + dev_name = config.get(CONF_NAME) dev_id = config.get(CONF_ID) - add_entities([EnOceanLight(sender_id, devname, dev_id)]) + add_entities([EnOceanLight(sender_id, dev_id, dev_name)]) class EnOceanLight(enocean.EnOceanDevice, Light): """Representation of an EnOcean light source.""" - def __init__(self, sender_id, devname, dev_id): + def __init__(self, sender_id, dev_id, dev_name): """Initialize the EnOcean light source.""" - enocean.EnOceanDevice.__init__(self) + super().__init__(dev_id, dev_name) self._on_state = False self._brightness = 50 self._sender_id = sender_id - self.dev_id = dev_id - self._devname = devname - self.stype = 'dimmer' @property def name(self): """Return the name of the device if any.""" - return self._devname + return self.dev_name @property def brightness(self): @@ -94,8 +91,14 @@ class EnOceanLight(enocean.EnOceanDevice, Light): self.send_command(command, [], 0x01) self._on_state = False - def value_changed(self, val): - """Update the internal state of this device.""" - self._brightness = math.floor(val / 100.0 * 256.0) - self._on_state = bool(val != 0) - self.schedule_update_ha_state() + def value_changed(self, packet): + """Update the internal state of this device. + + Dimmer devices like Eltako FUD61 send telegram in different RORGs. + We only care about the 4BS (0xA5). + """ + if packet.data[0] == 0xa5 and packet.data[1] == 0x02: + val = packet.data[2] + self._brightness = math.floor(val / 100.0 * 256.0) + self._on_state = bool(val != 0) + self.schedule_update_ha_state() diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index 7c4d7c0b8d9..e6f1c5d7826 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -3,8 +3,8 @@ "name": "Enocean", "documentation": "https://www.home-assistant.io/components/enocean", "requirements": [ - "enocean==0.40" + "enocean==0.50" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@bdurrer"] } diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 530738e1f88..62d0277946f 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -3,58 +3,201 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_ID, POWER_WATT) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components import enocean +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, POWER_WATT) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +CONF_MAX_TEMP = 'max_temp' +CONF_MIN_TEMP = 'min_temp' +CONF_RANGE_FROM = 'range_from' +CONF_RANGE_TO = 'range_to' + DEFAULT_NAME = 'EnOcean sensor' + +DEVICE_CLASS_POWER = 'powersensor' + +SENSOR_TYPES = { + DEVICE_CLASS_HUMIDITY: { + 'name': 'Humidity', + 'unit': '%', + 'icon': 'mdi:water-percent', + 'class': DEVICE_CLASS_HUMIDITY, + }, + DEVICE_CLASS_POWER: { + 'name': 'Power', + 'unit': POWER_WATT, + 'icon': 'mdi:power-plug', + 'class': DEVICE_CLASS_POWER, + }, + DEVICE_CLASS_TEMPERATURE: { + 'name': 'Temperature', + 'unit': TEMP_CELSIUS, + 'icon': 'mdi:thermometer', + 'class': DEVICE_CLASS_TEMPERATURE, + }, +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string, + vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int), + vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int), + vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int, + vol.Optional(CONF_RANGE_TO, default=0): cv.positive_int, }) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an EnOcean sensor device.""" dev_id = config.get(CONF_ID) - devname = config.get(CONF_NAME) + dev_name = config.get(CONF_NAME) + dev_class = config.get(CONF_DEVICE_CLASS) - add_entities([EnOceanSensor(dev_id, devname)]) + if dev_class == DEVICE_CLASS_TEMPERATURE: + temp_min = config.get(CONF_MIN_TEMP) + temp_max = config.get(CONF_MAX_TEMP) + range_from = config.get(CONF_RANGE_FROM) + range_to = config.get(CONF_RANGE_TO) + add_entities([EnOceanTemperatureSensor( + dev_id, dev_name, temp_min, temp_max, range_from, range_to)]) + + elif dev_class == DEVICE_CLASS_HUMIDITY: + add_entities([EnOceanHumiditySensor(dev_id, dev_name)]) + + elif dev_class == DEVICE_CLASS_POWER: + add_entities([EnOceanPowerSensor(dev_id, dev_name)]) -class EnOceanSensor(enocean.EnOceanDevice, Entity): - """Representation of an EnOcean sensor device such as a power meter.""" +class EnOceanSensor(enocean.EnOceanDevice): + """Representation of an EnOcean sensor device such as a power meter.""" - def __init__(self, dev_id, devname): + def __init__(self, dev_id, dev_name, sensor_type): """Initialize the EnOcean sensor device.""" - enocean.EnOceanDevice.__init__(self) - self.stype = "powersensor" - self.power = None - self.dev_id = dev_id - self.which = -1 - self.onoff = -1 - self.devname = devname + super().__init__(dev_id, dev_name) + self._sensor_type = sensor_type + self._device_class = SENSOR_TYPES[self._sensor_type]['class'] + self._dev_name = '{} {}'.format( + SENSOR_TYPES[self._sensor_type]['name'], dev_name) + self._unit_of_measurement = SENSOR_TYPES[self._sensor_type]['unit'] + self._icon = SENSOR_TYPES[self._sensor_type]['icon'] + self._state = None @property def name(self): """Return the name of the device.""" - return 'Power %s' % self.devname + return self._dev_name - def value_changed(self, value): - """Update the internal state of the device.""" - self.power = value - self.schedule_update_ha_state() + @property + def icon(self): + """Icon to use in the frontend.""" + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class @property def state(self): """Return the state of the device.""" - return self.power + return self._state @property def unit_of_measurement(self): """Return the unit of measurement.""" - return POWER_WATT + return self._unit_of_measurement + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + + +class EnOceanPowerSensor(EnOceanSensor): + """Representation of an EnOcean power sensor. + + EEPs (EnOcean Equipment Profiles): + - A5-12-01 (Automated Meter Reading, Electricity) + """ + + def __init__(self, dev_id, dev_name): + """Initialize the EnOcean power sensor device.""" + super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER) + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + if packet.rorg != 0xA5: + return + packet.parse_eep(0x12, 0x01) + if packet.parsed['DT']['raw_value'] == 1: + # this packet reports the current value + raw_val = packet.parsed['MR']['raw_value'] + divisor = packet.parsed['DIV']['raw_value'] + self._state = raw_val / (10 ** divisor) + self.schedule_update_ha_state() + + +class EnOceanTemperatureSensor(EnOceanSensor): + """Representation of an EnOcean temperature sensor device. + + EEPs (EnOcean Equipment Profiles): + - A5-02-01 to A5-02-1B All 8 Bit Temperature Sensors of A5-02 + - A5-10-01 to A5-10-14 (Room Operating Panels) + - A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%) + - A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%) + - A5-10-10 (Temp. and Humidity Sensor and Set Point) + - A5-10-12 (Temp. and Humidity Sensor, Set Point and Occupancy Control) + - 10 Bit Temp. Sensors are not supported (A5-02-20, A5-02-30) + + For the following EEPs the scales must be set to "0 to 250": + - A5-04-01 + - A5-04-02 + - A5-10-10 to A5-10-14 + """ + + def __init__(self, dev_id, dev_name, scale_min, scale_max, + range_from, range_to): + """Initialize the EnOcean temperature sensor device.""" + super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE) + self._scale_min = scale_min + self._scale_max = scale_max + self.range_from = range_from + self.range_to = range_to + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + if packet.data[0] != 0xa5: + return + temp_scale = self._scale_max - self._scale_min + temp_range = self.range_to - self.range_from + raw_val = packet.data[3] + temperature = temp_scale / temp_range * (raw_val - self.range_from) + temperature += self._scale_min + self._state = round(temperature, 1) + self.schedule_update_ha_state() + + +class EnOceanHumiditySensor(EnOceanSensor): + """Representation of an EnOcean humidity sensor device. + + EEPs (EnOcean Equipment Profiles): + - A5-04-01 (Temp. and Humidity Sensor, Range 0°C to +40°C and 0% to 100%) + - A5-04-02 (Temp. and Humidity Sensor, Range -20°C to +60°C and 0% to 100%) + - A5-10-10 to A5-10-14 (Room Operating Panels) + """ + + def __init__(self, dev_id, dev_name): + """Initialize the EnOcean humidity sensor device.""" + super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY) + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + if packet.rorg != 0xA5: + return + humidity = packet.data[2] * 100 / 250 + self._state = round(humidity, 1) + self.schedule_update_ha_state() diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index f0b132c9d1c..48d53949a47 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -3,16 +3,16 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_ID) from homeassistant.components import enocean -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = 'EnOcean Switch' CONF_CHANNEL = 'channel' +DEFAULT_NAME = 'EnOcean Switch' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), @@ -23,26 +23,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the EnOcean switch platform.""" - dev_id = config.get(CONF_ID) - devname = config.get(CONF_NAME) channel = config.get(CONF_CHANNEL) + dev_id = config.get(CONF_ID) + dev_name = config.get(CONF_NAME) - add_entities([EnOceanSwitch(dev_id, devname, channel)]) + add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): """Representation of an EnOcean switch device.""" - def __init__(self, dev_id, devname, channel): + def __init__(self, dev_id, dev_name, channel): """Initialize the EnOcean switch device.""" - enocean.EnOceanDevice.__init__(self) - self.dev_id = dev_id - self._devname = devname + super().__init__(dev_id, dev_name) self._light = None self._on_state = False self._on_state2 = False self.channel = channel - self.stype = "switch" @property def is_on(self): @@ -52,7 +49,7 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): @property def name(self): """Return the device name.""" - return self._devname + return self.dev_name def turn_on(self, **kwargs): """Turn on the switch.""" @@ -74,7 +71,24 @@ class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): packet_type=0x01) self._on_state = False - def value_changed(self, val): + def value_changed(self, packet): """Update the internal state of the switch.""" - self._on_state = val - self.schedule_update_ha_state() + if packet.data[0] == 0xa5: + # power meter telegram, turn on if > 10 watts + packet.parse_eep(0x12, 0x01) + if packet.parsed['DT']['raw_value'] == 1: + raw_val = packet.parsed['MR']['raw_value'] + divisor = packet.parsed['DIV']['raw_value'] + watts = raw_val / (10 ** divisor) + if watts > 1: + self._on_state = True + self.schedule_update_ha_state() + elif packet.data[0] == 0xd2: + # actuator status telegram + packet.parse_eep(0x01, 0x01) + if packet.parsed['CMD']['raw_value'] == 4: + channel = packet.parsed['IO']['raw_value'] + output = packet.parsed['OV']['raw_value'] + if channel == self.channel: + self._on_state = output > 0 + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 2007491a903..b328c0f361d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -383,7 +383,7 @@ elkm1-lib==0.7.13 emulated_roku==0.1.8 # homeassistant.components.enocean -enocean==0.40 +enocean==0.50 # homeassistant.components.entur_public_transport enturclient==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c09da4b3bb8..7299a3cfdc5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -90,6 +90,9 @@ eebrightbox==0.0.4 # homeassistant.components.emulated_roku emulated_roku==0.1.8 +# homeassistant.components.enocean +enocean==0.50 + # homeassistant.components.season ephem==3.7.6.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 63b0ef737e2..9586dc17947 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -58,6 +58,7 @@ TEST_REQUIREMENTS = ( 'dsmr_parser', 'eebrightbox', 'emulated_roku', + 'enocean', 'ephem', 'evohomeclient', 'feedparser-homeassistant', From 0d796a0fb9adcc00f02fe1b4dfb32d177175dba7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 24 Apr 2019 21:09:01 -0600 Subject: [PATCH 113/346] Convert Pollen.com sensor into IQVIA component (#22986) * Moved pollen integration to iqvia * Stitched in new library * Added __init__ * Completed component v1 * Updated requirements * Updated CODEOWNERS * Updated .coveragerc * Removed requirements * Static check --- .coveragerc | 2 +- CODEOWNERS | 2 +- homeassistant/components/iqvia/__init__.py | 238 +++++++++++ homeassistant/components/iqvia/const.py | 45 ++ homeassistant/components/iqvia/manifest.json | 13 + homeassistant/components/iqvia/sensor.py | 210 +++++++++ homeassistant/components/pollen/__init__.py | 1 - homeassistant/components/pollen/manifest.json | 13 - homeassistant/components/pollen/sensor.py | 403 ------------------ requirements_all.txt | 8 +- requirements_test_all.txt | 2 +- 11 files changed, 513 insertions(+), 424 deletions(-) create mode 100644 homeassistant/components/iqvia/__init__.py create mode 100644 homeassistant/components/iqvia/const.py create mode 100644 homeassistant/components/iqvia/manifest.json create mode 100644 homeassistant/components/iqvia/sensor.py delete mode 100644 homeassistant/components/pollen/__init__.py delete mode 100644 homeassistant/components/pollen/manifest.json delete mode 100644 homeassistant/components/pollen/sensor.py diff --git a/.coveragerc b/.coveragerc index ac674b9fada..3aeb2b5c187 100644 --- a/.coveragerc +++ b/.coveragerc @@ -278,6 +278,7 @@ omit = homeassistant/components/ios/* homeassistant/components/iota/* homeassistant/components/iperf3/* + homeassistant/components/iqvia/* homeassistant/components/irish_rail_transport/sensor.py homeassistant/components/iss/binary_sensor.py homeassistant/components/isy994/* @@ -441,7 +442,6 @@ omit = homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* - homeassistant/components/pollen/sensor.py homeassistant/components/postnl/sensor.py homeassistant/components/prezzibenzina/sensor.py homeassistant/components/proliphix/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index c3126205810..700d68b9449 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -117,6 +117,7 @@ homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes homeassistant/components/ios/* @robbiet480 homeassistant/components/ipma/* @dgomes +homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/knx/* @Julius2342 @@ -170,7 +171,6 @@ homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/pi_hole/* @fabaff homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike -homeassistant/components/pollen/* @bachya homeassistant/components/ps4/* @ktnrg45 homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py new file mode 100644 index 00000000000..5806d7ea487 --- /dev/null +++ b/homeassistant/components/iqvia/__init__.py @@ -0,0 +1,238 @@ +"""Support for IQVIA.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL) +from homeassistant.core import callback +from homeassistant.helpers import ( + aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE, + TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_HISTORIC, TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_HISTORIC, + TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + TYPE_ASTHMA_YESTERDAY, TYPE_DISEASE_FORECAST) + +_LOGGER = logging.getLogger(__name__) + +CONF_ZIP_CODE = 'zip_code' + +DATA_CONFIG = 'config' + +DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + +NOTIFICATION_ID = 'iqvia_setup' +NOTIFICATION_TITLE = 'IQVIA Setup' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_ZIP_CODE): str, + vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the IQVIA component.""" + from pyiqvia import Client + from pyiqvia.errors import IQVIAError + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_LISTENER] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + websession = aiohttp_client.async_get_clientsession(hass) + + try: + iqvia = IQVIAData( + Client(conf[CONF_ZIP_CODE], websession), + conf[CONF_MONITORED_CONDITIONS]) + await iqvia.async_update() + except IQVIAError as err: + _LOGGER.error('Unable to set up IQVIA: %s', err) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(err), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + hass.data[DOMAIN][DATA_CLIENT] = iqvia + + discovery.load_platform(hass, 'sensor', DOMAIN, {}, conf) + + async def refresh(event_time): + """Refresh IQVIA data.""" + _LOGGER.debug('Updating IQVIA data') + await iqvia.async_update() + async_dispatcher_send(hass, TOPIC_DATA_UPDATE) + + hass.data[DOMAIN][DATA_LISTENER] = async_track_time_interval( + hass, refresh, + timedelta( + seconds=conf.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL.seconds))) + + return True + + +class IQVIAData: + """Define a data object to retrieve info from IQVIA.""" + + def __init__(self, client, sensor_types): + """Initialize.""" + self._client = client + self.data = {} + self.sensor_types = sensor_types + self.zip_code = client.zip_code + + async def _get_data(self, method, key): + """Return API data from a specific call.""" + from pyiqvia.errors import IQVIAError + + try: + data = await method() + self.data[key] = data + except IQVIAError as err: + _LOGGER.error('Unable to get "%s" data: %s', key, err) + self.data[key] = {} + + async def async_update(self): + """Update IQVIA data.""" + from pyiqvia.errors import InvalidZipError + + # IQVIA sites require a bit more complicated error handling, given that + # it sometimes has parts (but not the whole thing) go down: + # + # 1. If `InvalidZipError` is thrown, quit everything immediately. + # 2. If an individual request throws any other error, try the others. + try: + if TYPE_ALLERGY_FORECAST in self.sensor_types: + await self._get_data( + self._client.allergens.extended, TYPE_ALLERGY_FORECAST) + await self._get_data( + self._client.allergens.outlook, TYPE_ALLERGY_OUTLOOK) + + if TYPE_ALLERGY_HISTORIC in self.sensor_types: + await self._get_data( + self._client.allergens.historic, TYPE_ALLERGY_HISTORIC) + + if any(s in self.sensor_types + for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY]): + await self._get_data( + self._client.allergens.current, TYPE_ALLERGY_INDEX) + + if TYPE_ASTHMA_FORECAST in self.sensor_types: + await self._get_data( + self._client.asthma.extended, TYPE_ASTHMA_FORECAST) + + if TYPE_ASTHMA_HISTORIC in self.sensor_types: + await self._get_data( + self._client.asthma.historic, TYPE_ASTHMA_HISTORIC) + + if any(s in self.sensor_types + for s in [TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + TYPE_ASTHMA_YESTERDAY]): + await self._get_data( + self._client.asthma.current, TYPE_ASTHMA_INDEX) + + if TYPE_DISEASE_FORECAST in self.sensor_types: + await self._get_data( + self._client.disease.extended, TYPE_DISEASE_FORECAST) + + _LOGGER.debug("New data retrieved: %s", self.data) + except InvalidZipError: + _LOGGER.error( + "Cannot retrieve data for ZIP code: %s", self._client.zip_code) + self.data = {} + + +class IQVIAEntity(Entity): + """Define a base IQVIA entity.""" + + def __init__(self, iqvia, kind, name, icon, zip_code): + """Initialize the sensor.""" + self._async_unsub_dispatcher_connect = None + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._icon = icon + self._iqvia = iqvia + self._kind = kind + self._name = name + self._state = None + self._zip_code = zip_code + + @property + def available(self): + """Return True if entity is available.""" + if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY): + return self._iqvia.data.get(TYPE_ALLERGY_INDEX) is not None + + if self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + TYPE_ASTHMA_YESTERDAY): + return self._iqvia.data.get(TYPE_ASTHMA_INDEX) is not None + + return self._iqvia.data.get(self._kind) is not None + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format(self._zip_code, self._kind) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return 'index' + + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_DATA_UPDATE, update) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py new file mode 100644 index 00000000000..cd2d85a25a4 --- /dev/null +++ b/homeassistant/components/iqvia/const.py @@ -0,0 +1,45 @@ +"""Define IQVIA constants.""" +DOMAIN = 'iqvia' + +DATA_CLIENT = 'client' +DATA_LISTENER = 'listener' + +TOPIC_DATA_UPDATE = 'data_update' + +TYPE_ALLERGY_FORECAST = 'allergy_average_forecasted' +TYPE_ALLERGY_HISTORIC = 'allergy_average_historical' +TYPE_ALLERGY_INDEX = 'allergy_index' +TYPE_ALLERGY_OUTLOOK = 'allergy_outlook' +TYPE_ALLERGY_TODAY = 'allergy_index_today' +TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow' +TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday' +TYPE_ASTHMA_FORECAST = 'asthma_average_forecasted' +TYPE_ASTHMA_HISTORIC = 'asthma_average_historical' +TYPE_ASTHMA_INDEX = 'asthma_index' +TYPE_ASTHMA_TODAY = 'asthma_index_today' +TYPE_ASTHMA_TOMORROW = 'asthma_index_tomorrow' +TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday' +TYPE_DISEASE_FORECAST = 'disease_average_forecasted' + +SENSORS = { + TYPE_ALLERGY_FORECAST: ( + 'ForecastSensor', 'Allergy Index: Forecasted Average', 'mdi:flower'), + TYPE_ALLERGY_HISTORIC: ( + 'HistoricalSensor', 'Allergy Index: Historical Average', 'mdi:flower'), + TYPE_ALLERGY_TODAY: ('IndexSensor', 'Allergy Index: Today', 'mdi:flower'), + TYPE_ALLERGY_TOMORROW: ( + 'IndexSensor', 'Allergy Index: Tomorrow', 'mdi:flower'), + TYPE_ALLERGY_YESTERDAY: ( + 'IndexSensor', 'Allergy Index: Yesterday', 'mdi:flower'), + TYPE_ASTHMA_TODAY: ('IndexSensor', 'Asthma Index: Today', 'mdi:flower'), + TYPE_ASTHMA_TOMORROW: ( + 'IndexSensor', 'Asthma Index: Tomorrow', 'mdi:flower'), + TYPE_ASTHMA_YESTERDAY: ( + 'IndexSensor', 'Asthma Index: Yesterday', 'mdi:flower'), + TYPE_ASTHMA_FORECAST: ( + 'ForecastSensor', 'Asthma Index: Forecasted Average', 'mdi:flower'), + TYPE_ASTHMA_HISTORIC: ( + 'HistoricalSensor', 'Asthma Index: Historical Average', 'mdi:flower'), + TYPE_DISEASE_FORECAST: ( + 'ForecastSensor', 'Cold & Flu: Forecasted Average', 'mdi:snowflake') +} diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json new file mode 100644 index 00000000000..6c2365767d0 --- /dev/null +++ b/homeassistant/components/iqvia/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "iqvia", + "name": "IQVIA", + "documentation": "https://www.home-assistant.io/components/iqvia", + "requirements": [ + "numpy==1.16.2", + "pyiqvia==0.2.0" + ], + "dependencies": [], + "codeowners": [ + "@bachya" + ] +} diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py new file mode 100644 index 00000000000..1a139c51bf0 --- /dev/null +++ b/homeassistant/components/iqvia/sensor.py @@ -0,0 +1,210 @@ +"""Support for IQVIA sensors.""" +import logging +from statistics import mean + +from homeassistant.components.iqvia import ( + DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK, + TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, + TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, IQVIAEntity) +from homeassistant.const import ATTR_STATE + +_LOGGER = logging.getLogger(__name__) + +ATTR_ALLERGEN_AMOUNT = 'allergen_amount' +ATTR_ALLERGEN_GENUS = 'allergen_genus' +ATTR_ALLERGEN_NAME = 'allergen_name' +ATTR_ALLERGEN_TYPE = 'allergen_type' +ATTR_CITY = 'city' +ATTR_OUTLOOK = 'outlook' +ATTR_RATING = 'rating' +ATTR_SEASON = 'season' +ATTR_TREND = 'trend' +ATTR_ZIP_CODE = 'zip_code' + +RATING_MAPPING = [{ + 'label': 'Low', + 'minimum': 0.0, + 'maximum': 2.4 +}, { + 'label': 'Low/Medium', + 'minimum': 2.5, + 'maximum': 4.8 +}, { + 'label': 'Medium', + 'minimum': 4.9, + 'maximum': 7.2 +}, { + 'label': 'Medium/High', + 'minimum': 7.3, + 'maximum': 9.6 +}, { + 'label': 'High', + 'minimum': 9.7, + 'maximum': 12 +}] + +TREND_INCREASING = 'Increasing' +TREND_SUBSIDING = 'Subsiding' + + +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None): + """Configure the platform and add the sensors.""" + iqvia = hass.data[DOMAIN][DATA_CLIENT] + + sensors = [] + for kind in iqvia.sensor_types: + sensor_class, name, icon = SENSORS[kind] + sensors.append( + globals()[sensor_class](iqvia, kind, name, icon, iqvia.zip_code)) + + async_add_entities(sensors, True) + + +def calculate_average_rating(indices): + """Calculate the human-friendly historical allergy average.""" + ratings = list( + r['label'] for n in indices for r in RATING_MAPPING + if r['minimum'] <= n <= r['maximum']) + return max(set(ratings), key=ratings.count) + + +def calculate_trend(indices): + """Calculate the "moving average" of a set of indices.""" + import numpy as np + + def moving_average(data, samples): + """Determine the "moving average" (http://tinyurl.com/yaereb3c).""" + ret = np.cumsum(data, dtype=float) + ret[samples:] = ret[samples:] - ret[:-samples] + return ret[samples - 1:] / samples + + increasing = np.all(np.diff(moving_average(np.array(indices), 4)) > 0) + + if increasing: + return TREND_INCREASING + return TREND_SUBSIDING + + +class ForecastSensor(IQVIAEntity): + """Define sensor related to forecast data.""" + + async def async_update(self): + """Update the sensor.""" + await self._iqvia.async_update() + if not self._iqvia.data: + return + + data = self._iqvia.data[self._kind].get('Location') + if not data: + return + + indices = [p['Index'] for p in data['periods']] + average = round(mean(indices), 1) + [rating] = [ + i['label'] for i in RATING_MAPPING + if i['minimum'] <= average <= i['maximum'] + ] + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_TREND: calculate_trend(indices), + ATTR_ZIP_CODE: data['ZIP'] + }) + + if self._kind == TYPE_ALLERGY_FORECAST: + outlook = self._iqvia.data[TYPE_ALLERGY_OUTLOOK] + self._attrs[ATTR_OUTLOOK] = outlook.get('Outlook') + self._attrs[ATTR_SEASON] = outlook.get('Season') + + self._state = average + + +class HistoricalSensor(IQVIAEntity): + """Define sensor related to historical data.""" + + async def async_update(self): + """Update the sensor.""" + await self._iqvia.async_update() + if not self._iqvia.data: + return + + data = self._iqvia.data[self._kind].get('Location') + if not data: + return + + indices = [p['Index'] for p in data['periods']] + average = round(mean(indices), 1) + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: calculate_average_rating(indices), + ATTR_STATE: data['State'], + ATTR_TREND: calculate_trend(indices), + ATTR_ZIP_CODE: data['ZIP'] + }) + + self._state = average + + +class IndexSensor(IQVIAEntity): + """Define sensor related to indices.""" + + async def async_update(self): + """Update the sensor.""" + await self._iqvia.async_update() + if not self._iqvia.data: + return + + data = {} + if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY): + data = self._iqvia.data[TYPE_ALLERGY_INDEX].get('Location') + elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + TYPE_ASTHMA_YESTERDAY): + data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location') + + if not data: + return + + key = self._kind.split('_')[-1].title() + [period] = [p for p in data['periods'] if p['Type'] == key] + [rating] = [ + i['label'] for i in RATING_MAPPING + if i['minimum'] <= period['Index'] <= i['maximum'] + ] + + self._attrs.update({ + ATTR_CITY: data['City'].title(), + ATTR_RATING: rating, + ATTR_STATE: data['State'], + ATTR_ZIP_CODE: data['ZIP'] + }) + + if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ALLERGY_YESTERDAY): + for idx, attrs in enumerate(period['Triggers']): + index = idx + 1 + self._attrs.update({ + '{0}_{1}'.format(ATTR_ALLERGEN_GENUS, index): + attrs['Genus'], + '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): + attrs['Name'], + '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): + attrs['PlantType'], + }) + elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + TYPE_ASTHMA_YESTERDAY): + for idx, attrs in enumerate(period['Triggers']): + index = idx + 1 + self._attrs.update({ + '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): + attrs['Name'], + '{0}_{1}'.format(ATTR_ALLERGEN_AMOUNT, index): + attrs['PPM'], + }) + + self._state = period['Index'] diff --git a/homeassistant/components/pollen/__init__.py b/homeassistant/components/pollen/__init__.py deleted file mode 100644 index 566297ecb14..00000000000 --- a/homeassistant/components/pollen/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The pollen component.""" diff --git a/homeassistant/components/pollen/manifest.json b/homeassistant/components/pollen/manifest.json deleted file mode 100644 index 2edf83a0d1f..00000000000 --- a/homeassistant/components/pollen/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "pollen", - "name": "Pollen", - "documentation": "https://www.home-assistant.io/components/pollen", - "requirements": [ - "numpy==1.16.2", - "pypollencom==2.2.3" - ], - "dependencies": [], - "codeowners": [ - "@bachya" - ] -} diff --git a/homeassistant/components/pollen/sensor.py b/homeassistant/components/pollen/sensor.py deleted file mode 100644 index 132155c7f65..00000000000 --- a/homeassistant/components/pollen/sensor.py +++ /dev/null @@ -1,403 +0,0 @@ -"""Support for Pollen.com allergen and cold/flu sensors.""" -from datetime import timedelta -import logging -from statistics import mean - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_STATE, CONF_MONITORED_CONDITIONS) -from homeassistant.helpers import aiohttp_client -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -_LOGGER = logging.getLogger(__name__) - -ATTR_ALLERGEN_AMOUNT = 'allergen_amount' -ATTR_ALLERGEN_GENUS = 'allergen_genus' -ATTR_ALLERGEN_NAME = 'allergen_name' -ATTR_ALLERGEN_TYPE = 'allergen_type' -ATTR_CITY = 'city' -ATTR_OUTLOOK = 'outlook' -ATTR_RATING = 'rating' -ATTR_SEASON = 'season' -ATTR_TREND = 'trend' -ATTR_ZIP_CODE = 'zip_code' - -CONF_ZIP_CODE = 'zip_code' - -DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) - -TYPE_ALLERGY_FORECAST = 'allergy_average_forecasted' -TYPE_ALLERGY_HISTORIC = 'allergy_average_historical' -TYPE_ALLERGY_INDEX = 'allergy_index' -TYPE_ALLERGY_OUTLOOK = 'allergy_outlook' -TYPE_ALLERGY_TODAY = 'allergy_index_today' -TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow' -TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday' -TYPE_ASTHMA_FORECAST = 'asthma_average_forecasted' -TYPE_ASTHMA_HISTORIC = 'asthma_average_historical' -TYPE_ASTHMA_INDEX = 'asthma_index' -TYPE_ASTHMA_TODAY = 'asthma_index_today' -TYPE_ASTHMA_TOMORROW = 'asthma_index_tomorrow' -TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday' -TYPE_DISEASE_FORECAST = 'disease_average_forecasted' - -SENSORS = { - TYPE_ALLERGY_FORECAST: ( - 'ForecastSensor', 'Allergy Index: Forecasted Average', 'mdi:flower'), - TYPE_ALLERGY_HISTORIC: ( - 'HistoricalSensor', 'Allergy Index: Historical Average', 'mdi:flower'), - TYPE_ALLERGY_TODAY: ('IndexSensor', 'Allergy Index: Today', 'mdi:flower'), - TYPE_ALLERGY_TOMORROW: ( - 'IndexSensor', 'Allergy Index: Tomorrow', 'mdi:flower'), - TYPE_ALLERGY_YESTERDAY: ( - 'IndexSensor', 'Allergy Index: Yesterday', 'mdi:flower'), - TYPE_ASTHMA_TODAY: ('IndexSensor', 'Asthma Index: Today', 'mdi:flower'), - TYPE_ASTHMA_TOMORROW: ( - 'IndexSensor', 'Asthma Index: Tomorrow', 'mdi:flower'), - TYPE_ASTHMA_YESTERDAY: ( - 'IndexSensor', 'Asthma Index: Yesterday', 'mdi:flower'), - TYPE_ASTHMA_FORECAST: ( - 'ForecastSensor', 'Asthma Index: Forecasted Average', 'mdi:flower'), - TYPE_ASTHMA_HISTORIC: ( - 'HistoricalSensor', 'Asthma Index: Historical Average', 'mdi:flower'), - TYPE_DISEASE_FORECAST: ( - 'ForecastSensor', 'Cold & Flu: Forecasted Average', 'mdi:snowflake') -} - -RATING_MAPPING = [{ - 'label': 'Low', - 'minimum': 0.0, - 'maximum': 2.4 -}, { - 'label': 'Low/Medium', - 'minimum': 2.5, - 'maximum': 4.8 -}, { - 'label': 'Medium', - 'minimum': 4.9, - 'maximum': 7.2 -}, { - 'label': 'Medium/High', - 'minimum': 7.3, - 'maximum': 9.6 -}, { - 'label': 'High', - 'minimum': 9.7, - 'maximum': 12 -}] - -TREND_INCREASING = 'Increasing' -TREND_SUBSIDING = 'Subsiding' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_ZIP_CODE): - str, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): - vol.All(cv.ensure_list, [vol.In(SENSORS)]) -}) - - -async def async_setup_platform( - hass, config, async_add_entities, discovery_info=None): - """Configure the platform and add the sensors.""" - from pypollencom import Client - - websession = aiohttp_client.async_get_clientsession(hass) - - pollen = PollenComData( - Client(config[CONF_ZIP_CODE], websession), - config[CONF_MONITORED_CONDITIONS]) - - await pollen.async_update() - - sensors = [] - for kind in config[CONF_MONITORED_CONDITIONS]: - sensor_class, name, icon = SENSORS[kind] - sensors.append( - globals()[sensor_class]( - pollen, kind, name, icon, config[CONF_ZIP_CODE])) - - async_add_entities(sensors, True) - - -def calculate_average_rating(indices): - """Calculate the human-friendly historical allergy average.""" - ratings = list( - r['label'] for n in indices for r in RATING_MAPPING - if r['minimum'] <= n <= r['maximum']) - return max(set(ratings), key=ratings.count) - - -def calculate_trend(indices): - """Calculate the "moving average" of a set of indices.""" - import numpy as np - - def moving_average(data, samples): - """Determine the "moving average" (http://tinyurl.com/yaereb3c).""" - ret = np.cumsum(data, dtype=float) - ret[samples:] = ret[samples:] - ret[:-samples] - return ret[samples - 1:] / samples - - increasing = np.all(np.diff(moving_average(np.array(indices), 4)) > 0) - - if increasing: - return TREND_INCREASING - return TREND_SUBSIDING - - -class BaseSensor(Entity): - """Define a base Pollen.com sensor.""" - - def __init__(self, pollen, kind, name, icon, zip_code): - """Initialize the sensor.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._icon = icon - self._kind = kind - self._name = name - self._state = None - self._zip_code = zip_code - self.pollen = pollen - - @property - def available(self): - """Return True if entity is available.""" - if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY): - return bool(self.pollen.data[TYPE_ALLERGY_INDEX]) - - if self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY): - return bool(self.pollen.data[TYPE_ASTHMA_INDEX]) - - return bool(self.pollen.data[self._kind]) - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return self._icon - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}'.format(self._zip_code, self._kind) - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return 'index' - - -class ForecastSensor(BaseSensor): - """Define sensor related to forecast data.""" - - async def async_update(self): - """Update the sensor.""" - await self.pollen.async_update() - if not self.pollen.data: - return - - data = self.pollen.data[self._kind].get('Location') - if not data: - return - - indices = [p['Index'] for p in data['periods']] - average = round(mean(indices), 1) - [rating] = [ - i['label'] for i in RATING_MAPPING - if i['minimum'] <= average <= i['maximum'] - ] - - self._attrs.update({ - ATTR_CITY: data['City'].title(), - ATTR_RATING: rating, - ATTR_STATE: data['State'], - ATTR_TREND: calculate_trend(indices), - ATTR_ZIP_CODE: data['ZIP'] - }) - - if self._kind == TYPE_ALLERGY_FORECAST: - outlook = self.pollen.data[TYPE_ALLERGY_OUTLOOK] - self._attrs[ATTR_OUTLOOK] = outlook.get('Outlook') - self._attrs[ATTR_SEASON] = outlook.get('Season') - - self._state = average - - -class HistoricalSensor(BaseSensor): - """Define sensor related to historical data.""" - - async def async_update(self): - """Update the sensor.""" - await self.pollen.async_update() - if not self.pollen.data: - return - - data = self.pollen.data[self._kind].get('Location') - if not data: - return - - indices = [p['Index'] for p in data['periods']] - average = round(mean(indices), 1) - - self._attrs.update({ - ATTR_CITY: data['City'].title(), - ATTR_RATING: calculate_average_rating(indices), - ATTR_STATE: data['State'], - ATTR_TREND: calculate_trend(indices), - ATTR_ZIP_CODE: data['ZIP'] - }) - - self._state = average - - -class IndexSensor(BaseSensor): - """Define sensor related to indices.""" - - async def async_update(self): - """Update the sensor.""" - await self.pollen.async_update() - if not self.pollen.data: - return - - data = {} - if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY): - data = self.pollen.data[TYPE_ALLERGY_INDEX].get('Location') - elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY): - data = self.pollen.data[TYPE_ASTHMA_INDEX].get('Location') - - if not data: - return - - key = self._kind.split('_')[-1].title() - [period] = [p for p in data['periods'] if p['Type'] == key] - [rating] = [ - i['label'] for i in RATING_MAPPING - if i['minimum'] <= period['Index'] <= i['maximum'] - ] - - self._attrs.update({ - ATTR_CITY: data['City'].title(), - ATTR_RATING: rating, - ATTR_STATE: data['State'], - ATTR_ZIP_CODE: data['ZIP'] - }) - - if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY): - for idx, attrs in enumerate(period['Triggers']): - index = idx + 1 - self._attrs.update({ - '{0}_{1}'.format(ATTR_ALLERGEN_GENUS, index): - attrs['Genus'], - '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): - attrs['Name'], - '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): - attrs['PlantType'], - }) - elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY): - for idx, attrs in enumerate(period['Triggers']): - index = idx + 1 - self._attrs.update({ - '{0}_{1}'.format(ATTR_ALLERGEN_NAME, index): - attrs['Name'], - '{0}_{1}'.format(ATTR_ALLERGEN_AMOUNT, index): - attrs['PPM'], - }) - - self._state = period['Index'] - - -class PollenComData: - """Define a data object to retrieve info from Pollen.com.""" - - def __init__(self, client, sensor_types): - """Initialize.""" - self._client = client - self._sensor_types = sensor_types - self.data = {} - - async def _get_data(self, method, key): - """Return API data from a specific call.""" - from pypollencom.errors import PollenComError - - try: - data = await method() - self.data[key] = data - except PollenComError as err: - _LOGGER.error('Unable to get "%s" data: %s', key, err) - self.data[key] = {} - - @Throttle(DEFAULT_SCAN_INTERVAL) - async def async_update(self): - """Update Pollen.com data.""" - from pypollencom.errors import InvalidZipError - - # Pollen.com requires a bit more complicated error handling, given that - # it sometimes has parts (but not the whole thing) go down: - # - # 1. If `InvalidZipError` is thrown, quit everything immediately. - # 2. If an individual request throws any other error, try the others. - - try: - if TYPE_ALLERGY_FORECAST in self._sensor_types: - await self._get_data( - self._client.allergens.extended, TYPE_ALLERGY_FORECAST) - await self._get_data( - self._client.allergens.outlook, TYPE_ALLERGY_OUTLOOK) - - if TYPE_ALLERGY_HISTORIC in self._sensor_types: - await self._get_data( - self._client.allergens.historic, TYPE_ALLERGY_HISTORIC) - - if any(s in self._sensor_types - for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY]): - await self._get_data( - self._client.allergens.current, TYPE_ALLERGY_INDEX) - - if TYPE_ASTHMA_FORECAST in self._sensor_types: - await self._get_data( - self._client.asthma.extended, TYPE_ASTHMA_FORECAST) - - if TYPE_ASTHMA_HISTORIC in self._sensor_types: - await self._get_data( - self._client.asthma.historic, TYPE_ASTHMA_HISTORIC) - - if any(s in self._sensor_types - for s in [TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY]): - await self._get_data( - self._client.asthma.current, TYPE_ASTHMA_INDEX) - - if TYPE_DISEASE_FORECAST in self._sensor_types: - await self._get_data( - self._client.disease.extended, TYPE_DISEASE_FORECAST) - - _LOGGER.debug("New data retrieved: %s", self.data) - except InvalidZipError: - _LOGGER.error( - "Cannot retrieve data for ZIP code: %s", self._client.zip_code) - self.data = {} diff --git a/requirements_all.txt b/requirements_all.txt index b328c0f361d..bc48a6538a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -763,8 +763,8 @@ nsw-fuel-api-client==1.0.10 # homeassistant.components.nuheat nuheat==0.3.0 +# homeassistant.components.iqvia # homeassistant.components.opencv -# homeassistant.components.pollen # homeassistant.components.tensorflow # homeassistant.components.trend numpy==1.16.2 @@ -1105,6 +1105,9 @@ pyicloud==0.9.1 # homeassistant.components.ipma pyipma==1.2.1 +# homeassistant.components.iqvia +pyiqvia==0.2.0 + # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 @@ -1230,9 +1233,6 @@ pypjlink2==1.2.0 # homeassistant.components.point pypoint==1.1.1 -# homeassistant.components.pollen -pypollencom==2.2.3 - # homeassistant.components.ps4 pyps4-homeassistant==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7299a3cfdc5..cd20177bcdd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -175,8 +175,8 @@ mbddns==0.1.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.iqvia # homeassistant.components.opencv -# homeassistant.components.pollen # homeassistant.components.tensorflow # homeassistant.components.trend numpy==1.16.2 From ec9db7f9a2f615e43be8584e962881b744e52390 Mon Sep 17 00:00:00 2001 From: dreed47 Date: Thu, 25 Apr 2019 00:11:07 -0400 Subject: [PATCH 114/346] fix for issue #21381 (#23306) --- homeassistant/components/zestimate/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 0a1f14324f6..036422d6800 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -47,10 +47,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Zestimate sensor.""" name = config.get(CONF_NAME) properties = config[CONF_ZPID] - params = {'zws-id': config[CONF_API_KEY]} sensors = [] for zpid in properties: + params = {'zws-id': config[CONF_API_KEY]} params['zpid'] = zpid sensors.append(ZestimateDataSensor(name, params)) add_entities(sensors, True) From 24766df1791c5cabe6e53d4a4a4d9c2dd7f31601 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Thu, 25 Apr 2019 06:13:31 +0200 Subject: [PATCH 115/346] Upgrade to pyubee==0.6 (#23355) --- homeassistant/components/ubee/device_tracker.py | 8 +++++++- homeassistant/components/ubee/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py index 8e610a4f51c..b81a2320b5e 100644 --- a/homeassistant/components/ubee/device_tracker.py +++ b/homeassistant/components/ubee/device_tracker.py @@ -18,7 +18,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string + vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): + vol.Any( + 'EVW32C-0N', + 'EVW320B', + 'EVW3200-Wifi', + 'EVW3226@UPC', + ), }) diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index 524dcb1d77b..f9f17e41546 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -3,7 +3,7 @@ "name": "Ubee", "documentation": "https://www.home-assistant.io/components/ubee", "requirements": [ - "pyubee==0.5" + "pyubee==0.6" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index bc48a6538a3..23d0c480074 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,7 +1441,7 @@ pytradfri[async]==6.0.1 pytrafikverket==0.1.5.9 # homeassistant.components.ubee -pyubee==0.5 +pyubee==0.6 # homeassistant.components.unifi pyunifi==2.16 From 6fb5b8467b4f54b409388f370d7c98c7f5a7f1e6 Mon Sep 17 00:00:00 2001 From: Ian Date: Wed, 24 Apr 2019 21:30:46 -0700 Subject: [PATCH 116/346] Fix tox.ini lint target (#23359) tox fails due to being unable to reference the `script` module when trying to run `script/gen_requirements_all.py`. Instead it needs to be run as a module. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d0c4336f544..003861d2107 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = deps = -r{toxinidir}/requirements_test.txt commands = - python script/gen_requirements_all.py validate + python -m script.gen_requirements_all validate flake8 {posargs} pydocstyle {posargs:homeassistant tests} From e3e7fb5ff61b217c1c629e610b105481882d8d22 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Wed, 24 Apr 2019 23:31:32 -0500 Subject: [PATCH 117/346] Bump pyheos to 0.4.1 (#23360) * Bump pyheos==0.4.1 * Refresh player after reconnection --- homeassistant/components/heos/manifest.json | 2 +- homeassistant/components/heos/media_player.py | 10 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/heos/test_media_player.py | 31 +++++++++++++++++-- 5 files changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 97b53935614..5b0a8e67893 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -3,7 +3,7 @@ "name": "Heos", "documentation": "https://www.home-assistant.io/components/heos", "requirements": [ - "pyheos==0.4.0" + "pyheos==0.4.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 56e9647df50..ae1b1c32003 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,4 +1,5 @@ """Denon HEOS Media Player.""" +import asyncio from functools import reduce, wraps import logging from operator import ior @@ -48,7 +49,7 @@ def log_command_error(command: str): from pyheos import CommandError try: await func(*args, **kwargs) - except CommandError as ex: + except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: _LOGGER.error("Unable to %s: %s", command, ex) return wrapper return decorator @@ -86,6 +87,13 @@ class HeosMediaPlayer(MediaPlayerDevice): async def _heos_event(self, event): """Handle connection event.""" + from pyheos import CommandError, const + if event == const.EVENT_CONNECTED: + try: + await self._player.refresh() + except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: + _LOGGER.error("Unable to refresh player %s: %s", + self._player, ex) await self.async_update_ha_state(True) async def _player_update(self, player_id, event): diff --git a/requirements_all.txt b/requirements_all.txt index 23d0c480074..902e921b74e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1079,7 +1079,7 @@ pygtt==1.1.2 pyhaversion==2.2.1 # homeassistant.components.heos -pyheos==0.4.0 +pyheos==0.4.1 # homeassistant.components.hikvision pyhik==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd20177bcdd..dd44ba61575 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -223,7 +223,7 @@ pydeconz==54 pydispatcher==2.0.5 # homeassistant.components.heos -pyheos==0.4.0 +pyheos==0.4.1 # homeassistant.components.homematic pyhomematic==0.1.58 diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index 4cf871f5ed0..e3e02110258 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -109,13 +109,40 @@ async def test_updates_start_from_signals( state = hass.states.get('media_player.test_player') assert state.state == STATE_UNAVAILABLE - # Test heos events update + +async def test_updates_from_connection_event( + hass, config_entry, config, controller, input_sources, caplog): + """Tests player updates from connection event after connection failure.""" + # Connected + await setup_platform(hass, config_entry, config) + player = controller.players[1] player.available = True player.heos.dispatcher.send( const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) await hass.async_block_till_done() state = hass.states.get('media_player.test_player') - assert state.state == STATE_PLAYING + assert state.state == STATE_IDLE + assert player.refresh.call_count == 1 + + # Connected handles refresh failure + player.reset_mock() + player.refresh.side_effect = CommandError(None, "Failure", 1) + player.heos.dispatcher.send( + const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) + await hass.async_block_till_done() + state = hass.states.get('media_player.test_player') + assert player.refresh.call_count == 1 + assert "Unable to refresh player" in caplog.text + + # Disconnected + player.reset_mock() + player.available = False + player.heos.dispatcher.send( + const.SIGNAL_HEOS_EVENT, const.EVENT_DISCONNECTED) + await hass.async_block_till_done() + state = hass.states.get('media_player.test_player') + assert state.state == STATE_UNAVAILABLE + assert player.refresh.call_count == 0 async def test_updates_from_sources_updated( From de6fdb09f493614a8b4ae245eeb1354cccc3a66b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 24 Apr 2019 22:37:29 -0700 Subject: [PATCH 118/346] Add media player external url (#23337) * Add media player external url * Lint * Simplify * Update __init__.py * Update __init__.py * Use 302 --- homeassistant/components/cast/media_player.py | 5 +++ .../components/media_player/__init__.py | 16 ++++++++ tests/components/media_player/test_init.py | 41 ++++++++++++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 4b2972b0c00..0a1406adcee 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1046,6 +1046,11 @@ class CastDevice(MediaPlayerDevice): return images[0].url if images and images[0].url else None + @property + def media_image_remotely_accessible(self) -> bool: + """If the image url is remotely accessible.""" + return True + @property def media_title(self): """Title of current playing media.""" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 7dcfdac5217..478f59d2817 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -324,6 +324,11 @@ class MediaPlayerDevice(Entity): """Image url of current playing media.""" return None + @property + def media_image_remotely_accessible(self) -> bool: + """If the image url is remotely accessible.""" + return False + @property def media_image_hash(self): """Hash value for media image.""" @@ -722,6 +727,9 @@ class MediaPlayerDevice(Entity): if self.state == STATE_OFF: return None + if self.media_image_remotely_accessible: + return self.media_image_url + image_hash = self.media_image_hash if image_hash is None: @@ -808,6 +816,14 @@ class MediaPlayerImageView(HomeAssistantView): if not authenticated: return web.Response(status=401) + if player.media_image_remotely_accessible: + url = player.media_image_url + if url is not None: + return web.Response(status=302, headers={ + 'location': url + }) + return web.Response(status=500) + data, content_type = await player.async_get_media_image() if data is None: diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 808c6e4f50f..23deffa972a 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -8,8 +8,8 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from tests.common import mock_coro -async def test_get_panels(hass, hass_ws_client): - """Test get_panels command.""" +async def test_get_image(hass, hass_ws_client): + """Test get image via WS command.""" await async_setup_component(hass, 'media_player', { 'media_player': { 'platform': 'demo' @@ -35,3 +35,40 @@ async def test_get_panels(hass, hass_ws_client): assert msg['result']['content_type'] == 'image/jpeg' assert msg['result']['content'] == \ base64.b64encode(b'image').decode('utf-8') + + +async def test_get_image_http(hass, hass_client): + """Test get image via http command.""" + await async_setup_component(hass, 'media_player', { + 'media_player': { + 'platform': 'demo' + } + }) + + client = await hass_client() + + with patch('homeassistant.components.media_player.MediaPlayerDevice.' + 'async_get_media_image', return_value=mock_coro( + (b'image', 'image/jpeg'))): + resp = await client.get('/api/media_player_proxy/media_player.bedroom') + content = await resp.read() + + assert content == b'image' + + +async def test_get_image_http_url(hass, hass_client): + """Test get image url via http command.""" + await async_setup_component(hass, 'media_player', { + 'media_player': { + 'platform': 'demo' + } + }) + + client = await hass_client() + + with patch('homeassistant.components.media_player.MediaPlayerDevice.' + 'media_image_remotely_accessible', return_value=True): + resp = await client.get('/api/media_player_proxy/media_player.bedroom', + allow_redirects=False) + assert resp.headers['Location'] == \ + 'https://img.youtube.com/vi/kxopViU98Xo/hqdefault.jpg' From c216ac72608e8a9943eab38ea716f9d9d5a8421c Mon Sep 17 00:00:00 2001 From: Richard Mitchell Date: Thu, 25 Apr 2019 07:38:10 +0200 Subject: [PATCH 119/346] Fix race condition. (#21244) If the updater is running at the same time, this can result in this dict changing size during iteration, which Python does not like. --- homeassistant/components/plex/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4cb4204f274..4a65808e049 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -172,12 +172,15 @@ def setup_plexserver( # add devices with a session and no client (ex. PlexConnect Apple TV's) if config.get(CONF_INCLUDE_NON_CLIENTS): - for machine_identifier, (session, player) in plex_sessions.items(): + # To avoid errors when plex sessions created during iteration + sessions = list(plex_sessions.items()) + for machine_identifier, (session, player) in sessions: if machine_identifier in available_client_ids: # Avoid using session if already added as a device. _LOGGER.debug("Skipping session, device exists: %s", machine_identifier) continue + if (machine_identifier not in plex_clients and machine_identifier is not None): new_client = PlexClient( From 86b017e2f051d954b08938efc698c15cdfafc2ff Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 25 Apr 2019 00:39:49 -0500 Subject: [PATCH 120/346] Add amcrest camera services and deprecate switches (#22949) * Add amcrest camera services and deprecate switches - Implement enabling and disabling motion detection from camera platform. - Add amcrest specific camera services for controlling audio stream, motion recording, continuous recording and camera color mode, as well as moving camera to PTZ preset and starting and stopping PTZ tour function. - Add camera attributes to indicate the state of the various camera settings controlled by the new services. - Deprecate switches in favor of camera services and attributes. * Rename services and move service handling to __init__.py Rename services from 'camera.amcrest_xxx' to 'amcrest.xxx'. This allows services to be documented in services.yaml. Add services.yaml. Reorganize hass.data[DATA_AMCREST] and do some general cleanup to make various platform modules more consistent. Move service handling code to __init__.py from camera.py. * Update per review comments, part 1 - Rebase - Add permission checking to services - Change cv.ensure_list_csv to cv.ensure_list - Add comment for "pointless-statement" in setup - Change handler_services to handled_services - Remove check if services have alreaday been registered - Pass ffmpeg instead of hass to AmcrestCam __init__ - Remove writing motion_detection attr from device_state_attributes - Change service methods from callbacks to coroutines * Update per review comments, part 2 - Use dispatcher to signal camera entities to run services. - Reorganize a bit, including moving a few things to new modules const.py & helpers.py. * Update per review comments, part 3 Move call data extraction from camera.py to __init__.py. --- homeassistant/components/amcrest/__init__.py | 220 +++++++----- .../components/amcrest/binary_sensor.py | 33 +- homeassistant/components/amcrest/camera.py | 312 ++++++++++++++++-- homeassistant/components/amcrest/const.py | 7 + homeassistant/components/amcrest/helpers.py | 10 + homeassistant/components/amcrest/sensor.py | 54 +-- .../components/amcrest/services.yaml | 75 +++++ homeassistant/components/amcrest/switch.py | 53 ++- 8 files changed, 582 insertions(+), 182 deletions(-) create mode 100644 homeassistant/components/amcrest/const.py create mode 100644 homeassistant/components/amcrest/helpers.py create mode 100644 homeassistant/components/amcrest/services.yaml diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index 3a0a983fceb..6de31caa90e 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -5,16 +5,30 @@ from datetime import timedelta import aiohttp import voluptuous as vol +from homeassistant.auth.permissions.const import POLICY_CONTROL +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.camera import DOMAIN as CAMERA +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, - CONF_BINARY_SENSORS, CONF_SENSORS, CONF_SWITCHES, CONF_SCAN_INTERVAL, - HTTP_BASIC_AUTHENTICATION) + ATTR_ENTITY_ID, CONF_AUTHENTICATION, CONF_BINARY_SENSORS, CONF_HOST, + CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, + CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION) +from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.service import async_extract_entity_ids + +from .binary_sensor import BINARY_SENSORS +from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST +from .const import DOMAIN, DATA_AMCREST +from .helpers import service_signal +from .sensor import SENSOR_MOTION_DETECTOR, SENSORS +from .switch import SWITCHES _LOGGER = logging.getLogger(__name__) -CONF_AUTHENTICATION = 'authentication' CONF_RESOLUTION = 'resolution' CONF_STREAM_SOURCE = 'stream_source' CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' @@ -22,12 +36,7 @@ CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments' DEFAULT_NAME = 'Amcrest Camera' DEFAULT_PORT = 80 DEFAULT_RESOLUTION = 'high' -DEFAULT_STREAM_SOURCE = 'snapshot' DEFAULT_ARGUMENTS = '-pred 1' -TIMEOUT = 10 - -DATA_AMCREST = 'amcrest' -DOMAIN = 'amcrest' NOTIFICATION_ID = 'amcrest_notification' NOTIFICATION_TITLE = 'Amcrest Camera Setup' @@ -43,70 +52,60 @@ AUTHENTICATION_LIST = { 'basic': 'basic' } -STREAM_SOURCE_LIST = { - 'mjpeg': 0, - 'snapshot': 1, - 'rtsp': 2, -} -BINARY_SENSORS = { - 'motion_detected': 'Motion Detected' -} - -# Sensor types are defined like: Name, units, icon -SENSOR_MOTION_DETECTOR = 'motion_detector' -SENSORS = { - SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'], - 'sdcard': ['SD Used', '%', 'mdi:sd'], - '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'] -} - - -def _deprecated_sensors(value): - if SENSOR_MOTION_DETECTOR in value: +def _deprecated_sensor_values(sensors): + if SENSOR_MOTION_DETECTOR in sensors: _LOGGER.warning( - 'sensors option %s is deprecated. ' - 'Please remove from your configuration and ' - 'use binary_sensors option motion_detected instead.', - SENSOR_MOTION_DETECTOR) - return value + "The 'sensors' option value '%s' is deprecated, " + "please remove it from your configuration and use " + "the 'binary_sensors' option with value 'motion_detected' " + "instead.", SENSOR_MOTION_DETECTOR) + return sensors -def _has_unique_names(value): - names = [camera[CONF_NAME] for camera in value] +def _deprecated_switches(config): + if CONF_SWITCHES in config: + _LOGGER.warning( + "The 'switches' option (with value %s) is deprecated, " + "please remove it from your configuration and use " + "camera services and attributes instead.", + config[CONF_SWITCHES]) + return config + + +def _has_unique_names(devices): + names = [device[CONF_NAME] for device in devices] vol.Schema(vol.Unique())(names) - return value + return devices -AMCREST_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): - vol.All(vol.In(AUTHENTICATION_LIST)), - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): - vol.All(vol.In(RESOLUTION_LIST)), - vol.Optional(CONF_STREAM_SOURCE, default=DEFAULT_STREAM_SOURCE): - vol.All(vol.In(STREAM_SOURCE_LIST)), - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): - cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): - cv.time_period, - vol.Optional(CONF_BINARY_SENSORS): - vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), - vol.Optional(CONF_SENSORS): - vol.All(cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensors), - vol.Optional(CONF_SWITCHES): - vol.All(cv.ensure_list, [vol.In(SWITCHES)]), -}) +AMCREST_SCHEMA = vol.All( + vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): + vol.All(vol.In(AUTHENTICATION_LIST)), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): + vol.All(vol.In(RESOLUTION_LIST)), + vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): + vol.All(vol.In(STREAM_SOURCE_LIST)), + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): + cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_BINARY_SENSORS): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSORS)], + _deprecated_sensor_values), + vol.Optional(CONF_SWITCHES): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + }), + _deprecated_switches +) CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.All(cv.ensure_list, [AMCREST_SCHEMA], _has_unique_names) @@ -117,21 +116,22 @@ def setup(hass, config): """Set up the Amcrest IP Camera component.""" from amcrest import AmcrestCamera, AmcrestError - hass.data.setdefault(DATA_AMCREST, {}) - amcrest_cams = config[DOMAIN] + hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []}) + devices = config[DOMAIN] - for device in amcrest_cams: + for device in devices: name = device[CONF_NAME] username = device[CONF_USERNAME] password = device[CONF_PASSWORD] try: - camera = AmcrestCamera(device[CONF_HOST], - device[CONF_PORT], - username, - password).camera + api = AmcrestCamera(device[CONF_HOST], + device[CONF_PORT], + username, + password).camera # pylint: disable=pointless-statement - camera.current_time + # Test camera communications. + api.current_time except AmcrestError as ex: _LOGGER.error("Unable to connect to %s camera: %s", name, str(ex)) @@ -148,7 +148,7 @@ def setup(hass, config): binary_sensors = device.get(CONF_BINARY_SENSORS) sensors = device.get(CONF_SENSORS) switches = device.get(CONF_SWITCHES) - stream_source = STREAM_SOURCE_LIST[device[CONF_STREAM_SOURCE]] + stream_source = device[CONF_STREAM_SOURCE] # currently aiohttp only works with basic authentication # only valid for mjpeg streaming @@ -157,47 +157,97 @@ def setup(hass, config): else: authentication = None - hass.data[DATA_AMCREST][name] = AmcrestDevice( - camera, name, authentication, ffmpeg_arguments, stream_source, + hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice( + api, authentication, ffmpeg_arguments, stream_source, resolution) discovery.load_platform( - hass, 'camera', DOMAIN, { + hass, CAMERA, DOMAIN, { CONF_NAME: name, }, config) if binary_sensors: discovery.load_platform( - hass, 'binary_sensor', DOMAIN, { + hass, BINARY_SENSOR, DOMAIN, { CONF_NAME: name, CONF_BINARY_SENSORS: binary_sensors }, config) if sensors: discovery.load_platform( - hass, 'sensor', DOMAIN, { + hass, SENSOR, DOMAIN, { CONF_NAME: name, CONF_SENSORS: sensors, }, config) if switches: discovery.load_platform( - hass, 'switch', DOMAIN, { + hass, SWITCH, DOMAIN, { CONF_NAME: name, CONF_SWITCHES: switches }, config) - return len(hass.data[DATA_AMCREST]) >= 1 + if not hass.data[DATA_AMCREST]['devices']: + return False + + def have_permission(user, entity_id): + return not user or user.permissions.check_entity( + entity_id, POLICY_CONTROL) + + async def async_extract_from_service(call): + if call.context.user_id: + user = await hass.auth.async_get_user(call.context.user_id) + if user is None: + raise UnknownUser(context=call.context) + else: + user = None + + if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: + # Return all entity_ids user has permission to control. + return [ + entity_id for entity_id in hass.data[DATA_AMCREST]['cameras'] + if have_permission(user, entity_id) + ] + + call_ids = await async_extract_entity_ids(hass, call) + entity_ids = [] + for entity_id in hass.data[DATA_AMCREST]['cameras']: + if entity_id not in call_ids: + continue + if not have_permission(user, entity_id): + raise Unauthorized( + context=call.context, + entity_id=entity_id, + permission=POLICY_CONTROL + ) + entity_ids.append(entity_id) + return entity_ids + + async def async_service_handler(call): + args = [] + for arg in CAMERA_SERVICES[call.service][2]: + args.append(call.data[arg]) + for entity_id in await async_extract_from_service(call): + async_dispatcher_send( + hass, + service_signal(call.service, entity_id), + *args + ) + + for service, params in CAMERA_SERVICES.items(): + hass.services.async_register( + DOMAIN, service, async_service_handler, params[0]) + + return True class AmcrestDevice: """Representation of a base Amcrest discovery device.""" - def __init__(self, camera, name, authentication, ffmpeg_arguments, + def __init__(self, api, authentication, ffmpeg_arguments, stream_source, resolution): """Initialize the entity.""" - self.device = camera - self.name = name + self.api = api self.authentication = authentication self.ffmpeg_arguments = ffmpeg_arguments self.stream_source = stream_source diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index b591616a88d..0eb9e42e707 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -5,38 +5,39 @@ import logging from homeassistant.components.binary_sensor import ( BinarySensorDevice, DEVICE_CLASS_MOTION) from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS -from . import DATA_AMCREST, BINARY_SENSORS + +from .const import BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS) + +BINARY_SENSORS = { + 'motion_detected': 'Motion Detected' +} -async def async_setup_platform(hass, config, async_add_devices, +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a binary sensor for an Amcrest IP Camera.""" if discovery_info is None: return - device_name = discovery_info[CONF_NAME] - binary_sensors = discovery_info[CONF_BINARY_SENSORS] - amcrest = hass.data[DATA_AMCREST][device_name] - - amcrest_binary_sensors = [] - for sensor_type in binary_sensors: - amcrest_binary_sensors.append( - AmcrestBinarySensor(amcrest.name, amcrest.device, sensor_type)) - - async_add_devices(amcrest_binary_sensors, True) + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST]['devices'][name] + async_add_entities( + [AmcrestBinarySensor(name, device, sensor_type) + for sensor_type in discovery_info[CONF_BINARY_SENSORS]], + True) class AmcrestBinarySensor(BinarySensorDevice): """Binary sensor for Amcrest camera.""" - def __init__(self, name, camera, sensor_type): + def __init__(self, name, device, sensor_type): """Initialize entity.""" self._name = '{} {}'.format(name, BINARY_SENSORS[sensor_type]) - self._camera = camera + self._api = device.api self._sensor_type = sensor_type self._state = None @@ -62,7 +63,7 @@ class AmcrestBinarySensor(BinarySensorDevice): _LOGGER.debug('Pulling data from %s binary sensor', self._name) try: - self._state = self._camera.is_motion_detected + self._state = self._api.is_motion_detected except AmcrestError as error: _LOGGER.error( 'Could not update %s binary sensor due to error: %s', diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 07f5d403ba8..e646c11f2e9 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -2,18 +2,72 @@ import asyncio import logging +import voluptuous as vol + from homeassistant.components.camera import ( - Camera, SUPPORT_ON_OFF, SUPPORT_STREAM) + Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM) from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONF_NAME, STATE_ON, STATE_OFF) from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, async_get_clientsession) +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_AMCREST, STREAM_SOURCE_LIST, TIMEOUT +from .const import CAMERA_WEB_SESSION_TIMEOUT, DATA_AMCREST +from .helpers import service_signal _LOGGER = logging.getLogger(__name__) +STREAM_SOURCE_LIST = [ + 'snapshot', + 'mjpeg', + 'rtsp', +] + +_SRV_EN_REC = 'enable_recording' +_SRV_DS_REC = 'disable_recording' +_SRV_EN_AUD = 'enable_audio' +_SRV_DS_AUD = 'disable_audio' +_SRV_EN_MOT_REC = 'enable_motion_recording' +_SRV_DS_MOT_REC = 'disable_motion_recording' +_SRV_GOTO = 'goto_preset' +_SRV_CBW = 'set_color_bw' +_SRV_TOUR_ON = 'start_tour' +_SRV_TOUR_OFF = 'stop_tour' + +_ATTR_PRESET = 'preset' +_ATTR_COLOR_BW = 'color_bw' + +_CBW_COLOR = 'color' +_CBW_AUTO = 'auto' +_CBW_BW = 'bw' +_CBW = [_CBW_COLOR, _CBW_AUTO, _CBW_BW] + +_SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(_ATTR_PRESET): vol.All(vol.Coerce(int), vol.Range(min=1)), +}) +_SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend({ + vol.Required(_ATTR_COLOR_BW): vol.In(_CBW), +}) + +CAMERA_SERVICES = { + _SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, 'async_enable_recording', ()), + _SRV_DS_REC: (CAMERA_SERVICE_SCHEMA, 'async_disable_recording', ()), + _SRV_EN_AUD: (CAMERA_SERVICE_SCHEMA, 'async_enable_audio', ()), + _SRV_DS_AUD: (CAMERA_SERVICE_SCHEMA, 'async_disable_audio', ()), + _SRV_EN_MOT_REC: ( + CAMERA_SERVICE_SCHEMA, 'async_enable_motion_recording', ()), + _SRV_DS_MOT_REC: ( + CAMERA_SERVICE_SCHEMA, 'async_disable_motion_recording', ()), + _SRV_GOTO: (_SRV_GOTO_SCHEMA, 'async_goto_preset', (_ATTR_PRESET,)), + _SRV_CBW: (_SRV_CBW_SCHEMA, 'async_set_color_bw', (_ATTR_COLOR_BW,)), + _SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, 'async_start_tour', ()), + _SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, 'async_stop_tour', ()), +} + +_BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -21,28 +75,33 @@ async def async_setup_platform(hass, config, async_add_entities, if discovery_info is None: return - device_name = discovery_info[CONF_NAME] - amcrest = hass.data[DATA_AMCREST][device_name] - - async_add_entities([AmcrestCam(hass, amcrest)], True) + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST]['devices'][name] + async_add_entities([ + AmcrestCam(name, device, hass.data[DATA_FFMPEG])], True) class AmcrestCam(Camera): """An implementation of an Amcrest IP camera.""" - def __init__(self, hass, amcrest): + def __init__(self, name, device, ffmpeg): """Initialize an Amcrest camera.""" - super(AmcrestCam, self).__init__() - self._name = amcrest.name - self._camera = amcrest.device - self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = amcrest.ffmpeg_arguments - self._stream_source = amcrest.stream_source - self._resolution = amcrest.resolution - self._token = self._auth = amcrest.authentication + super().__init__() + self._name = name + self._api = device.api + self._ffmpeg = ffmpeg + self._ffmpeg_arguments = device.ffmpeg_arguments + self._stream_source = device.stream_source + self._resolution = device.resolution + self._token = self._auth = device.authentication self._is_recording = False + self._motion_detection_enabled = None self._model = None + self._audio_enabled = None + self._motion_recording_enabled = None + self._color_bw = None self._snapshot_lock = asyncio.Lock() + self._unsub_dispatcher = [] async def async_camera_image(self): """Return a still image response from the camera.""" @@ -56,7 +115,7 @@ class AmcrestCam(Camera): try: # Send the request to snap a picture and return raw jpg data response = await self.hass.async_add_executor_job( - self._camera.snapshot, self._resolution) + self._api.snapshot, self._resolution) return response.data except AmcrestError as error: _LOGGER.error( @@ -67,15 +126,16 @@ class AmcrestCam(Camera): async def handle_async_mjpeg_stream(self, request): """Return an MJPEG stream.""" # The snapshot implementation is handled by the parent class - if self._stream_source == STREAM_SOURCE_LIST['snapshot']: + if self._stream_source == 'snapshot': return await super().handle_async_mjpeg_stream(request) - if self._stream_source == STREAM_SOURCE_LIST['mjpeg']: + if self._stream_source == 'mjpeg': # stream an MJPEG image stream directly from the camera websession = async_get_clientsession(self.hass) - streaming_url = self._camera.mjpeg_url(typeno=self._resolution) + streaming_url = self._api.mjpeg_url(typeno=self._resolution) stream_coro = websession.get( - streaming_url, auth=self._token, timeout=TIMEOUT) + streaming_url, auth=self._token, + timeout=CAMERA_WEB_SESSION_TIMEOUT) return await async_aiohttp_proxy_web( self.hass, request, stream_coro) @@ -83,7 +143,7 @@ class AmcrestCam(Camera): # streaming via ffmpeg from haffmpeg.camera import CameraMjpeg - streaming_url = self._camera.rtsp_url(typeno=self._resolution) + streaming_url = self._api.rtsp_url(typeno=self._resolution) stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) await stream.open_camera( streaming_url, extra_cmd=self._ffmpeg_arguments) @@ -103,6 +163,19 @@ class AmcrestCam(Camera): """Return the name of this camera.""" return self._name + @property + def device_state_attributes(self): + """Return the Amcrest-specific camera state attributes.""" + attr = {} + if self._audio_enabled is not None: + attr['audio'] = _BOOL_TO_STATE.get(self._audio_enabled) + if self._motion_recording_enabled is not None: + attr['motion_recording'] = _BOOL_TO_STATE.get( + self._motion_recording_enabled) + if self._color_bw is not None: + attr[_ATTR_COLOR_BW] = self._color_bw + return attr + @property def supported_features(self): """Return supported features.""" @@ -120,6 +193,11 @@ class AmcrestCam(Camera): """Return the camera brand.""" return 'Amcrest' + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_detection_enabled + @property def model(self): """Return the camera model.""" @@ -128,7 +206,7 @@ class AmcrestCam(Camera): @property def stream_source(self): """Return the source of the stream.""" - return self._camera.rtsp_url(typeno=self._resolution) + return self._api.rtsp_url(typeno=self._resolution) @property def is_on(self): @@ -137,6 +215,21 @@ class AmcrestCam(Camera): # Other Entity method overrides + async def async_added_to_hass(self): + """Subscribe to signals and add camera to list.""" + for service, params in CAMERA_SERVICES.items(): + self._unsub_dispatcher.append(async_dispatcher_connect( + self.hass, + service_signal(service, self.entity_id), + getattr(self, params[1]))) + self.hass.data[DATA_AMCREST]['cameras'].append(self.entity_id) + + async def async_will_remove_from_hass(self): + """Remove camera from list and disconnect from signals.""" + self.hass.data[DATA_AMCREST]['cameras'].remove(self.entity_id) + for unsub_dispatcher in self._unsub_dispatcher: + unsub_dispatcher() + def update(self): """Update entity status.""" from amcrest import AmcrestError @@ -144,15 +237,21 @@ class AmcrestCam(Camera): _LOGGER.debug('Pulling data from %s camera', self.name) if self._model is None: try: - self._model = self._camera.device_type.split('=')[-1].strip() + self._model = self._api.device_type.split('=')[-1].strip() except AmcrestError as error: _LOGGER.error( 'Could not get %s camera model due to error: %s', self.name, error) self._model = '' try: - self.is_streaming = self._camera.video_enabled - self._is_recording = self._camera.record_mode == 'Manual' + self.is_streaming = self._api.video_enabled + self._is_recording = self._api.record_mode == 'Manual' + self._motion_detection_enabled = ( + self._api.is_motion_detector_on()) + self._audio_enabled = self._api.audio_enabled + self._motion_recording_enabled = ( + self._api.is_record_on_motion_detection()) + self._color_bw = _CBW[self._api.day_night_color] except AmcrestError as error: _LOGGER.error( 'Could not get %s camera attributes due to error: %s', @@ -168,14 +267,71 @@ class AmcrestCam(Camera): """Turn on camera.""" self._enable_video_stream(True) - # Utility methods + def enable_motion_detection(self): + """Enable motion detection in the camera.""" + self._enable_motion_detection(True) + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + self._enable_motion_detection(False) + + # Additional Amcrest Camera service methods + + async def async_enable_recording(self): + """Call the job and enable recording.""" + await self.hass.async_add_executor_job(self._enable_recording, True) + + async def async_disable_recording(self): + """Call the job and disable recording.""" + await self.hass.async_add_executor_job(self._enable_recording, False) + + async def async_enable_audio(self): + """Call the job and enable audio.""" + await self.hass.async_add_executor_job(self._enable_audio, True) + + async def async_disable_audio(self): + """Call the job and disable audio.""" + await self.hass.async_add_executor_job(self._enable_audio, False) + + async def async_enable_motion_recording(self): + """Call the job and enable motion recording.""" + await self.hass.async_add_executor_job(self._enable_motion_recording, + True) + + async def async_disable_motion_recording(self): + """Call the job and disable motion recording.""" + await self.hass.async_add_executor_job(self._enable_motion_recording, + False) + + async def async_goto_preset(self, preset): + """Call the job and move camera to preset position.""" + await self.hass.async_add_executor_job(self._goto_preset, preset) + + async def async_set_color_bw(self, color_bw): + """Call the job and set camera color mode.""" + await self.hass.async_add_executor_job(self._set_color_bw, color_bw) + + async def async_start_tour(self): + """Call the job and start camera tour.""" + await self.hass.async_add_executor_job(self._start_tour, True) + + async def async_stop_tour(self): + """Call the job and stop camera tour.""" + await self.hass.async_add_executor_job(self._start_tour, False) + + # Methods to send commands to Amcrest camera and handle errors def _enable_video_stream(self, enable): """Enable or disable camera video stream.""" from amcrest import AmcrestError + # Given the way the camera's state is determined by + # is_streaming and is_recording, we can't leave + # recording on if video stream is being turned off. + if self.is_recording and not enable: + self._enable_recording(False) try: - self._camera.video_enabled = enable + self._api.video_enabled = enable except AmcrestError as error: _LOGGER.error( 'Could not %s %s camera video stream due to error: %s', @@ -183,3 +339,103 @@ class AmcrestCam(Camera): else: self.is_streaming = enable self.schedule_update_ha_state() + + def _enable_recording(self, enable): + """Turn recording on or off.""" + from amcrest import AmcrestError + + # Given the way the camera's state is determined by + # is_streaming and is_recording, we can't leave + # video stream off if recording is being turned on. + if not self.is_streaming and enable: + self._enable_video_stream(True) + rec_mode = {'Automatic': 0, 'Manual': 1} + try: + self._api.record_mode = rec_mode[ + 'Manual' if enable else 'Automatic'] + except AmcrestError as error: + _LOGGER.error( + 'Could not %s %s camera recording due to error: %s', + 'enable' if enable else 'disable', self.name, error) + else: + self._is_recording = enable + self.schedule_update_ha_state() + + def _enable_motion_detection(self, enable): + """Enable or disable motion detection.""" + from amcrest import AmcrestError + + try: + self._api.motion_detection = str(enable).lower() + except AmcrestError as error: + _LOGGER.error( + 'Could not %s %s camera motion detection due to error: %s', + 'enable' if enable else 'disable', self.name, error) + else: + self._motion_detection_enabled = enable + self.schedule_update_ha_state() + + def _enable_audio(self, enable): + """Enable or disable audio stream.""" + from amcrest import AmcrestError + + try: + self._api.audio_enabled = enable + except AmcrestError as error: + _LOGGER.error( + 'Could not %s %s camera audio stream due to error: %s', + 'enable' if enable else 'disable', self.name, error) + else: + self._audio_enabled = enable + self.schedule_update_ha_state() + + def _enable_motion_recording(self, enable): + """Enable or disable motion recording.""" + from amcrest import AmcrestError + + try: + self._api.motion_recording = str(enable).lower() + except AmcrestError as error: + _LOGGER.error( + 'Could not %s %s camera motion recording due to error: %s', + 'enable' if enable else 'disable', self.name, error) + else: + self._motion_recording_enabled = enable + self.schedule_update_ha_state() + + def _goto_preset(self, preset): + """Move camera position and zoom to preset.""" + from amcrest import AmcrestError + + try: + self._api.go_to_preset( + action='start', preset_point_number=preset) + except AmcrestError as error: + _LOGGER.error( + 'Could not move %s camera to preset %i due to error: %s', + self.name, preset, error) + + def _set_color_bw(self, cbw): + """Set camera color mode.""" + from amcrest import AmcrestError + + try: + self._api.day_night_color = _CBW.index(cbw) + except AmcrestError as error: + _LOGGER.error( + 'Could not set %s camera color mode to %s due to error: %s', + self.name, cbw, error) + else: + self._color_bw = cbw + self.schedule_update_ha_state() + + def _start_tour(self, start): + """Start camera tour.""" + from amcrest import AmcrestError + + try: + self._api.tour(start=start) + except AmcrestError as error: + _LOGGER.error( + 'Could not %s %s camera tour due to error: %s', + 'start' if start else 'stop', self.name, error) diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py new file mode 100644 index 00000000000..a0230937e95 --- /dev/null +++ b/homeassistant/components/amcrest/const.py @@ -0,0 +1,7 @@ +"""Constants for amcrest component.""" +DOMAIN = 'amcrest' +DATA_AMCREST = DOMAIN + +BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 +CAMERA_WEB_SESSION_TIMEOUT = 10 +SENSOR_SCAN_INTERVAL_SECS = 10 diff --git a/homeassistant/components/amcrest/helpers.py b/homeassistant/components/amcrest/helpers.py new file mode 100644 index 00000000000..270c969a6cc --- /dev/null +++ b/homeassistant/components/amcrest/helpers.py @@ -0,0 +1,10 @@ +"""Helpers for amcrest component.""" +from .const import DOMAIN + + +def service_signal(service, entity_id=None): + """Encode service and entity_id into signal.""" + signal = '{}_{}'.format(DOMAIN, service) + if entity_id: + signal += '_{}'.format(entity_id.replace('.', '_')) + return signal diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 56cb021052e..4d2cd88c5ae 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -5,11 +5,19 @@ import logging from homeassistant.const import CONF_NAME, CONF_SENSORS from homeassistant.helpers.entity import Entity -from . import DATA_AMCREST, SENSORS +from .const import DATA_AMCREST, SENSOR_SCAN_INTERVAL_SECS _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) + +# Sensor types are defined like: Name, units, icon +SENSOR_MOTION_DETECTOR = 'motion_detector' +SENSORS = { + SENSOR_MOTION_DETECTOR: ['Motion Detected', None, 'mdi:run'], + 'sdcard': ['SD Used', '%', 'mdi:sd'], + 'ptz_preset': ['PTZ Preset', None, 'mdi:camera-iris'], +} async def async_setup_platform( @@ -18,30 +26,26 @@ async def async_setup_platform( if discovery_info is None: return - device_name = discovery_info[CONF_NAME] - sensors = discovery_info[CONF_SENSORS] - amcrest = hass.data[DATA_AMCREST][device_name] - - amcrest_sensors = [] - for sensor_type in sensors: - amcrest_sensors.append( - AmcrestSensor(amcrest.name, amcrest.device, sensor_type)) - - async_add_entities(amcrest_sensors, True) + name = discovery_info[CONF_NAME] + device = hass.data[DATA_AMCREST]['devices'][name] + async_add_entities( + [AmcrestSensor(name, device, sensor_type) + for sensor_type in discovery_info[CONF_SENSORS]], + True) class AmcrestSensor(Entity): """A sensor implementation for Amcrest IP camera.""" - def __init__(self, name, camera, sensor_type): + def __init__(self, name, device, sensor_type): """Initialize a sensor for Amcrest camera.""" - self._attrs = {} - self._camera = camera + self._name = '{} {}'.format(name, SENSORS[sensor_type][0]) + self._api = device.api self._sensor_type = sensor_type - self._name = '{0}_{1}'.format( - name, SENSORS.get(self._sensor_type)[0]) - self._icon = 'mdi:{}'.format(SENSORS.get(self._sensor_type)[2]) self._state = None + self._attrs = {} + self._unit_of_measurement = SENSORS[sensor_type][1] + self._icon = SENSORS[sensor_type][2] @property def name(self): @@ -66,22 +70,22 @@ class AmcrestSensor(Entity): @property def unit_of_measurement(self): """Return the units of measurement.""" - return SENSORS.get(self._sensor_type)[1] + return self._unit_of_measurement def update(self): """Get the latest data and updates the state.""" _LOGGER.debug("Pulling data from %s sensor.", self._name) if self._sensor_type == 'motion_detector': - self._state = self._camera.is_motion_detected - self._attrs['Record Mode'] = self._camera.record_mode + self._state = self._api.is_motion_detected + self._attrs['Record Mode'] = self._api.record_mode elif self._sensor_type == 'ptz_preset': - self._state = self._camera.ptz_presets_count + self._state = self._api.ptz_presets_count elif self._sensor_type == 'sdcard': - sd_used = self._camera.storage_used - sd_total = self._camera.storage_total + sd_used = self._api.storage_used + sd_total = self._api.storage_total self._attrs['Total'] = '{0} {1}'.format(*sd_total) self._attrs['Used'] = '{0} {1}'.format(*sd_used) - self._state = self._camera.storage_used_percent + self._state = self._api.storage_used_percent diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml new file mode 100644 index 00000000000..d6e7a02a4f9 --- /dev/null +++ b/homeassistant/components/amcrest/services.yaml @@ -0,0 +1,75 @@ +enable_recording: + description: Enable continuous recording to camera storage. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +disable_recording: + description: Disable continuous recording to camera storage. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +enable_audio: + description: Enable audio stream. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +disable_audio: + description: Disable audio stream. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +enable_motion_recording: + description: Enable recording a clip to camera storage when motion is detected. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +disable_motion_recording: + description: Disable recording a clip to camera storage when motion is detected. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +goto_preset: + description: Move camera to PTZ preset. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + preset: + description: Preset number, starting from 1. + example: 1 + +set_color_bw: + description: Set camera color mode. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + color_bw: + description: Color mode, one of 'auto', 'color' or 'bw'. + example: auto + +start_tour: + description: Start camera's PTZ tour function. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' + +stop_tour: + description: Stop camera's PTZ tour function. + fields: + entity_id: + description: "Name(s) of the cameras, or 'all' for all cameras." + example: 'camera.house_front' diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py index 90f750d1797..5989d4daf1e 100644 --- a/homeassistant/components/amcrest/switch.py +++ b/homeassistant/components/amcrest/switch.py @@ -1,13 +1,19 @@ """Support for toggling Amcrest IP camera settings.""" import logging -from homeassistant.const import CONF_NAME, CONF_SWITCHES, STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, CONF_SWITCHES from homeassistant.helpers.entity import ToggleEntity -from . import DATA_AMCREST, SWITCHES +from .const import DATA_AMCREST _LOGGER = logging.getLogger(__name__) +# Switch types are defined like: Name, icon +SWITCHES = { + 'motion_detection': ['Motion Detection', 'mdi:run-fast'], + 'motion_recording': ['Motion Recording', 'mdi:record-rec'] +} + async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): @@ -16,67 +22,58 @@ async def async_setup_platform( 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, name)) - - async_add_entities(all_switches, True) + device = hass.data[DATA_AMCREST]['devices'][name] + async_add_entities( + [AmcrestSwitch(name, device, setting) + for setting in discovery_info[CONF_SWITCHES]], + True) class AmcrestSwitch(ToggleEntity): """Representation of an Amcrest IP camera switch.""" - def __init__(self, setting, camera, name): + def __init__(self, name, device, setting): """Initialize the Amcrest switch.""" + self._name = '{} {}'.format(name, SWITCHES[setting][0]) + self._api = device.api self._setting = setting - self._camera = camera - self._name = '{} {}'.format(SWITCHES[setting][0], name) + self._state = False 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 + return self._state def turn_on(self, **kwargs): """Turn setting on.""" if self._setting == 'motion_detection': - self._camera.motion_detection = 'true' + self._api.motion_detection = 'true' elif self._setting == 'motion_recording': - self._camera.motion_recording = 'true' + self._api.motion_recording = 'true' def turn_off(self, **kwargs): """Turn setting off.""" if self._setting == 'motion_detection': - self._camera.motion_detection = 'false' + self._api.motion_detection = 'false' elif self._setting == 'motion_recording': - self._camera.motion_recording = 'false' + self._api.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() + detection = self._api.is_motion_detector_on() elif self._setting == 'motion_recording': - detection = self._camera.is_record_on_motion_detection() + detection = self._api.is_record_on_motion_detection() - self._state = STATE_ON if detection else STATE_OFF + self._state = detection @property def icon(self): From 5376e152867d12743a8d0fbcbe4a18f072641069 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 25 Apr 2019 10:14:16 +0200 Subject: [PATCH 121/346] Convert some test helpers to coroutines and adjust tests (#23352) * Convert some test helpers to coroutines * Fix tests --- .../components/alarm_control_panel/common.py | 50 ++-- tests/components/climate/common.py | 69 ++---- tests/components/demo/test_fan.py | 34 +-- tests/components/demo/test_light.py | 14 +- .../device_sun_light_trigger/test_init.py | 11 +- tests/components/fan/common.py | 46 ++-- .../generic_thermostat/test_climate.py | 232 +++++++----------- tests/components/group/test_light.py | 14 +- tests/components/light/common.py | 32 +-- tests/components/lock/common.py | 19 +- .../manual/test_alarm_control_panel.py | 156 ++++-------- .../mqtt/test_alarm_control_panel.py | 39 +-- tests/components/mqtt/test_climate.py | 109 ++++---- tests/components/mqtt/test_fan.py | 48 ++-- tests/components/mqtt/test_legacy_vacuum.py | 145 +++-------- tests/components/mqtt/test_light.py | 58 ++--- tests/components/mqtt/test_light_json.py | 83 +++---- tests/components/mqtt/test_lock.py | 12 +- tests/components/mqtt/test_state_vacuum.py | 87 +------ tests/components/mqtt/test_switch.py | 7 +- tests/components/switch/common.py | 16 +- tests/components/switch/test_light.py | 15 +- tests/components/template/test_fan.py | 96 +++----- tests/components/vacuum/common.py | 97 +++----- 24 files changed, 498 insertions(+), 991 deletions(-) diff --git a/tests/components/alarm_control_panel/common.py b/tests/components/alarm_control_panel/common.py index 829c05fef31..6aba3973a0d 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -9,12 +9,9 @@ from homeassistant.const import ( SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_ARM_CUSTOM_BYPASS) from homeassistant.loader import bind_hass -from homeassistant.core import callback -@callback -@bind_hass -def async_alarm_disarm(hass, code=None, entity_id=None): +async def async_alarm_disarm(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} if code: @@ -22,8 +19,8 @@ def async_alarm_disarm(hass, code=None, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_ALARM_DISARM, data)) + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_DISARM, data, blocking=True) @bind_hass @@ -38,9 +35,7 @@ def alarm_disarm(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_DISARM, data) -@callback -@bind_hass -def async_alarm_arm_home(hass, code=None, entity_id=None): +async def async_alarm_arm_home(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} if code: @@ -48,8 +43,8 @@ def async_alarm_arm_home(hass, code=None, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_ALARM_ARM_HOME, data)) + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_HOME, data, blocking=True) @bind_hass @@ -64,9 +59,7 @@ def alarm_arm_home(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_HOME, data) -@callback -@bind_hass -def async_alarm_arm_away(hass, code=None, entity_id=None): +async def async_alarm_arm_away(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} if code: @@ -74,8 +67,8 @@ def async_alarm_arm_away(hass, code=None, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data)) + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_AWAY, data, blocking=True) @bind_hass @@ -90,9 +83,7 @@ def alarm_arm_away(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) -@callback -@bind_hass -def async_alarm_arm_night(hass, code=None, entity_id=None): +async def async_alarm_arm_night(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} if code: @@ -100,8 +91,8 @@ def async_alarm_arm_night(hass, code=None, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data)) + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_NIGHT, data, blocking=True) @bind_hass @@ -116,9 +107,7 @@ def alarm_arm_night(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) -@callback -@bind_hass -def async_alarm_trigger(hass, code=None, entity_id=None): +async def async_alarm_trigger(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} if code: @@ -126,8 +115,8 @@ def async_alarm_trigger(hass, code=None, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_ALARM_TRIGGER, data)) + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_TRIGGER, data, blocking=True) @bind_hass @@ -142,9 +131,7 @@ def alarm_trigger(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_TRIGGER, data) -@callback -@bind_hass -def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): +async def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): """Send the alarm the command for disarm.""" data = {} if code: @@ -152,9 +139,8 @@ def async_alarm_arm_custom_bypass(hass, code=None, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call( - DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data)) + await hass.services.async_call( + DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS, data, blocking=True) @bind_hass diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index b5b6137a0a8..21bc4536a9b 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -12,13 +12,10 @@ from homeassistant.components.climate.const import ( SERVICE_SET_FAN_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_SWING_MODE) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE) -from homeassistant.core import callback from homeassistant.loader import bind_hass -@callback -@bind_hass -def async_set_away_mode(hass, away_mode, entity_id=None): +async def async_set_away_mode(hass, away_mode, entity_id=None): """Turn all or specified climate devices away mode on.""" data = { ATTR_AWAY_MODE: away_mode @@ -27,8 +24,8 @@ def async_set_away_mode(hass, away_mode, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_AWAY_MODE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_AWAY_MODE, data, blocking=True) @bind_hass @@ -44,9 +41,7 @@ def set_away_mode(hass, away_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_AWAY_MODE, data) -@callback -@bind_hass -def async_set_hold_mode(hass, hold_mode, entity_id=None): +async def async_set_hold_mode(hass, hold_mode, entity_id=None): """Set new hold mode.""" data = { ATTR_HOLD_MODE: hold_mode @@ -55,8 +50,8 @@ def async_set_hold_mode(hass, hold_mode, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_HOLD_MODE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_HOLD_MODE, data, blocking=True) @bind_hass @@ -72,9 +67,7 @@ def set_hold_mode(hass, hold_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HOLD_MODE, data) -@callback -@bind_hass -def async_set_aux_heat(hass, aux_heat, entity_id=None): +async def async_set_aux_heat(hass, aux_heat, entity_id=None): """Turn all or specified climate devices auxiliary heater on.""" data = { ATTR_AUX_HEAT: aux_heat @@ -83,8 +76,8 @@ def async_set_aux_heat(hass, aux_heat, entity_id=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_AUX_HEAT, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_AUX_HEAT, data, blocking=True) @bind_hass @@ -100,11 +93,9 @@ def set_aux_heat(hass, aux_heat, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_AUX_HEAT, data) -@callback -@bind_hass -def async_set_temperature(hass, temperature=None, entity_id=None, - target_temp_high=None, target_temp_low=None, - operation_mode=None): +async def async_set_temperature(hass, temperature=None, entity_id=None, + target_temp_high=None, target_temp_low=None, + operation_mode=None): """Set new target temperature.""" kwargs = { key: value for key, value in [ @@ -116,8 +107,8 @@ def async_set_temperature(hass, temperature=None, entity_id=None, ] if value is not None } _LOGGER.debug("set_temperature start data=%s", kwargs) - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_TEMPERATURE, kwargs, blocking=True) @bind_hass @@ -138,17 +129,15 @@ def set_temperature(hass, temperature=None, entity_id=None, hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, kwargs) -@callback -@bind_hass -def async_set_humidity(hass, humidity, entity_id=None): +async def async_set_humidity(hass, humidity, entity_id=None): """Set new target humidity.""" data = {ATTR_HUMIDITY: humidity} if entity_id is not None: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) @bind_hass @@ -162,17 +151,15 @@ def set_humidity(hass, humidity, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_HUMIDITY, data) -@callback -@bind_hass -def async_set_fan_mode(hass, fan, entity_id=None): +async def async_set_fan_mode(hass, fan, entity_id=None): """Set all or specified climate devices fan mode on.""" data = {ATTR_FAN_MODE: fan} if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_FAN_MODE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_MODE, data, blocking=True) @bind_hass @@ -186,17 +173,15 @@ def set_fan_mode(hass, fan, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_FAN_MODE, data) -@callback -@bind_hass -def async_set_operation_mode(hass, operation_mode, entity_id=None): +async def async_set_operation_mode(hass, operation_mode, entity_id=None): """Set new target operation mode.""" data = {ATTR_OPERATION_MODE: operation_mode} if entity_id is not None: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_OPERATION_MODE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_OPERATION_MODE, data, blocking=True) @bind_hass @@ -210,17 +195,15 @@ def set_operation_mode(hass, operation_mode, entity_id=None): hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, data) -@callback -@bind_hass -def async_set_swing_mode(hass, swing_mode, entity_id=None): +async def async_set_swing_mode(hass, swing_mode, entity_id=None): """Set new target swing mode.""" data = {ATTR_SWING_MODE: swing_mode} if entity_id is not None: data[ATTR_ENTITY_ID] = entity_id - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_SWING_MODE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_SWING_MODE, data, blocking=True) @bind_hass diff --git a/tests/components/demo/test_fan.py b/tests/components/demo/test_fan.py index 5a819b0c5da..79093f5ff02 100644 --- a/tests/components/demo/test_fan.py +++ b/tests/components/demo/test_fan.py @@ -29,12 +29,10 @@ async def test_turn_on(hass): """Test turning on the device.""" assert STATE_OFF == get_entity(hass).state - common.async_turn_on(hass, FAN_ENTITY_ID) - await hass.async_block_till_done() + await common.async_turn_on(hass, FAN_ENTITY_ID) assert STATE_OFF != get_entity(hass).state - common.async_turn_on(hass, FAN_ENTITY_ID, fan.SPEED_HIGH) - await hass.async_block_till_done() + await common.async_turn_on(hass, FAN_ENTITY_ID, fan.SPEED_HIGH) assert STATE_ON == get_entity(hass).state assert fan.SPEED_HIGH == \ get_entity(hass).attributes[fan.ATTR_SPEED] @@ -44,12 +42,10 @@ async def test_turn_off(hass): """Test turning off the device.""" assert STATE_OFF == get_entity(hass).state - common.async_turn_on(hass, FAN_ENTITY_ID) - await hass.async_block_till_done() + await common.async_turn_on(hass, FAN_ENTITY_ID) assert STATE_OFF != get_entity(hass).state - common.async_turn_off(hass, FAN_ENTITY_ID) - await hass.async_block_till_done() + await common.async_turn_off(hass, FAN_ENTITY_ID) assert STATE_OFF == get_entity(hass).state @@ -57,12 +53,10 @@ async def test_turn_off_without_entity_id(hass): """Test turning off all fans.""" assert STATE_OFF == get_entity(hass).state - common.async_turn_on(hass, FAN_ENTITY_ID) - await hass.async_block_till_done() + await common.async_turn_on(hass, FAN_ENTITY_ID) assert STATE_OFF != get_entity(hass).state - common.async_turn_off(hass) - await hass.async_block_till_done() + await common.async_turn_off(hass) assert STATE_OFF == get_entity(hass).state @@ -70,8 +64,8 @@ async def test_set_direction(hass): """Test setting the direction of the device.""" assert STATE_OFF == get_entity(hass).state - common.async_set_direction(hass, FAN_ENTITY_ID, fan.DIRECTION_REVERSE) - await hass.async_block_till_done() + await common.async_set_direction(hass, FAN_ENTITY_ID, + fan.DIRECTION_REVERSE) assert fan.DIRECTION_REVERSE == \ get_entity(hass).attributes.get('direction') @@ -80,8 +74,7 @@ async def test_set_speed(hass): """Test setting the speed of the device.""" assert STATE_OFF == get_entity(hass).state - common.async_set_speed(hass, FAN_ENTITY_ID, fan.SPEED_LOW) - await hass.async_block_till_done() + await common.async_set_speed(hass, FAN_ENTITY_ID, fan.SPEED_LOW) assert fan.SPEED_LOW == \ get_entity(hass).attributes.get('speed') @@ -90,12 +83,10 @@ async def test_oscillate(hass): """Test oscillating the fan.""" assert not get_entity(hass).attributes.get('oscillating') - common.async_oscillate(hass, FAN_ENTITY_ID, True) - await hass.async_block_till_done() + await common.async_oscillate(hass, FAN_ENTITY_ID, True) assert get_entity(hass).attributes.get('oscillating') - common.async_oscillate(hass, FAN_ENTITY_ID, False) - await hass.async_block_till_done() + await common.async_oscillate(hass, FAN_ENTITY_ID, False) assert not get_entity(hass).attributes.get('oscillating') @@ -103,6 +94,5 @@ async def test_is_on(hass): """Test is on service call.""" assert not fan.is_on(hass, FAN_ENTITY_ID) - common.async_turn_on(hass, FAN_ENTITY_ID) - await hass.async_block_till_done() + await common.async_turn_on(hass, FAN_ENTITY_ID) assert fan.is_on(hass, FAN_ENTITY_ID) diff --git a/tests/components/demo/test_light.py b/tests/components/demo/test_light.py index 8711acaa318..5013e316ea2 100644 --- a/tests/components/demo/test_light.py +++ b/tests/components/demo/test_light.py @@ -20,32 +20,30 @@ def setup_comp(hass): async def test_state_attributes(hass): """Test light state attributes.""" - common.async_turn_on( + await common.async_turn_on( hass, ENTITY_LIGHT, xy_color=(.4, .4), brightness=25) - await hass.async_block_till_done() state = hass.states.get(ENTITY_LIGHT) assert light.is_on(hass, ENTITY_LIGHT) assert (0.4, 0.4) == state.attributes.get(light.ATTR_XY_COLOR) assert 25 == state.attributes.get(light.ATTR_BRIGHTNESS) assert (255, 234, 164) == state.attributes.get(light.ATTR_RGB_COLOR) assert 'rainbow' == state.attributes.get(light.ATTR_EFFECT) - common.async_turn_on( + await common.async_turn_on( hass, ENTITY_LIGHT, rgb_color=(251, 253, 255), white_value=254) - await hass.async_block_till_done() state = hass.states.get(ENTITY_LIGHT) assert 254 == state.attributes.get(light.ATTR_WHITE_VALUE) assert (250, 252, 255) == state.attributes.get(light.ATTR_RGB_COLOR) assert (0.319, 0.326) == state.attributes.get(light.ATTR_XY_COLOR) - common.async_turn_on(hass, ENTITY_LIGHT, color_temp=400, effect='none') - await hass.async_block_till_done() + await common.async_turn_on( + hass, ENTITY_LIGHT, color_temp=400, effect='none') state = hass.states.get(ENTITY_LIGHT) assert 400 == state.attributes.get(light.ATTR_COLOR_TEMP) assert 153 == state.attributes.get(light.ATTR_MIN_MIREDS) assert 500 == state.attributes.get(light.ATTR_MAX_MIREDS) assert 'none' == state.attributes.get(light.ATTR_EFFECT) - common.async_turn_on(hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) - await hass.async_block_till_done() + await common.async_turn_on( + hass, ENTITY_LIGHT, kelvin=3000, brightness_pct=50) state = hass.states.get(ENTITY_LIGHT) assert 333 == state.attributes.get(light.ATTR_COLOR_TEMP) assert 127 == state.attributes.get(light.ATTR_BRIGHTNESS) diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 634c56ffbad..d4356ace48c 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -65,9 +65,7 @@ async def test_lights_on_when_sun_sets(hass, scanner): hass, device_sun_light_trigger.DOMAIN, { device_sun_light_trigger.DOMAIN: {}}) - common_light.async_turn_off(hass) - - await hass.async_block_till_done() + await common_light.async_turn_off(hass) test_time = test_time.replace(hour=3) with patch('homeassistant.util.dt.utcnow', return_value=test_time): @@ -79,9 +77,7 @@ async def test_lights_on_when_sun_sets(hass, scanner): async def test_lights_turn_off_when_everyone_leaves(hass, scanner): """Test lights turn off when everyone leaves the house.""" - common_light.async_turn_on(hass) - - await hass.async_block_till_done() + await common_light.async_turn_on(hass) assert await async_setup_component( hass, device_sun_light_trigger.DOMAIN, { @@ -99,8 +95,7 @@ async def test_lights_turn_on_when_coming_home_after_sun_set(hass, scanner): """Test lights turn on when coming home after sun set.""" test_time = datetime(2017, 4, 5, 3, 2, 3, tzinfo=dt_util.UTC) with patch('homeassistant.util.dt.utcnow', return_value=test_time): - common_light.async_turn_off(hass) - await hass.async_block_till_done() + await common_light.async_turn_off(hass) assert await async_setup_component( hass, device_sun_light_trigger.DOMAIN, { diff --git a/tests/components/fan/common.py b/tests/components/fan/common.py index f3873dd9fe0..4df0d5c3760 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -8,13 +8,10 @@ from homeassistant.components.fan import ( SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF) -from homeassistant.loader import bind_hass -from homeassistant.core import callback -@callback -@bind_hass -def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: +async def async_turn_on(hass, entity_id: str = None, + speed: str = None) -> None: """Turn all or specified fan on.""" data = { key: value for key, value in [ @@ -23,24 +20,20 @@ def async_turn_on(hass, entity_id: str = None, speed: str = None) -> None: ] if value is not None } - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) -@callback -@bind_hass -def async_turn_off(hass, entity_id: str = None) -> None: +async def async_turn_off(hass, entity_id: str = None) -> None: """Turn all or specified fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) -@callback -@bind_hass -def async_oscillate(hass, entity_id: str = None, - should_oscillate: bool = True) -> None: +async def async_oscillate(hass, entity_id: str = None, + should_oscillate: bool = True) -> None: """Set oscillation on all or specified fan.""" data = { key: value for key, value in [ @@ -49,13 +42,12 @@ def async_oscillate(hass, entity_id: str = None, ] if value is not None } - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_OSCILLATE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_OSCILLATE, data, blocking=True) -@callback -@bind_hass -def async_set_speed(hass, entity_id: str = None, speed: str = None) -> None: +async def async_set_speed(hass, entity_id: str = None, + speed: str = None) -> None: """Set speed for all or specified fan.""" data = { key: value for key, value in [ @@ -64,13 +56,11 @@ def async_set_speed(hass, entity_id: str = None, speed: str = None) -> None: ] if value is not None } - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_SPEED, data, blocking=True) -@callback -@bind_hass -def async_set_direction( +async def async_set_direction( hass, entity_id: str = None, direction: str = None) -> None: """Set direction for all or specified fan.""" data = { @@ -80,5 +70,5 @@ def async_set_direction( ] if value is not None } - hass.async_create_task( - hass.services.async_call(DOMAIN, SERVICE_SET_DIRECTION, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_DIRECTION, data, blocking=True) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 60d2250a13d..71472dc8443 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -4,6 +4,8 @@ import pytest from asynctest import mock import pytz +import voluptuous as vol + import homeassistant.core as ha from homeassistant.core import ( callback, DOMAIN as HASS_DOMAIN, CoreState, State) @@ -88,8 +90,7 @@ async def test_heater_input_boolean(hass, setup_comp_1): _setup_sensor(hass, 18) await hass.async_block_till_done() - common.async_set_temperature(hass, 23) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 23) assert STATE_ON == \ hass.states.get(heater_switch).state @@ -116,8 +117,7 @@ async def test_heater_switch(hass, setup_comp_1): hass.states.get(heater_switch).state _setup_sensor(hass, 18) - await hass.async_block_till_done() - common.async_set_temperature(hass, 23) + await common.async_set_temperature(hass, 23) await hass.async_block_till_done() assert STATE_ON == \ @@ -167,22 +167,19 @@ async def test_get_operation_modes(hass, setup_comp_2): async def test_set_target_temp(hass, setup_comp_2): """Test the setting of the target temperature.""" - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) state = hass.states.get(ENTITY) assert 30.0 == state.attributes.get('temperature') - common.async_set_temperature(hass, None) - await hass.async_block_till_done() + with pytest.raises(vol.Invalid): + await common.async_set_temperature(hass, None) state = hass.states.get(ENTITY) assert 30.0 == state.attributes.get('temperature') async def test_set_away_mode(hass, setup_comp_2): """Test the setting away mode.""" - common.async_set_temperature(hass, 23) - await hass.async_block_till_done() - common.async_set_away_mode(hass, True) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 23) + await common.async_set_away_mode(hass, True) state = hass.states.get(ENTITY) assert 16 == state.attributes.get('temperature') @@ -192,14 +189,11 @@ async def test_set_away_mode_and_restore_prev_temp(hass, setup_comp_2): Verify original temperature is restored. """ - common.async_set_temperature(hass, 23) - await hass.async_block_till_done() - common.async_set_away_mode(hass, True) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 23) + await common.async_set_away_mode(hass, True) state = hass.states.get(ENTITY) assert 16 == state.attributes.get('temperature') - common.async_set_away_mode(hass, False) - await hass.async_block_till_done() + await common.async_set_away_mode(hass, False) state = hass.states.get(ENTITY) assert 23 == state.attributes.get('temperature') @@ -209,16 +203,12 @@ async def test_set_away_mode_twice_and_restore_prev_temp(hass, setup_comp_2): Verify original temperature is restored. """ - common.async_set_temperature(hass, 23) - await hass.async_block_till_done() - common.async_set_away_mode(hass, True) - await hass.async_block_till_done() - common.async_set_away_mode(hass, True) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 23) + await common.async_set_away_mode(hass, True) + await common.async_set_away_mode(hass, True) state = hass.states.get(ENTITY) assert 16 == state.attributes.get('temperature') - common.async_set_away_mode(hass, False) - await hass.async_block_till_done() + await common.async_set_away_mode(hass, False) state = hass.states.get(ENTITY) assert 23 == state.attributes.get('temperature') @@ -240,8 +230,7 @@ async def test_set_target_temp_heater_on(hass, setup_comp_2): calls = _setup_switch(hass, False) _setup_sensor(hass, 25) await hass.async_block_till_done() - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) assert 1 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -254,8 +243,7 @@ async def test_set_target_temp_heater_off(hass, setup_comp_2): calls = _setup_switch(hass, True) _setup_sensor(hass, 30) await hass.async_block_till_done() - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) assert 2 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -266,8 +254,7 @@ async def test_set_target_temp_heater_off(hass, setup_comp_2): async def test_temp_change_heater_on_within_tolerance(hass, setup_comp_2): """Test if temperature change doesn't turn on within tolerance.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 29) await hass.async_block_till_done() assert 0 == len(calls) @@ -276,8 +263,7 @@ async def test_temp_change_heater_on_within_tolerance(hass, setup_comp_2): async def test_temp_change_heater_on_outside_tolerance(hass, setup_comp_2): """Test if temperature change turn heater on outside cold tolerance.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 27) await hass.async_block_till_done() assert 1 == len(calls) @@ -290,8 +276,7 @@ async def test_temp_change_heater_on_outside_tolerance(hass, setup_comp_2): async def test_temp_change_heater_off_within_tolerance(hass, setup_comp_2): """Test if temperature change doesn't turn off within tolerance.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 33) await hass.async_block_till_done() assert 0 == len(calls) @@ -300,8 +285,7 @@ async def test_temp_change_heater_off_within_tolerance(hass, setup_comp_2): async def test_temp_change_heater_off_outside_tolerance(hass, setup_comp_2): """Test if temperature change turn heater off outside hot tolerance.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 35) await hass.async_block_till_done() assert 1 == len(calls) @@ -314,10 +298,8 @@ async def test_temp_change_heater_off_outside_tolerance(hass, setup_comp_2): async def test_running_when_operating_mode_is_off(hass, setup_comp_2): """Test that the switch turns off when enabled is set False.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) + await common.async_set_operation_mode(hass, STATE_OFF) assert 1 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -328,10 +310,8 @@ async def test_running_when_operating_mode_is_off(hass, setup_comp_2): async def test_no_state_change_when_operation_mode_off(hass, setup_comp_2): """Test that the switch doesn't turn on when enabled is False.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) + await common.async_set_operation_mode(hass, STATE_OFF) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) @@ -340,8 +320,7 @@ async def test_no_state_change_when_operation_mode_off(hass, setup_comp_2): @mock.patch('logging.Logger.error') async def test_invalid_operating_mode(log_mock, hass, setup_comp_2): """Test error handling for invalid operation mode.""" - common.async_set_operation_mode(hass, 'invalid mode') - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'invalid mode') assert log_mock.call_count == 1 @@ -350,13 +329,12 @@ async def test_operating_mode_heat(hass, setup_comp_2): Switch turns on when temp below setpoint and mode changes. """ - common.async_set_operation_mode(hass, STATE_OFF) - common.async_set_temperature(hass, 30) + await common.async_set_operation_mode(hass, STATE_OFF) + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() calls = _setup_switch(hass, False) - common.async_set_operation_mode(hass, STATE_HEAT) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_HEAT) assert 1 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -402,8 +380,7 @@ async def test_set_target_temp_ac_off(hass, setup_comp_3): calls = _setup_switch(hass, True) _setup_sensor(hass, 25) await hass.async_block_till_done() - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) assert 2 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -413,12 +390,11 @@ async def test_set_target_temp_ac_off(hass, setup_comp_3): async def test_turn_away_mode_on_cooling(hass, setup_comp_3): """Test the setting away mode when cooling.""" + _setup_switch(hass, True) _setup_sensor(hass, 25) await hass.async_block_till_done() - common.async_set_temperature(hass, 19) - await hass.async_block_till_done() - common.async_set_away_mode(hass, True) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 19) + await common.async_set_away_mode(hass, True) state = hass.states.get(ENTITY) assert 30 == state.attributes.get('temperature') @@ -428,13 +404,12 @@ async def test_operating_mode_cool(hass, setup_comp_3): Switch turns on when temp below setpoint and mode changes. """ - common.async_set_operation_mode(hass, STATE_OFF) - common.async_set_temperature(hass, 25) + await common.async_set_operation_mode(hass, STATE_OFF) + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() calls = _setup_switch(hass, False) - common.async_set_operation_mode(hass, STATE_COOL) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_COOL) assert 1 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -447,8 +422,7 @@ async def test_set_target_temp_ac_on(hass, setup_comp_3): calls = _setup_switch(hass, False) _setup_sensor(hass, 30) await hass.async_block_till_done() - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) assert 1 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -459,8 +433,7 @@ async def test_set_target_temp_ac_on(hass, setup_comp_3): async def test_temp_change_ac_off_within_tolerance(hass, setup_comp_3): """Test if temperature change doesn't turn ac off within tolerance.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 29.8) await hass.async_block_till_done() assert 0 == len(calls) @@ -469,8 +442,7 @@ async def test_temp_change_ac_off_within_tolerance(hass, setup_comp_3): async def test_set_temp_change_ac_off_outside_tolerance(hass, setup_comp_3): """Test if temperature change turn ac off.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 27) await hass.async_block_till_done() assert 1 == len(calls) @@ -483,8 +455,7 @@ async def test_set_temp_change_ac_off_outside_tolerance(hass, setup_comp_3): async def test_temp_change_ac_on_within_tolerance(hass, setup_comp_3): """Test if temperature change doesn't turn ac on within tolerance.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 25.2) await hass.async_block_till_done() assert 0 == len(calls) @@ -493,8 +464,7 @@ async def test_temp_change_ac_on_within_tolerance(hass, setup_comp_3): async def test_temp_change_ac_on_outside_tolerance(hass, setup_comp_3): """Test if temperature change turn ac on.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 1 == len(calls) @@ -507,10 +477,8 @@ async def test_temp_change_ac_on_outside_tolerance(hass, setup_comp_3): async def test_running_when_operating_mode_is_off_2(hass, setup_comp_3): """Test that the switch turns off when enabled is set False.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) + await common.async_set_operation_mode(hass, STATE_OFF) assert 1 == len(calls) call = calls[0] assert HASS_DOMAIN == call.domain @@ -521,10 +489,8 @@ async def test_running_when_operating_mode_is_off_2(hass, setup_comp_3): async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3): """Test that the switch doesn't turn on when enabled is False.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) + await common.async_set_operation_mode(hass, STATE_OFF) _setup_sensor(hass, 35) await hass.async_block_till_done() assert 0 == len(calls) @@ -550,8 +516,7 @@ def setup_comp_4(hass): async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) @@ -564,8 +529,7 @@ async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 1 == len(calls) @@ -578,8 +542,7 @@ async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) @@ -592,8 +555,7 @@ async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 1 == len(calls) @@ -606,13 +568,11 @@ async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): async def test_mode_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): """Test if mode change turns ac off despite minimum cycle.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_OFF) assert 1 == len(calls) call = calls[0] assert 'homeassistant' == call.domain @@ -623,13 +583,11 @@ async def test_mode_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): """Test if mode change turns ac on despite minimum cycle.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, STATE_HEAT) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_HEAT) assert 1 == len(calls) call = calls[0] assert 'homeassistant' == call.domain @@ -657,8 +615,7 @@ def setup_comp_5(hass): async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) @@ -671,8 +628,7 @@ async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 1 == len(calls) @@ -686,8 +642,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough_2( hass, setup_comp_5): """Test if temperature change turn ac on.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) @@ -700,8 +655,7 @@ async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 1 == len(calls) @@ -715,13 +669,11 @@ async def test_mode_change_ac_trigger_off_not_long_enough_2( hass, setup_comp_5): """Test if mode change turns ac off despite minimum cycle.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_OFF) assert 1 == len(calls) call = calls[0] assert 'homeassistant' == call.domain @@ -732,13 +684,11 @@ async def test_mode_change_ac_trigger_off_not_long_enough_2( async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): """Test if mode change turns ac on despite minimum cycle.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, STATE_HEAT) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_HEAT) assert 1 == len(calls) call = calls[0] assert 'homeassistant' == call.domain @@ -766,8 +716,7 @@ async def test_temp_change_heater_trigger_off_not_long_enough( hass, setup_comp_6): """Test if temp change doesn't turn heater off because of time.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) @@ -777,8 +726,7 @@ async def test_temp_change_heater_trigger_on_not_long_enough( hass, setup_comp_6): """Test if temp change doesn't turn heater on because of time.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) @@ -791,8 +739,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 1 == len(calls) @@ -809,8 +756,7 @@ async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6): with mock.patch('homeassistant.helpers.condition.dt_util.utcnow', return_value=fake_changed): calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 1 == len(calls) @@ -824,13 +770,11 @@ async def test_mode_change_heater_trigger_off_not_long_enough( hass, setup_comp_6): """Test if mode change turns heater off despite minimum cycle.""" calls = _setup_switch(hass, True) - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) _setup_sensor(hass, 30) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_OFF) assert 1 == len(calls) call = calls[0] assert 'homeassistant' == call.domain @@ -842,13 +786,11 @@ async def test_mode_change_heater_trigger_on_not_long_enough( hass, setup_comp_6): """Test if mode change turns heater on despite minimum cycle.""" calls = _setup_switch(hass, False) - common.async_set_temperature(hass, 30) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 30) _setup_sensor(hass, 25) await hass.async_block_till_done() assert 0 == len(calls) - common.async_set_operation_mode(hass, STATE_HEAT) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_HEAT) assert 1 == len(calls) call = calls[0] assert 'homeassistant' == call.domain @@ -881,8 +823,7 @@ async def test_temp_change_ac_trigger_on_long_enough_3(hass, setup_comp_7): await hass.async_block_till_done() _setup_sensor(hass, 30) await hass.async_block_till_done() - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) _send_time_changed(hass, test_time) await hass.async_block_till_done() @@ -905,8 +846,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): await hass.async_block_till_done() _setup_sensor(hass, 20) await hass.async_block_till_done() - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) _send_time_changed(hass, test_time) await hass.async_block_till_done() @@ -952,8 +892,7 @@ async def test_temp_change_heater_trigger_on_long_enough_2(hass, setup_comp_8): await hass.async_block_till_done() _setup_sensor(hass, 20) await hass.async_block_till_done() - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) _send_time_changed(hass, test_time) await hass.async_block_till_done() @@ -977,8 +916,7 @@ async def test_temp_change_heater_trigger_off_long_enough_2( await hass.async_block_till_done() _setup_sensor(hass, 30) await hass.async_block_till_done() - common.async_set_temperature(hass, 25) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 25) test_time = datetime.datetime.now(pytz.UTC) _send_time_changed(hass, test_time) await hass.async_block_till_done() @@ -1019,8 +957,7 @@ def setup_comp_9(hass): async def test_turn_on_when_off(hass, setup_comp_9): """Test if climate.turn_on turns on a turned off device.""" - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_OFF) await hass.services.async_call('climate', SERVICE_TURN_ON) await hass.async_block_till_done() state_heat = hass.states.get(HEAT_ENTITY) @@ -1033,9 +970,8 @@ async def test_turn_on_when_off(hass, setup_comp_9): async def test_turn_on_when_on(hass, setup_comp_9): """Test if climate.turn_on does nothing to a turned on device.""" - common.async_set_operation_mode(hass, STATE_HEAT, HEAT_ENTITY) - common.async_set_operation_mode(hass, STATE_COOL, COOL_ENTITY) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_HEAT, HEAT_ENTITY) + await common.async_set_operation_mode(hass, STATE_COOL, COOL_ENTITY) await hass.services.async_call('climate', SERVICE_TURN_ON) await hass.async_block_till_done() state_heat = hass.states.get(HEAT_ENTITY) @@ -1048,9 +984,8 @@ async def test_turn_on_when_on(hass, setup_comp_9): async def test_turn_off_when_on(hass, setup_comp_9): """Test if climate.turn_off turns off a turned on device.""" - common.async_set_operation_mode(hass, STATE_HEAT, HEAT_ENTITY) - common.async_set_operation_mode(hass, STATE_COOL, COOL_ENTITY) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_HEAT, HEAT_ENTITY) + await common.async_set_operation_mode(hass, STATE_COOL, COOL_ENTITY) await hass.services.async_call('climate', SERVICE_TURN_OFF) await hass.async_block_till_done() state_heat = hass.states.get(HEAT_ENTITY) @@ -1063,8 +998,7 @@ async def test_turn_off_when_on(hass, setup_comp_9): async def test_turn_off_when_off(hass, setup_comp_9): """Test if climate.turn_off does nothing to a turned off device.""" - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_OFF) await hass.services.async_call('climate', SERVICE_TURN_OFF) await hass.async_block_till_done() state_heat = hass.states.get(HEAT_ENTITY) @@ -1096,12 +1030,10 @@ def setup_comp_10(hass): async def test_precision(hass, setup_comp_10): """Test that setting precision to tenths works as intended.""" - common.async_set_operation_mode(hass, STATE_OFF) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, STATE_OFF) await hass.services.async_call('climate', SERVICE_TURN_OFF) await hass.async_block_till_done() - common.async_set_temperature(hass, 23.27) - await hass.async_block_till_done() + await common.async_set_temperature(hass, 23.27) state = hass.states.get(ENTITY) assert 23.3 == state.attributes.get('temperature') diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 51580e503bd..7c28a72a883 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -301,30 +301,26 @@ async def test_service_calls(hass): await hass.async_block_till_done() assert hass.states.get('light.light_group').state == 'on' - common.async_toggle(hass, 'light.light_group') - await hass.async_block_till_done() + await common.async_toggle(hass, 'light.light_group') assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - common.async_turn_on(hass, 'light.light_group') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.light_group') assert hass.states.get('light.bed_light').state == 'on' assert hass.states.get('light.ceiling_lights').state == 'on' assert hass.states.get('light.kitchen_lights').state == 'on' - common.async_turn_off(hass, 'light.light_group') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.light_group') assert hass.states.get('light.bed_light').state == 'off' assert hass.states.get('light.ceiling_lights').state == 'off' assert hass.states.get('light.kitchen_lights').state == 'off' - common.async_turn_on(hass, 'light.light_group', brightness=128, - effect='Random', rgb_color=(42, 255, 255)) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.light_group', brightness=128, + effect='Random', rgb_color=(42, 255, 255)) state = hass.states.get('light.bed_light') assert state.state == 'on' diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 906e0458dba..81922e71234 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -9,7 +9,6 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.core import callback from homeassistant.loader import bind_hass @@ -25,13 +24,11 @@ def turn_on(hass, entity_id=None, transition=None, brightness=None, profile, flash, effect, color_name) -@callback -@bind_hass -def async_turn_on(hass, entity_id=None, transition=None, brightness=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): +async def async_turn_on(hass, entity_id=None, transition=None, brightness=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.""" data = { key: value for key, value in [ @@ -52,7 +49,8 @@ def async_turn_on(hass, entity_id=None, transition=None, brightness=None, ] if value is not None } - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass @@ -61,9 +59,7 @@ def turn_off(hass, entity_id=None, transition=None): hass.add_job(async_turn_off, hass, entity_id, transition) -@callback -@bind_hass -def async_turn_off(hass, entity_id=None, transition=None): +async def async_turn_off(hass, entity_id=None, transition=None): """Turn all or specified light off.""" data = { key: value for key, value in [ @@ -72,8 +68,8 @@ def async_turn_off(hass, entity_id=None, transition=None): ] if value is not None } - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) @bind_hass @@ -82,9 +78,7 @@ def toggle(hass, entity_id=None, transition=None): hass.add_job(async_toggle, hass, entity_id, transition) -@callback -@bind_hass -def async_toggle(hass, entity_id=None, transition=None): +async def async_toggle(hass, entity_id=None, transition=None): """Toggle all or specified light.""" data = { key: value for key, value in [ @@ -93,5 +87,5 @@ def async_toggle(hass, entity_id=None, transition=None): ] if value is not None } - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TOGGLE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data, blocking=True) diff --git a/tests/components/lock/common.py b/tests/components/lock/common.py index c5a71a3eb96..4a91204948e 100644 --- a/tests/components/lock/common.py +++ b/tests/components/lock/common.py @@ -6,7 +6,6 @@ components. Instead call the service directly. from homeassistant.components.lock import DOMAIN from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK, SERVICE_OPEN) -from homeassistant.core import callback from homeassistant.loader import bind_hass @@ -22,9 +21,7 @@ def lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_LOCK, data) -@callback -@bind_hass -def async_lock(hass, entity_id=None, code=None): +async def async_lock(hass, entity_id=None, code=None): """Lock all or specified locks.""" data = {} if code: @@ -32,7 +29,7 @@ def async_lock(hass, entity_id=None, code=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_LOCK, data)) + await hass.services.async_call(DOMAIN, SERVICE_LOCK, data, blocking=True) @bind_hass @@ -47,9 +44,7 @@ def unlock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_UNLOCK, data) -@callback -@bind_hass -def async_unlock(hass, entity_id=None, code=None): +async def async_unlock(hass, entity_id=None, code=None): """Lock all or specified locks.""" data = {} if code: @@ -57,7 +52,7 @@ def async_unlock(hass, entity_id=None, code=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_UNLOCK, data)) + await hass.services.async_call(DOMAIN, SERVICE_UNLOCK, data, blocking=True) @bind_hass @@ -72,9 +67,7 @@ def open_lock(hass, entity_id=None, code=None): hass.services.call(DOMAIN, SERVICE_OPEN, data) -@callback -@bind_hass -def async_open_lock(hass, entity_id=None, code=None): +async def async_open_lock(hass, entity_id=None, code=None): """Lock all or specified locks.""" data = {} if code: @@ -82,4 +75,4 @@ def async_open_lock(hass, entity_id=None, code=None): if entity_id: data[ATTR_ENTITY_ID] = entity_id - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_OPEN, data)) + await hass.services.async_call(DOMAIN, SERVICE_OPEN, data, blocking=True) diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index a6e59af64d5..f0f10720853 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -42,8 +42,7 @@ async def test_arm_home_no_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_home(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass, CODE) assert STATE_ALARM_ARMED_HOME == \ hass.states.get(entity_id).state @@ -66,8 +65,7 @@ async def test_arm_home_with_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_home(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass, CODE, entity_id) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -102,8 +100,7 @@ async def test_arm_home_with_invalid_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_home(hass, CODE + '2') - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass, CODE + '2') assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -126,8 +123,7 @@ async def test_arm_away_no_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE, entity_id) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state @@ -150,8 +146,7 @@ async def test_arm_home_with_template_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_home(hass, 'abc') - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass, 'abc') state = hass.states.get(entity_id) assert STATE_ALARM_ARMED_HOME == state.state @@ -174,8 +169,7 @@ async def test_arm_away_with_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -210,8 +204,7 @@ async def test_arm_away_with_invalid_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE + '2') - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE + '2') assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -234,8 +227,7 @@ async def test_arm_night_no_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_night(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass, CODE) assert STATE_ALARM_ARMED_NIGHT == \ hass.states.get(entity_id).state @@ -258,8 +250,7 @@ async def test_arm_night_with_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_night(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass, CODE, entity_id) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -278,8 +269,7 @@ async def test_arm_night_with_pending(hass): assert state.state == STATE_ALARM_ARMED_NIGHT # Do not go to the pending state when updating to the same state - common.async_alarm_arm_night(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass, CODE, entity_id) assert STATE_ALARM_ARMED_NIGHT == \ hass.states.get(entity_id).state @@ -302,8 +292,7 @@ async def test_arm_night_with_invalid_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_night(hass, CODE + '2') - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass, CODE + '2') assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -325,8 +314,7 @@ async def test_trigger_no_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -359,14 +347,12 @@ async def test_trigger_with_delay(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state @@ -400,8 +386,7 @@ async def test_trigger_zero_trigger_time(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass) assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -424,8 +409,7 @@ async def test_trigger_zero_trigger_time_with_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass) assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -448,8 +432,7 @@ async def test_trigger_with_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -497,14 +480,12 @@ async def test_trigger_with_unused_specific_delay(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state @@ -542,14 +523,12 @@ async def test_trigger_with_specific_delay(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state @@ -587,14 +566,12 @@ async def test_trigger_with_pending_and_delay(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert state.state == STATE_ALARM_PENDING @@ -644,14 +621,12 @@ async def test_trigger_with_pending_and_specific_delay(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert state.state == STATE_ALARM_PENDING @@ -692,8 +667,7 @@ async def test_armed_home_with_specific_pending(hass): entity_id = 'alarm_control_panel.test' - common.async_alarm_arm_home(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -723,8 +697,7 @@ async def test_armed_away_with_specific_pending(hass): entity_id = 'alarm_control_panel.test' - common.async_alarm_arm_away(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -754,8 +727,7 @@ async def test_armed_night_with_specific_pending(hass): entity_id = 'alarm_control_panel.test' - common.async_alarm_arm_night(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -787,8 +759,7 @@ async def test_trigger_with_specific_pending(hass): entity_id = 'alarm_control_panel.test' - common.async_alarm_trigger(hass) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -829,8 +800,7 @@ async def test_trigger_with_disarm_after_trigger(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_TRIGGERED == \ hass.states.get(entity_id).state @@ -865,8 +835,7 @@ async def test_trigger_with_zero_specific_trigger_time(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -892,8 +861,7 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_TRIGGERED == \ hass.states.get(entity_id).state @@ -927,8 +895,7 @@ async def test_trigger_with_specific_trigger_time(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_TRIGGERED == \ hass.states.get(entity_id).state @@ -960,14 +927,12 @@ async def test_trigger_with_no_disarm_after_trigger(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE, entity_id) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_TRIGGERED == \ hass.states.get(entity_id).state @@ -999,14 +964,12 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE, entity_id) assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_TRIGGERED == \ hass.states.get(entity_id).state @@ -1020,8 +983,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass): assert STATE_ALARM_ARMED_AWAY == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) assert STATE_ALARM_TRIGGERED == \ hass.states.get(entity_id).state @@ -1052,14 +1014,12 @@ async def test_disarm_while_pending_trigger(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state - common.async_alarm_disarm(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_disarm(hass, entity_id=entity_id) assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -1091,14 +1051,12 @@ async def test_disarm_during_trigger_with_invalid_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_trigger(hass) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state - common.async_alarm_disarm(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_disarm(hass, entity_id=entity_id) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -1131,20 +1089,17 @@ async def test_disarm_with_template_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_home(hass, 'def') - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass, 'def') state = hass.states.get(entity_id) assert STATE_ALARM_ARMED_HOME == state.state - common.async_alarm_disarm(hass, 'def') - await hass.async_block_till_done() + await common.async_alarm_disarm(hass, 'def') state = hass.states.get(entity_id) assert STATE_ALARM_ARMED_HOME == state.state - common.async_alarm_disarm(hass, 'abc') - await hass.async_block_till_done() + await common.async_alarm_disarm(hass, 'abc') state = hass.states.get(entity_id) assert STATE_ALARM_DISARMED == state.state @@ -1167,8 +1122,7 @@ async def test_arm_custom_bypass_no_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_custom_bypass(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_custom_bypass(hass, CODE) assert STATE_ALARM_ARMED_CUSTOM_BYPASS == \ hass.states.get(entity_id).state @@ -1191,8 +1145,7 @@ async def test_arm_custom_bypass_with_pending(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_custom_bypass(hass, CODE, entity_id) - await hass.async_block_till_done() + await common.async_alarm_arm_custom_bypass(hass, CODE, entity_id) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -1228,8 +1181,7 @@ async def test_arm_custom_bypass_with_invalid_code(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_custom_bypass(hass, CODE + '2') - await hass.async_block_till_done() + await common.async_alarm_arm_custom_bypass(hass, CODE + '2') assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state @@ -1250,8 +1202,7 @@ async def test_armed_custom_bypass_with_specific_pending(hass): entity_id = 'alarm_control_panel.test' - common.async_alarm_arm_custom_bypass(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_custom_bypass(hass) assert STATE_ALARM_PENDING == \ hass.states.get(entity_id).state @@ -1290,8 +1241,7 @@ async def test_arm_away_after_disabled_disarmed(hass): assert STATE_ALARM_DISARMED == \ hass.states.get(entity_id).state - common.async_alarm_arm_away(hass, CODE) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, CODE) state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state @@ -1300,8 +1250,7 @@ async def test_arm_away_after_disabled_disarmed(hass): assert STATE_ALARM_ARMED_AWAY == \ state.attributes['post_pending_state'] - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state @@ -1319,8 +1268,7 @@ async def test_arm_away_after_disabled_disarmed(hass): state = hass.states.get(entity_id) assert STATE_ALARM_ARMED_AWAY == state.state - common.async_alarm_trigger(hass, entity_id=entity_id) - await hass.async_block_till_done() + await common.async_alarm_trigger(hass, entity_id=entity_id) state = hass.states.get(entity_id) assert STATE_ALARM_PENDING == state.state diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 4514e5285aa..28348b99fde 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -92,8 +92,7 @@ async def test_arm_home_publishes_mqtt(hass, mqtt_mock): } }) - common.async_alarm_arm_home(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'ARM_HOME', 0, False) @@ -116,8 +115,7 @@ async def test_arm_home_not_publishes_mqtt_with_invalid_code_when_req( }) call_count = mqtt_mock.async_publish.call_count - common.async_alarm_arm_home(hass, 'abcd') - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass, 'abcd') assert mqtt_mock.async_publish.call_count == call_count @@ -137,8 +135,7 @@ async def test_arm_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): } }) - common.async_alarm_arm_home(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_home(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'ARM_HOME', 0, False) @@ -154,8 +151,7 @@ async def test_arm_away_publishes_mqtt(hass, mqtt_mock): } }) - common.async_alarm_arm_away(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'ARM_AWAY', 0, False) @@ -178,8 +174,7 @@ async def test_arm_away_not_publishes_mqtt_with_invalid_code_when_req( }) call_count = mqtt_mock.async_publish.call_count - common.async_alarm_arm_away(hass, 'abcd') - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass, 'abcd') assert mqtt_mock.async_publish.call_count == call_count @@ -199,8 +194,7 @@ async def test_arm_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): } }) - common.async_alarm_arm_away(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_away(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'ARM_AWAY', 0, False) @@ -216,8 +210,7 @@ async def test_arm_night_publishes_mqtt(hass, mqtt_mock): } }) - common.async_alarm_arm_night(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'ARM_NIGHT', 0, False) @@ -240,8 +233,7 @@ async def test_arm_night_not_publishes_mqtt_with_invalid_code_when_req( }) call_count = mqtt_mock.async_publish.call_count - common.async_alarm_arm_night(hass, 'abcd') - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass, 'abcd') assert mqtt_mock.async_publish.call_count == call_count @@ -261,8 +253,7 @@ async def test_arm_night_publishes_mqtt_when_code_not_req(hass, mqtt_mock): } }) - common.async_alarm_arm_night(hass) - await hass.async_block_till_done() + await common.async_alarm_arm_night(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'ARM_NIGHT', 0, False) @@ -278,8 +269,7 @@ async def test_disarm_publishes_mqtt(hass, mqtt_mock): } }) - common.async_alarm_disarm(hass) - await hass.async_block_till_done() + await common.async_alarm_disarm(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'DISARM', 0, False) @@ -301,8 +291,7 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): } }) - common.async_alarm_disarm(hass, 1234) - await hass.async_block_till_done() + await common.async_alarm_disarm(hass, 1234) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', '{\"action\":\"DISARM\",\"code\":\"1234\"}', 0, @@ -325,8 +314,7 @@ async def test_disarm_publishes_mqtt_when_code_not_req(hass, mqtt_mock): } }) - common.async_alarm_disarm(hass) - await hass.async_block_till_done() + await common.async_alarm_disarm(hass) mqtt_mock.async_publish.assert_called_once_with( 'alarm/command', 'DISARM', 0, False) @@ -349,8 +337,7 @@ async def test_disarm_not_publishes_mqtt_with_invalid_code_when_req( }) call_count = mqtt_mock.async_publish.call_count - common.async_alarm_disarm(hass, 'abcd') - await hass.async_block_till_done() + await common.async_alarm_disarm(hass, 'abcd') assert mqtt_mock.async_publish.call_count == call_count diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 11e2984cbb3..d6a49fd2002 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1,9 +1,12 @@ """The tests for the mqtt climate component.""" import copy import json +import pytest import unittest from unittest.mock import ANY +import voluptuous as vol + from homeassistant.components import mqtt from homeassistant.components.climate import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP) @@ -89,11 +92,11 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'off' assert state.state == 'off' - common.async_set_operation_mode(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + with pytest.raises(vol.Invalid) as excinfo: + await common.async_set_operation_mode(hass, None, ENTITY_CLIMATE) assert ("string value is None for dictionary value @ " "data['operation_mode']")\ - in caplog.text + in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'off' assert state.state == 'off' @@ -106,8 +109,7 @@ async def test_set_operation(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'off' assert state.state == 'off' - common.async_set_operation_mode(hass, 'cool', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'cool', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'cool' assert state.state == 'cool' @@ -125,8 +127,7 @@ async def test_set_operation_pessimistic(hass, mqtt_mock): assert state.attributes.get('operation_mode') is None assert state.state == 'unknown' - common.async_set_operation_mode(hass, 'cool', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'cool', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') is None assert state.state == 'unknown' @@ -151,8 +152,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'off' assert state.state == 'off' - common.async_set_operation_mode(hass, 'on', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'on', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'on' assert state.state == 'on' @@ -162,8 +162,7 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): ]) mqtt_mock.async_publish.reset_mock() - common.async_set_operation_mode(hass, 'off', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'off', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'off' assert state.state == 'off' @@ -180,10 +179,10 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') == 'low' - common.async_set_fan_mode(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + with pytest.raises(vol.Invalid) as excinfo: + await common.async_set_fan_mode(hass, None, ENTITY_CLIMATE) assert "string value is None for dictionary value @ data['fan_mode']"\ - in caplog.text + in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') == 'low' @@ -197,8 +196,7 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') is None - common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') is None @@ -217,8 +215,7 @@ async def test_set_fan_mode(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('fan_mode') == 'low' - common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_fan_mode(hass, 'high', ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'fan-mode-topic', 'high', 0, False) state = hass.states.get(ENTITY_CLIMATE) @@ -231,10 +228,10 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') == 'off' - common.async_set_swing_mode(hass, None, ENTITY_CLIMATE) - await hass.async_block_till_done() + with pytest.raises(vol.Invalid) as excinfo: + await common.async_set_swing_mode(hass, None, ENTITY_CLIMATE) assert "string value is None for dictionary value @ data['swing_mode']"\ - in caplog.text + in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') == 'off' @@ -248,8 +245,7 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') is None - common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') is None @@ -268,8 +264,7 @@ async def test_set_swing(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('swing_mode') == 'off' - common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_swing_mode(hass, 'on', ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'swing-mode-topic', 'on', 0, False) state = hass.states.get(ENTITY_CLIMATE) @@ -282,16 +277,14 @@ async def test_set_target_temperature(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('temperature') == 21 - common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'heat' mqtt_mock.async_publish.assert_called_once_with( 'mode-topic', 'heat', 0, False) mqtt_mock.async_publish.reset_mock() - common.async_set_temperature(hass, temperature=47, - entity_id=ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_temperature(hass, temperature=47, + entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('temperature') == 47 mqtt_mock.async_publish.assert_called_once_with( @@ -299,10 +292,9 @@ async def test_set_target_temperature(hass, mqtt_mock): # also test directly supplying the operation mode to set_temperature mqtt_mock.async_publish.reset_mock() - common.async_set_temperature(hass, temperature=21, - operation_mode='cool', - entity_id=ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_temperature(hass, temperature=21, + operation_mode='cool', + entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('operation_mode') == 'cool' assert state.attributes.get('temperature') == 21 @@ -321,11 +313,9 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('temperature') is None - common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) - await hass.async_block_till_done() - common.async_set_temperature(hass, temperature=47, - entity_id=ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_operation_mode(hass, 'heat', ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=47, + entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('temperature') is None @@ -342,10 +332,9 @@ async def test_set_target_temperature_low_high(hass, mqtt_mock): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) - common.async_set_temperature(hass, target_temp_low=20, - target_temp_high=23, - entity_id=ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_temperature(hass, target_temp_low=20, + target_temp_high=23, + entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('target_temp_low') == 20 assert state.attributes.get('target_temp_high') == 23 @@ -367,10 +356,9 @@ async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('target_temp_low') is None assert state.attributes.get('target_temp_high') is None - common.async_set_temperature(hass, target_temp_low=20, - target_temp_high=23, - entity_id=ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_temperature(hass, target_temp_low=20, + target_temp_high=23, + entity_id=ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('target_temp_low') is None assert state.attributes.get('target_temp_high') is None @@ -414,8 +402,7 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('away_mode') == 'off' - common.async_set_away_mode(hass, True, ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_away_mode(hass, True, ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('away_mode') == 'off' @@ -442,16 +429,14 @@ async def test_set_away_mode(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('away_mode') == 'off' - common.async_set_away_mode(hass, True, ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_away_mode(hass, True, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'away-mode-topic', 'AN', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('away_mode') == 'on' - common.async_set_away_mode(hass, False, ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_away_mode(hass, False, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'away-mode-topic', 'AUS', 0, False) state = hass.states.get(ENTITY_CLIMATE) @@ -467,8 +452,7 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None - common.async_set_hold_mode(hass, 'on', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_hold_mode(hass, 'on', ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None @@ -487,16 +471,14 @@ async def test_set_hold(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') is None - common.async_set_hold_mode(hass, 'on', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_hold_mode(hass, 'on', ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'hold-topic', 'on', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('hold_mode') == 'on' - common.async_set_hold_mode(hass, 'off', ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_hold_mode(hass, 'off', ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'hold-topic', 'off', 0, False) state = hass.states.get(ENTITY_CLIMATE) @@ -512,8 +494,7 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('aux_heat') == 'off' - common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('aux_heat') == 'off' @@ -536,16 +517,14 @@ async def test_set_aux(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('aux_heat') == 'off' - common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_aux_heat(hass, True, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'aux-topic', 'ON', 0, False) mqtt_mock.async_publish.reset_mock() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get('aux_heat') == 'on' - common.async_set_aux_heat(hass, False, ENTITY_CLIMATE) - await hass.async_block_till_done() + await common.async_set_aux_heat(hass, False, ENTITY_CLIMATE) mqtt_mock.async_publish.assert_called_once_with( 'aux-topic', 'OFF', 0, False) state = hass.states.get(ENTITY_CLIMATE) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 31aebecc236..5644aaa8912 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -172,8 +172,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_turn_on(hass, 'fan.test') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'fan.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'StAtE_On', 0, False) mqtt_mock.async_publish.reset_mock() @@ -181,8 +180,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_ON assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_turn_off(hass, 'fan.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'fan.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'StAtE_OfF', 0, False) mqtt_mock.async_publish.reset_mock() @@ -190,8 +188,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_oscillate(hass, 'fan.test', True) - await hass.async_block_till_done() + await common.async_oscillate(hass, 'fan.test', True) mqtt_mock.async_publish.assert_called_once_with( 'oscillation-command-topic', 'OsC_On', 0, False) mqtt_mock.async_publish.reset_mock() @@ -199,8 +196,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_oscillate(hass, 'fan.test', False) - await hass.async_block_till_done() + await common.async_oscillate(hass, 'fan.test', False) mqtt_mock.async_publish.assert_called_once_with( 'oscillation-command-topic', 'OsC_OfF', 0, False) mqtt_mock.async_publish.reset_mock() @@ -208,8 +204,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'speed_lOw', 0, False) mqtt_mock.async_publish.reset_mock() @@ -217,8 +212,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'speed_mEdium', 0, False) mqtt_mock.async_publish.reset_mock() @@ -226,8 +220,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'speed_High', 0, False) mqtt_mock.async_publish.reset_mock() @@ -235,8 +228,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'speed_OfF', 0, False) mqtt_mock.async_publish.reset_mock() @@ -265,8 +257,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_turn_on(hass, 'fan.test') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'fan.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'ON', 0, False) mqtt_mock.async_publish.reset_mock() @@ -274,8 +265,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_ON assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_turn_off(hass, 'fan.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'fan.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'OFF', 0, False) mqtt_mock.async_publish.reset_mock() @@ -283,8 +273,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_oscillate(hass, 'fan.test', True) - await hass.async_block_till_done() + await common.async_oscillate(hass, 'fan.test', True) mqtt_mock.async_publish.assert_called_once_with( 'oscillation-command-topic', 'oscillate_on', 0, False) mqtt_mock.async_publish.reset_mock() @@ -292,8 +281,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_oscillate(hass, 'fan.test', False) - await hass.async_block_till_done() + await common.async_oscillate(hass, 'fan.test', False) mqtt_mock.async_publish.assert_called_once_with( 'oscillation-command-topic', 'oscillate_off', 0, False) mqtt_mock.async_publish.reset_mock() @@ -301,8 +289,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_LOW) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'low', 0, False) mqtt_mock.async_publish.reset_mock() @@ -310,8 +297,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_MEDIUM) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'medium', 0, False) mqtt_mock.async_publish.reset_mock() @@ -319,8 +305,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_HIGH) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'high', 0, False) mqtt_mock.async_publish.reset_mock() @@ -328,8 +313,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) - await hass.async_block_till_done() + await common.async_set_speed(hass, 'fan.test', fan.SPEED_OFF) mqtt_mock.async_publish.assert_called_once_with( 'speed-command-topic', 'off', 0, False) mqtt_mock.async_publish.reset_mock() diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 5a7bf6c2d8b..8beceb7d660 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -66,82 +66,61 @@ async def test_all_commands(hass, mqtt_mock): vacuum.DOMAIN: config, }) - common.turn_on(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_turn_on(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'turn_on', 0, False) mqtt_mock.async_publish.reset_mock() - common.turn_off(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_turn_off(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'turn_off', 0, False) mqtt_mock.async_publish.reset_mock() - common.stop(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_stop(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'stop', 0, False) mqtt_mock.async_publish.reset_mock() - common.clean_spot(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_clean_spot(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'clean_spot', 0, False) mqtt_mock.async_publish.reset_mock() - common.locate(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_locate(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'locate', 0, False) mqtt_mock.async_publish.reset_mock() - common.start_pause(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_start_pause(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'start_pause', 0, False) mqtt_mock.async_publish.reset_mock() - common.return_to_base(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_return_to_base(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/command', 'return_to_base', 0, False) mqtt_mock.async_publish.reset_mock() - common.set_fan_speed(hass, 'high', 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_set_fan_speed(hass, 'high', 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/set_fan_speed', 'high', 0, False) mqtt_mock.async_publish.reset_mock() - common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', + entity_id='vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/send_command', '44 FE 93', 0, False) mqtt_mock.async_publish.reset_mock() - common.send_command(hass, '44 FE 93', {"key": "value"}, - entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { "command": "44 FE 93", "key": "value" } - common.send_command(hass, '44 FE 93', {"key": "value"}, - entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { "command": "44 FE 93", "key": "value" @@ -160,57 +139,40 @@ async def test_commands_without_supported_features(hass, mqtt_mock): vacuum.DOMAIN: config, }) - common.turn_on(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_turn_on(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.turn_off(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_turn_off(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.stop(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_stop(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.clean_spot(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_clean_spot(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.locate(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_locate(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.start_pause(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_start_pause(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.return_to_base(hass, 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_return_to_base(hass, 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.set_fan_speed(hass, 'high', 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_set_fan_speed(hass, 'high', 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', + entity_id='vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() @@ -228,7 +190,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): }) state = hass.states.get('vacuum.mqtttest') - assert STATE_OFF == state.state + assert state.state == STATE_OFF assert state.attributes.get(ATTR_BATTERY_LEVEL) is None assert state.attributes.get(ATTR_BATTERY_ICON) is None @@ -251,8 +213,6 @@ async def test_status(hass, mqtt_mock): "fan_speed": "max" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_ON assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' @@ -268,8 +228,6 @@ async def test_status(hass, mqtt_mock): }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_OFF assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-charging-60' @@ -291,8 +249,6 @@ async def test_status_battery(hass, mqtt_mock): "battery_level": 54 }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' @@ -311,8 +267,6 @@ async def test_status_cleaning(hass, mqtt_mock): "cleaning": true }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_ON @@ -331,8 +285,6 @@ async def test_status_docked(hass, mqtt_mock): "docked": true }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_OFF @@ -351,8 +303,6 @@ async def test_status_charging(hass, mqtt_mock): "charging": true }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-outline' @@ -371,8 +321,6 @@ async def test_status_fan_speed(hass, mqtt_mock): "fan_speed": "max" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.attributes.get(ATTR_FAN_SPEED) == 'max' @@ -391,7 +339,6 @@ async def test_status_error(hass, mqtt_mock): "error": "Error1" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.attributes.get(ATTR_STATUS) == 'Error: Error1' @@ -399,7 +346,6 @@ async def test_status_error(hass, mqtt_mock): "error": "" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.attributes.get(ATTR_STATUS) == 'Stopped' @@ -419,7 +365,6 @@ async def test_battery_template(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'retroroomba/battery_level', '54') - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-50' @@ -436,7 +381,6 @@ async def test_status_invalid_json(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'vacuum/state', '{"asdfasas false}') - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_OFF assert state.attributes.get(ATTR_STATUS) == "Stopped" @@ -532,21 +476,17 @@ async def test_default_availability_payload(hass, mqtt_mock): }) state = hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_custom_availability_payload(hass, mqtt_mock): @@ -563,21 +503,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): }) state = hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE != state.state + assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') - assert STATE_UNAVAILABLE == state.state + assert state.state == STATE_UNAVAILABLE async def test_discovery_removal_vacuum(hass, mqtt_mock): @@ -593,7 +529,6 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state is not None @@ -601,7 +536,6 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state is None @@ -631,7 +565,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.milk') assert state is not None @@ -665,7 +598,6 @@ async def test_discovery_update_vacuum(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state is not None @@ -686,7 +618,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('vacuum.test') assert state.attributes.get('val') == '100' @@ -704,7 +635,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('vacuum.test') assert state.attributes.get('val') is None @@ -723,7 +653,6 @@ async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('vacuum.test') assert state.attributes.get('val') is None @@ -748,8 +677,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state.attributes.get('val') == '100' @@ -757,19 +684,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state.attributes.get('val') == '75' @@ -792,8 +714,6 @@ async def test_unique_id(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 2 # all vacuums group is 1, unique id created is 1 @@ -825,7 +745,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -866,7 +785,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -877,7 +795,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 75fd92dddc0..ea2b535b0fa 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -540,8 +540,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get('white_value') == 50 assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_turn_on(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', 'on', 2, False) @@ -549,8 +548,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_ON - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', 'off', 2, False) @@ -559,13 +557,12 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF mqtt_mock.reset_mock() - common.async_turn_on(hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - common.async_turn_on(hass, 'light.test', - brightness=50, hs_color=[359, 78]) - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], - white_value=80) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + await common.async_turn_on(hass, 'light.test', + brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], + white_value=80) mqtt_mock.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 2, False), @@ -604,8 +601,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 64]) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 64]) mqtt_mock.async_publish.assert_has_calls([ mock.call('test_light_rgb/set', 'on', 0, False), @@ -635,8 +631,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', color_temp=100) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', color_temp=100) mqtt_mock.async_publish.assert_has_calls([ mock.call('test_light_color_temp/set', 'on', 0, False), @@ -801,8 +796,7 @@ async def test_on_command_first(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', brightness=50) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', brightness=50) # Should get the following MQTT messages. # test_light/set: 'ON' @@ -813,8 +807,7 @@ async def test_on_command_first(hass, mqtt_mock): ], any_order=True) mqtt_mock.async_publish.reset_mock() - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light/set', 'OFF', 0, False) @@ -834,8 +827,7 @@ async def test_on_command_last(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', brightness=50) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', brightness=50) # Should get the following MQTT messages. # test_light/bright: 50 @@ -846,8 +838,7 @@ async def test_on_command_last(hass, mqtt_mock): ], any_order=True) mqtt_mock.async_publish.reset_mock() - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light/set', 'OFF', 0, False) @@ -870,8 +861,7 @@ async def test_on_command_brightness(hass, mqtt_mock): assert state.state == STATE_OFF # Turn on w/ no brightness - should set to max - common.async_turn_on(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test') # Should get the following MQTT messages. # test_light/bright: 255 @@ -879,28 +869,24 @@ async def test_on_command_brightness(hass, mqtt_mock): 'test_light/bright', 255, 0, False) mqtt_mock.async_publish.reset_mock() - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light/set', 'OFF', 0, False) mqtt_mock.async_publish.reset_mock() # Turn on w/ brightness - common.async_turn_on(hass, 'light.test', brightness=50) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', brightness=50) mqtt_mock.async_publish.assert_called_once_with( 'test_light/bright', 50, 0, False) mqtt_mock.async_publish.reset_mock() - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') # Turn on w/ just a color to insure brightness gets # added and sent. - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0]) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls([ mock.call('test_light/rgb', '255,128,0', 0, False), @@ -922,8 +908,7 @@ async def test_on_command_rgb(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', brightness=127) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', brightness=127) # Should get the following MQTT messages. # test_light/rgb: '127,127,127' @@ -934,8 +919,7 @@ async def test_on_command_rgb(hass, mqtt_mock): ], any_order=True) mqtt_mock.async_publish.reset_mock() - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light/set', 'OFF', 0, False) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 018f706a1a0..a3958669369 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -306,8 +306,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 191 assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_turn_on(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', '{"state": "ON"}', 2, False) @@ -315,8 +314,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_ON - common.async_turn_off(hass, 'light.test') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test') mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', '{"state": "OFF"}', 2, False) @@ -325,13 +323,12 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF mqtt_mock.reset_mock() - common.async_turn_on(hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - common.async_turn_on(hass, 'light.test', - brightness=50, hs_color=[359, 78]) - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], - white_value=80) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + await common.async_turn_on(hass, 'light.test', + brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], + white_value=80) mqtt_mock.async_publish.assert_has_calls([ mock.call( @@ -383,13 +380,12 @@ async def test_sending_hs_color(hass, mqtt_mock): assert state.state == STATE_OFF mqtt_mock.reset_mock() - common.async_turn_on(hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - common.async_turn_on(hass, 'light.test', - brightness=50, hs_color=[359, 78]) - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], - white_value=80) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + await common.async_turn_on(hass, 'light.test', + brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], + white_value=80) mqtt_mock.async_publish.assert_has_calls([ mock.call( @@ -428,13 +424,12 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - common.async_turn_on(hass, 'light.test', - brightness=50, hs_color=[359, 78]) - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], - brightness=255) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + await common.async_turn_on(hass, 'light.test', + brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], + brightness=255) mqtt_mock.async_publish.assert_has_calls([ mock.call( @@ -471,13 +466,12 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - common.async_turn_on(hass, 'light.test', - brightness=50, hs_color=[359, 78]) - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], - white_value=80) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + await common.async_turn_on(hass, 'light.test', + brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], + white_value=80) mqtt_mock.async_publish.assert_has_calls([ mock.call( @@ -517,13 +511,12 @@ async def test_sending_xy_color(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_OFF - common.async_turn_on(hass, 'light.test', - brightness=50, xy_color=[0.123, 0.123]) - common.async_turn_on(hass, 'light.test', - brightness=50, hs_color=[359, 78]) - common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], - white_value=80) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', + brightness=50, xy_color=[0.123, 0.123]) + await common.async_turn_on(hass, 'light.test', + brightness=50, hs_color=[359, 78]) + await common.async_turn_on(hass, 'light.test', rgb_color=[255, 128, 0], + white_value=80) mqtt_mock.async_publish.assert_has_calls([ mock.call( @@ -565,8 +558,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): assert state.state == STATE_OFF assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 - common.async_turn_on(hass, 'light.test', flash='short') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', flash='short') mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', JsonValidator( @@ -575,8 +567,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_ON - common.async_turn_on(hass, 'light.test', flash='long') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', flash='long') mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', JsonValidator( @@ -602,8 +593,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 - common.async_turn_on(hass, 'light.test', transition=15) - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.test', transition=15) mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', JsonValidator( @@ -612,8 +602,7 @@ async def test_transition(hass, mqtt_mock): state = hass.states.get('light.test') assert state.state == STATE_ON - common.async_turn_off(hass, 'light.test', transition=30) - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.test', transition=30) mqtt_mock.async_publish.assert_called_once_with( 'test_light_rgb/set', JsonValidator( diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 6328d2b7c1a..2ab75b584d2 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -86,8 +86,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_lock(hass, 'lock.test') - await hass.async_block_till_done() + await common.async_lock(hass, 'lock.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'LOCK', 0, False) @@ -96,8 +95,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state is STATE_LOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_unlock(hass, 'lock.test') - await hass.async_block_till_done() + await common.async_unlock(hass, 'lock.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'UNLOCK', 0, False) @@ -125,8 +123,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_lock(hass, 'lock.test') - await hass.async_block_till_done() + await common.async_lock(hass, 'lock.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'LOCK', 0, False) @@ -135,8 +132,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_LOCKED assert state.attributes.get(ATTR_ASSUMED_STATE) - common.async_unlock(hass, 'lock.test') - await hass.async_block_till_done() + await common.async_unlock(hass, 'lock.test') mqtt_mock.async_publish.assert_called_once_with( 'command-topic', 'UNLOCK', 0, False) diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 0c871fdcfd0..ecd63a1dcdc 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -64,70 +64,53 @@ async def test_all_commands(hass, mqtt_mock): await hass.services.async_call( DOMAIN, SERVICE_START, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, 'start', 0, False) mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_STOP, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, 'stop', 0, False) mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_PAUSE, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, 'pause', 0, False) mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_LOCATE, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, 'locate', 0, False) mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, 'clean_spot', 0, False) mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_called_once_with( COMMAND_TOPIC, 'return_to_base', 0, False) mqtt_mock.async_publish.reset_mock() - common.set_fan_speed(hass, 'medium', 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_set_fan_speed(hass, 'medium', 'vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/set_fan_speed', 'medium', 0, False) mqtt_mock.async_publish.reset_mock() - common.send_command(hass, '44 FE 93', entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', + entity_id='vacuum.mqtttest') mqtt_mock.async_publish.assert_called_once_with( 'vacuum/send_command', '44 FE 93', 0, False) mqtt_mock.async_publish.reset_mock() - common.send_command(hass, '44 FE 93', {"key": "value"}, - entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') assert json.loads(mqtt_mock.async_publish.mock_calls[-1][1][1]) == { "command": "44 FE 93", "key": "value" @@ -148,56 +131,40 @@ async def test_commands_without_supported_features(hass, mqtt_mock): await hass.services.async_call( DOMAIN, SERVICE_START, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_PAUSE, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_STOP, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_RETURN_TO_BASE, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_LOCATE, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() await hass.services.async_call( DOMAIN, SERVICE_CLEAN_SPOT, blocking=True) - await hass.async_block_till_done() - await hass.async_block_till_done() mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.set_fan_speed(hass, 'medium', 'vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_set_fan_speed(hass, 'medium', 'vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() mqtt_mock.async_publish.reset_mock() - common.send_command(hass, '44 FE 93', {"key": "value"}, - entity_id='vacuum.mqtttest') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_send_command(hass, '44 FE 93', {"key": "value"}, + entity_id='vacuum.mqtttest') mqtt_mock.async_publish.assert_not_called() @@ -217,8 +184,6 @@ async def test_status(hass, mqtt_mock): "fan_speed": "max" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_CLEANING assert state.attributes.get(ATTR_BATTERY_LEVEL) == 54 @@ -232,8 +197,6 @@ async def test_status(hass, mqtt_mock): }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_DOCKED assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-charging-60' @@ -259,8 +222,6 @@ async def test_no_fan_vacuum(hass, mqtt_mock): "state": "cleaning" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_CLEANING assert state.attributes.get(ATTR_FAN_SPEED) is None @@ -274,8 +235,6 @@ async def test_no_fan_vacuum(hass, mqtt_mock): "fan_speed": "max" }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_CLEANING @@ -291,8 +250,6 @@ async def test_no_fan_vacuum(hass, mqtt_mock): }""" async_fire_mqtt_message(hass, 'vacuum/state', message) - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_DOCKED assert state.attributes.get(ATTR_BATTERY_ICON) == 'mdi:battery-charging-60' @@ -311,7 +268,6 @@ async def test_status_invalid_json(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'vacuum/state', '{"asdfasas false}') - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_UNKNOWN @@ -331,15 +287,11 @@ async def test_default_availability_payload(hass, mqtt_mock): assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'online') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert STATE_UNAVAILABLE != state.state async_fire_mqtt_message(hass, 'availability-topic', 'offline') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_UNAVAILABLE @@ -362,15 +314,11 @@ async def test_custom_availability_payload(hass, mqtt_mock): assert state.state == STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'good') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state != STATE_UNAVAILABLE async_fire_mqtt_message(hass, 'availability-topic', 'nogood') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.mqtttest') assert state.state == STATE_UNAVAILABLE @@ -390,7 +338,6 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state is not None @@ -398,7 +345,6 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', '') await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state is None @@ -430,7 +376,6 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.milk') assert state is not None @@ -466,7 +411,6 @@ async def test_discovery_update_vacuum(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state is not None @@ -487,7 +431,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'attr-topic', '{ "val": "100" }') - await hass.async_block_till_done() state = hass.states.get('vacuum.test') assert state.attributes.get('val') == '100' @@ -505,7 +448,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', '[ "list", "of", "things"]') - await hass.async_block_till_done() state = hass.states.get('vacuum.test') assert state.attributes.get('val') is None @@ -524,7 +466,6 @@ async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): }) async_fire_mqtt_message(hass, 'attr-topic', 'This is not JSON') - await hass.async_block_till_done() state = hass.states.get('vacuum.test') assert state.attributes.get('val') is None @@ -549,8 +490,6 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): data1) await hass.async_block_till_done() async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "100" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state.attributes.get('val') == '100' @@ -558,19 +497,14 @@ async def test_discovery_update_attr(hass, mqtt_mock, caplog): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data2) await hass.async_block_till_done() - await hass.async_block_till_done() # Verify we are no longer subscribing to the old topic async_fire_mqtt_message(hass, 'attr-topic1', '{ "val": "50" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state.attributes.get('val') == '100' # Verify we are subscribing to the new topic async_fire_mqtt_message(hass, 'attr-topic2', '{ "val": "75" }') - await hass.async_block_till_done() - await hass.async_block_till_done() state = hass.states.get('vacuum.beer') assert state.attributes.get('val') == '75' @@ -593,8 +527,6 @@ async def test_unique_id(hass, mqtt_mock): }) async_fire_mqtt_message(hass, 'test-topic', 'payload') - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(hass.states.async_entity_ids()) == 2 # all vacuums group is 1, unique id created is 1 @@ -626,7 +558,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -667,7 +598,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None @@ -678,7 +608,6 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config', data) await hass.async_block_till_done() - await hass.async_block_till_done() device = registry.async_get_device({('mqtt', 'helloworld')}, set()) assert device is not None diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index df6706b01cf..f469cc8a139 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -74,8 +74,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): assert state.state == STATE_ON assert state.attributes.get(ATTR_ASSUMED_STATE) - common.turn_on(hass, 'switch.test') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'switch.test') mock_publish.async_publish.assert_called_once_with( 'command-topic', 'beer on', 2, False) @@ -83,9 +82,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mock_publish): state = hass.states.get('switch.test') assert state.state == STATE_ON - common.turn_off(hass, 'switch.test') - await hass.async_block_till_done() - await hass.async_block_till_done() + await common.async_turn_off(hass, 'switch.test') mock_publish.async_publish.assert_called_once_with( 'command-topic', 'beer off', 2, False) diff --git a/tests/components/switch/common.py b/tests/components/switch/common.py index 8db8e425ddb..2da42c8bcc8 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -6,7 +6,6 @@ components. Instead call the service directly. from homeassistant.components.switch import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.core import callback from homeassistant.loader import bind_hass @@ -16,12 +15,11 @@ def turn_on(hass, entity_id=None): hass.add_job(async_turn_on, hass, entity_id) -@callback -@bind_hass -def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=None): """Turn all or specified switch on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call(DOMAIN, SERVICE_TURN_ON, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass @@ -30,10 +28,8 @@ def turn_off(hass, entity_id=None): hass.add_job(async_turn_off, hass, entity_id) -@callback -@bind_hass -def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=None): """Turn all or specified switch off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job( - hass.services.async_call(DOMAIN, SERVICE_TURN_OFF, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index 5e6bebb56ef..efe96efb5a8 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -37,20 +37,17 @@ async def test_light_service_calls(hass): assert hass.states.get('light.light_switch').state == 'on' - common.async_toggle(hass, 'light.light_switch') - await hass.async_block_till_done() + await common.async_toggle(hass, 'light.light_switch') assert hass.states.get('switch.decorative_lights').state == 'off' assert hass.states.get('light.light_switch').state == 'off' - common.async_turn_on(hass, 'light.light_switch') - await hass.async_block_till_done() + await common.async_turn_on(hass, 'light.light_switch') assert hass.states.get('switch.decorative_lights').state == 'on' assert hass.states.get('light.light_switch').state == 'on' - common.async_turn_off(hass, 'light.light_switch') - await hass.async_block_till_done() + await common.async_turn_off(hass, 'light.light_switch') assert hass.states.get('switch.decorative_lights').state == 'off' assert hass.states.get('light.light_switch').state == 'off' @@ -68,14 +65,12 @@ async def test_switch_service_calls(hass): assert hass.states.get('light.light_switch').state == 'on' - switch_common.async_turn_off(hass, 'switch.decorative_lights') - await hass.async_block_till_done() + await switch_common.async_turn_off(hass, 'switch.decorative_lights') assert hass.states.get('switch.decorative_lights').state == 'off' assert hass.states.get('light.light_switch').state == 'off' - switch_common.async_turn_on(hass, 'switch.decorative_lights') - await hass.async_block_till_done() + await switch_common.async_turn_on(hass, 'switch.decorative_lights') assert hass.states.get('switch.decorative_lights').state == 'on' assert hass.states.get('light.light_switch').state == 'on' diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 85e63025bbc..02eec391c4d 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -2,6 +2,8 @@ import logging import pytest +import voluptuous as vol + from homeassistant import setup from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.fan import ( @@ -279,16 +281,14 @@ async def test_on_off(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # verify assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON _verify(hass, STATE_ON, None, None, None) # Turn off fan - common.async_turn_off(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_off(hass, _TEST_FAN) # verify assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF @@ -300,8 +300,7 @@ async def test_on_with_speed(hass, calls): await _register_components(hass) # Turn on fan with high speed - common.async_turn_on(hass, _TEST_FAN, SPEED_HIGH) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN, SPEED_HIGH) # verify assert hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON @@ -314,20 +313,17 @@ async def test_set_speed(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's speed to high - common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH _verify(hass, STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to medium - common.async_set_speed(hass, _TEST_FAN, SPEED_MEDIUM) - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, SPEED_MEDIUM) # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM @@ -339,12 +335,10 @@ async def test_set_invalid_speed_from_initial_stage(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's speed to 'invalid' - common.async_set_speed(hass, _TEST_FAN, 'invalid') - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, 'invalid') # verify speed is unchanged assert hass.states.get(_SPEED_INPUT_SELECT).state == '' @@ -356,20 +350,17 @@ async def test_set_invalid_speed(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's speed to high - common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, SPEED_HIGH) # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH _verify(hass, STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to 'invalid' - common.async_set_speed(hass, _TEST_FAN, 'invalid') - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, 'invalid') # verify speed is unchanged assert hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH @@ -381,20 +372,17 @@ async def test_custom_speed_list(hass, calls): await _register_components(hass, ['1', '2', '3']) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's speed to '1' - common.async_set_speed(hass, _TEST_FAN, '1') - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, '1') # verify assert hass.states.get(_SPEED_INPUT_SELECT).state == '1' _verify(hass, STATE_ON, '1', None, None) # Set fan's speed to 'medium' which is invalid - common.async_set_speed(hass, _TEST_FAN, SPEED_MEDIUM) - await hass.async_block_till_done() + await common.async_set_speed(hass, _TEST_FAN, SPEED_MEDIUM) # verify that speed is unchanged assert hass.states.get(_SPEED_INPUT_SELECT).state == '1' @@ -406,20 +394,17 @@ async def test_set_osc(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's osc to True - common.async_oscillate(hass, _TEST_FAN, True) - await hass.async_block_till_done() + await common.async_oscillate(hass, _TEST_FAN, True) # verify assert hass.states.get(_OSC_INPUT).state == 'True' _verify(hass, STATE_ON, None, True, None) # Set fan's osc to False - common.async_oscillate(hass, _TEST_FAN, False) - await hass.async_block_till_done() + await common.async_oscillate(hass, _TEST_FAN, False) # verify assert hass.states.get(_OSC_INPUT).state == 'False' @@ -431,12 +416,11 @@ async def test_set_invalid_osc_from_initial_state(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's osc to 'invalid' - common.async_oscillate(hass, _TEST_FAN, 'invalid') - await hass.async_block_till_done() + with pytest.raises(vol.Invalid): + await common.async_oscillate(hass, _TEST_FAN, 'invalid') # verify assert hass.states.get(_OSC_INPUT).state == '' @@ -448,20 +432,18 @@ async def test_set_invalid_osc(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's osc to True - common.async_oscillate(hass, _TEST_FAN, True) - await hass.async_block_till_done() + await common.async_oscillate(hass, _TEST_FAN, True) # verify assert hass.states.get(_OSC_INPUT).state == 'True' _verify(hass, STATE_ON, None, True, None) - # Set fan's osc to False - common.async_oscillate(hass, _TEST_FAN, None) - await hass.async_block_till_done() + # Set fan's osc to None + with pytest.raises(vol.Invalid): + await common.async_oscillate(hass, _TEST_FAN, None) # verify osc is unchanged assert hass.states.get(_OSC_INPUT).state == 'True' @@ -473,12 +455,10 @@ async def test_set_direction(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's direction to forward - common.async_set_direction(hass, _TEST_FAN, DIRECTION_FORWARD) - await hass.async_block_till_done() + await common.async_set_direction(hass, _TEST_FAN, DIRECTION_FORWARD) # verify assert hass.states.get(_DIRECTION_INPUT_SELECT).state \ @@ -486,8 +466,7 @@ async def test_set_direction(hass, calls): _verify(hass, STATE_ON, None, None, DIRECTION_FORWARD) # Set fan's direction to reverse - common.async_set_direction(hass, _TEST_FAN, DIRECTION_REVERSE) - await hass.async_block_till_done() + await common.async_set_direction(hass, _TEST_FAN, DIRECTION_REVERSE) # verify assert hass.states.get(_DIRECTION_INPUT_SELECT).state \ @@ -500,12 +479,10 @@ async def test_set_invalid_direction_from_initial_stage(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's direction to 'invalid' - common.async_set_direction(hass, _TEST_FAN, 'invalid') - await hass.async_block_till_done() + await common.async_set_direction(hass, _TEST_FAN, 'invalid') # verify direction is unchanged assert hass.states.get(_DIRECTION_INPUT_SELECT).state == '' @@ -517,12 +494,10 @@ async def test_set_invalid_direction(hass, calls): await _register_components(hass) # Turn on fan - common.async_turn_on(hass, _TEST_FAN) - await hass.async_block_till_done() + await common.async_turn_on(hass, _TEST_FAN) # Set fan's direction to forward - common.async_set_direction(hass, _TEST_FAN, DIRECTION_FORWARD) - await hass.async_block_till_done() + await common.async_set_direction(hass, _TEST_FAN, DIRECTION_FORWARD) # verify assert hass.states.get(_DIRECTION_INPUT_SELECT).state == \ @@ -530,8 +505,7 @@ async def test_set_invalid_direction(hass, calls): _verify(hass, STATE_ON, None, None, DIRECTION_FORWARD) # Set fan's direction to 'invalid' - common.async_set_direction(hass, _TEST_FAN, 'invalid') - await hass.async_block_till_done() + await common.async_set_direction(hass, _TEST_FAN, 'invalid') # verify direction is unchanged assert hass.states.get(_DIRECTION_INPUT_SELECT).state == \ diff --git a/tests/components/vacuum/common.py b/tests/components/vacuum/common.py index 62a0e429c0a..7dfdd043237 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -10,7 +10,6 @@ from homeassistant.components.vacuum import ( from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON) -from homeassistant.core import callback from homeassistant.loader import bind_hass @@ -20,13 +19,11 @@ def turn_on(hass, entity_id=None): hass.add_job(async_turn_on, hass, entity_id) -@callback -@bind_hass -def async_turn_on(hass, entity_id=None): +async def async_turn_on(hass, entity_id=None): """Turn all or specified vacuum on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) @bind_hass @@ -35,13 +32,11 @@ def turn_off(hass, entity_id=None): hass.add_job(async_turn_off, hass, entity_id) -@callback -@bind_hass -def async_turn_off(hass, entity_id=None): +async def async_turn_off(hass, entity_id=None): """Turn all or specified vacuum off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) @bind_hass @@ -50,13 +45,11 @@ def toggle(hass, entity_id=None): hass.add_job(async_toggle, hass, entity_id) -@callback -@bind_hass -def async_toggle(hass, entity_id=None): +async def async_toggle(hass, entity_id=None): """Toggle all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_TOGGLE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data, blocking=True) @bind_hass @@ -65,13 +58,11 @@ def locate(hass, entity_id=None): hass.add_job(async_locate, hass, entity_id) -@callback -@bind_hass -def async_locate(hass, entity_id=None): +async def async_locate(hass, entity_id=None): """Locate all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_LOCATE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_LOCATE, data, blocking=True) @bind_hass @@ -80,13 +71,11 @@ def clean_spot(hass, entity_id=None): hass.add_job(async_clean_spot, hass, entity_id) -@callback -@bind_hass -def async_clean_spot(hass, entity_id=None): +async def async_clean_spot(hass, entity_id=None): """Tell all or specified vacuum to perform a spot clean-up.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_CLEAN_SPOT, data)) + await hass.services.async_call( + DOMAIN, SERVICE_CLEAN_SPOT, data, blocking=True) @bind_hass @@ -95,13 +84,11 @@ def return_to_base(hass, entity_id=None): hass.add_job(async_return_to_base, hass, entity_id) -@callback -@bind_hass -def async_return_to_base(hass, entity_id=None): +async def async_return_to_base(hass, entity_id=None): """Tell all or specified vacuum to return to base.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_RETURN_TO_BASE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_RETURN_TO_BASE, data, blocking=True) @bind_hass @@ -110,13 +97,11 @@ def start_pause(hass, entity_id=None): hass.add_job(async_start_pause, hass, entity_id) -@callback -@bind_hass -def async_start_pause(hass, entity_id=None): +async def async_start_pause(hass, entity_id=None): """Tell all or specified vacuum to start or pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_START_PAUSE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_START_PAUSE, data, blocking=True) @bind_hass @@ -125,13 +110,11 @@ def start(hass, entity_id=None): hass.add_job(async_start, hass, entity_id) -@callback -@bind_hass -def async_start(hass, entity_id=None): +async def async_start(hass, entity_id=None): """Tell all or specified vacuum to start or resume the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_START, data)) + await hass.services.async_call( + DOMAIN, SERVICE_START, data, blocking=True) @bind_hass @@ -140,13 +123,11 @@ def pause(hass, entity_id=None): hass.add_job(async_pause, hass, entity_id) -@callback -@bind_hass -def async_pause(hass, entity_id=None): +async def async_pause(hass, entity_id=None): """Tell all or the specified vacuum to pause the current task.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_PAUSE, data)) + await hass.services.async_call( + DOMAIN, SERVICE_PAUSE, data, blocking=True) @bind_hass @@ -155,13 +136,11 @@ def stop(hass, entity_id=None): hass.add_job(async_stop, hass, entity_id) -@callback -@bind_hass -def async_stop(hass, entity_id=None): +async def async_stop(hass, entity_id=None): """Stop all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else None - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_STOP, data)) + await hass.services.async_call( + DOMAIN, SERVICE_STOP, data, blocking=True) @bind_hass @@ -170,14 +149,12 @@ def set_fan_speed(hass, fan_speed, entity_id=None): hass.add_job(async_set_fan_speed, hass, fan_speed, entity_id) -@callback -@bind_hass -def async_set_fan_speed(hass, fan_speed, entity_id=None): +async def async_set_fan_speed(hass, fan_speed, entity_id=None): """Set fan speed for all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_FAN_SPEED] = fan_speed - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_SET_FAN_SPEED, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_SPEED, data, blocking=True) @bind_hass @@ -186,13 +163,11 @@ def send_command(hass, command, params=None, entity_id=None): hass.add_job(async_send_command, hass, command, params, entity_id) -@callback -@bind_hass -def async_send_command(hass, command, params=None, entity_id=None): +async def async_send_command(hass, command, params=None, entity_id=None): """Send command to all or specified vacuum.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} data[ATTR_COMMAND] = command if params is not None: data[ATTR_PARAMS] = params - hass.async_add_job(hass.services.async_call( - DOMAIN, SERVICE_SEND_COMMAND, data)) + await hass.services.async_call( + DOMAIN, SERVICE_SEND_COMMAND, data, blocking=True) From 3d91d76d3d87dc28958c70c25cbd7568c8c20d4c Mon Sep 17 00:00:00 2001 From: Chuang Zheng <545029543@qq.com> Date: Thu, 25 Apr 2019 20:50:28 +0800 Subject: [PATCH 122/346] async_setup_component stage_1_domains (#23375) --- homeassistant/bootstrap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index c2039161ceb..3959eb88035 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -355,7 +355,7 @@ async def _async_set_up_integrations( if stage_1_domains: await asyncio.gather(*[ async_setup_component(hass, domain, config) - for domain in logging_domains + for domain in stage_1_domains ]) # Load all integrations From 4816a24b3c990fc9563ae302ede5142f0a4134aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 25 Apr 2019 20:25:33 +0200 Subject: [PATCH 123/346] Update xiaomi library (#23391) --- homeassistant/components/xiaomi_aqara/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index a79f2960497..8620b1dc34c 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi aqara", "documentation": "https://www.home-assistant.io/components/xiaomi_aqara", "requirements": [ - "PyXiaomiGateway==0.12.2" + "PyXiaomiGateway==0.12.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 902e921b74e..c82fe3741b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -70,7 +70,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.12.2 +PyXiaomiGateway==0.12.3 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.5 From 7e8f2d72b64e2eca195bfa19d25e11d5822048f6 Mon Sep 17 00:00:00 2001 From: Jason Hu Date: Thu, 25 Apr 2019 12:58:10 -0700 Subject: [PATCH 124/346] Add error handling for migration failure (#23383) --- homeassistant/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index a7267441cdb..44008214535 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -398,8 +398,12 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: if TTS_PRE_92 in config_raw: _LOGGER.info("Migrating google tts to google_translate tts") config_raw = config_raw.replace(TTS_PRE_92, TTS_92) - with open(config_path, 'wt', encoding='utf-8') as config_file: - config_file.write(config_raw) + try: + with open(config_path, 'wt', encoding='utf-8') as config_file: + config_file.write(config_raw) + except IOError: + _LOGGER.exception("Migrating to google_translate tts failed") + pass with open(version_path, 'wt') as outp: outp.write(__version__) From 39932d132ddf4b6c02f2fc68961bf0e76245f0f1 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 25 Apr 2019 22:12:11 +0200 Subject: [PATCH 125/346] Add device classes for media player and map to google types (#23236) * Add device classes for media player and map to google types * Switch default class for media_player to media --- homeassistant/components/demo/media_player.py | 8 +++- .../components/google_assistant/const.py | 7 +++- .../components/media_player/__init__.py | 10 +++++ tests/components/google_assistant/__init__.py | 8 ++-- .../google_assistant/test_smart_home.py | 41 +++++++++++++++++++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index cb3f3b5b46a..5a97b43af86 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -51,7 +51,7 @@ class AbstractDemoPlayer(MediaPlayerDevice): # We only implement the methods that we support - def __init__(self, name): + def __init__(self, name, device_class=None): """Initialize the demo device.""" self._name = name self._player_state = STATE_PLAYING @@ -60,6 +60,7 @@ class AbstractDemoPlayer(MediaPlayerDevice): self._shuffle = False self._sound_mode_list = SOUND_MODE_LIST self._sound_mode = DEFAULT_SOUND_MODE + self._device_class = device_class @property def should_poll(self): @@ -101,6 +102,11 @@ class AbstractDemoPlayer(MediaPlayerDevice): """Return a list of available sound modes.""" return self._sound_mode_list + @property + def device_class(self): + """Return the device class of the media player.""" + return self._device_class + def turn_on(self): """Turn the media player on.""" self._player_state = STATE_PLAYING diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index b6f57546cec..0f15d10f181 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -51,6 +51,9 @@ TYPE_GARAGE = PREFIX_TYPES + 'GARAGE' TYPE_OUTLET = PREFIX_TYPES + 'OUTLET' TYPE_SENSOR = PREFIX_TYPES + 'SENSOR' TYPE_DOOR = PREFIX_TYPES + 'DOOR' +TYPE_TV = PREFIX_TYPES + 'TV' +TYPE_SPEAKER = PREFIX_TYPES + 'SPEAKER' +TYPE_MEDIA = PREFIX_TYPES + 'MEDIA' SERVICE_REQUEST_SYNC = 'request_sync' HOMEGRAPH_URL = 'https://homegraph.googleapis.com/' @@ -86,7 +89,7 @@ DOMAIN_TO_GOOGLE_TYPES = { input_boolean.DOMAIN: TYPE_SWITCH, light.DOMAIN: TYPE_LIGHT, lock.DOMAIN: TYPE_LOCK, - media_player.DOMAIN: TYPE_SWITCH, + media_player.DOMAIN: TYPE_MEDIA, scene.DOMAIN: TYPE_SCENE, script.DOMAIN: TYPE_SCENE, switch.DOMAIN: TYPE_SWITCH, @@ -104,6 +107,8 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR, + (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, + (media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER, } CHALLENGE_ACK_NEEDED = 'ackNeeded' diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 478f59d2817..b23d95ab625 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -66,6 +66,16 @@ ENTITY_IMAGE_CACHE = { SCAN_INTERVAL = timedelta(seconds=10) +DEVICE_CLASS_TV = 'tv' +DEVICE_CLASS_SPEAKER = 'speaker' + +DEVICE_CLASSES = [ + DEVICE_CLASS_TV, + DEVICE_CLASS_SPEAKER, +] + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) + # Service call validation schemas MEDIA_PLAYER_SCHEMA = vol.Schema({ ATTR_ENTITY_ID: cv.comp_entity_ids, diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index f3732c12213..76ccc79a378 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -147,7 +147,7 @@ DEMO_DEVICES = [{ 'action.devices.traits.Modes' ], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.MEDIA', 'willReportState': False }, { @@ -162,7 +162,7 @@ DEMO_DEVICES = [{ 'action.devices.traits.Modes' ], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.MEDIA', 'willReportState': False }, { @@ -171,7 +171,7 @@ DEMO_DEVICES = [{ 'name': 'Lounge room' }, 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Modes'], - 'type': 'action.devices.types.SWITCH', + 'type': 'action.devices.types.MEDIA', 'willReportState': False }, { 'id': @@ -182,7 +182,7 @@ DEMO_DEVICES = [{ 'traits': ['action.devices.traits.OnOff', 'action.devices.traits.Volume'], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.MEDIA', 'willReportState': False }, { diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index ce750b74e23..ea5291f28f7 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -16,6 +16,7 @@ from homeassistant.components.google_assistant import ( from homeassistant.components.demo.binary_sensor import DemoBinarySensor from homeassistant.components.demo.cover import DemoCover from homeassistant.components.demo.light import DemoLight +from homeassistant.components.demo.media_player import AbstractDemoPlayer from homeassistant.components.demo.switch import DemoSwitch from homeassistant.helpers import device_registry @@ -684,6 +685,46 @@ async def test_device_class_cover(hass, device_class, google_type): } +@pytest.mark.parametrize("device_class,google_type", [ + ('non_existing_class', 'action.devices.types.MEDIA'), + ('speaker', 'action.devices.types.SPEAKER'), + ('tv', 'action.devices.types.TV'), +]) +async def test_device_media_player(hass, device_class, google_type): + """Test that a binary entity syncs to the correct device type.""" + sensor = AbstractDemoPlayer( + 'Demo', + device_class=device_class + ) + sensor.hass = hass + sensor.entity_id = 'media_player.demo' + await sensor.async_update_ha_state() + + result = await sh.async_handle_message( + hass, BASIC_CONFIG, 'test-agent', + { + "requestId": REQ_ID, + "inputs": [{ + "intent": "action.devices.SYNC" + }] + }) + + assert result == { + 'requestId': REQ_ID, + 'payload': { + 'agentUserId': 'test-agent', + 'devices': [{ + 'attributes': {}, + 'id': sensor.entity_id, + 'name': {'name': sensor.name}, + 'traits': ['action.devices.traits.OnOff'], + 'type': google_type, + 'willReportState': False + }] + } + } + + async def test_query_disconnect(hass): """Test a disconnect message.""" result = await sh.async_handle_message( From d2e0c6dbc2ea38459758c2b7047bbb38bbc4252d Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 25 Apr 2019 23:21:23 +0200 Subject: [PATCH 126/346] Bump youtube-dl version to 2019.04.24 (#23398) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 9007cb5c7be..0320bc3b0b4 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.04.17" + "youtube_dl==2019.04.24" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index c82fe3741b8..a0c9397f2b3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1827,7 +1827,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.04.17 +youtube_dl==2019.04.24 # homeassistant.components.zengge zengge==0.2 From e182b9592126e08bd924d248bea3322bf36c6de3 Mon Sep 17 00:00:00 2001 From: panosmz Date: Fri, 26 Apr 2019 00:35:30 +0300 Subject: [PATCH 127/346] add key parameter (#23381) --- homeassistant/components/oasa_telematics/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 60c2f9a231b..374e22d77dd 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -187,5 +187,5 @@ class OASATelematicsData(): return # Sort the data by time - sort = sorted(self.info, itemgetter(ATTR_NEXT_ARRIVAL)) + sort = sorted(self.info, key=itemgetter(ATTR_NEXT_ARRIVAL)) self.info = sort From cef7ce11ad0e5016d85b2fdbc5058f99874f23f9 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Fri, 26 Apr 2019 00:12:36 +0200 Subject: [PATCH 128/346] check if sabotage attr is in device (#23397) --- homeassistant/components/homematicip_cloud/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 1396493a527..dee43e3f367 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -78,7 +78,7 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self): """Return true if the shutter contact is on/open.""" - if self._device.sabotage: + if hasattr(self._device, 'sabotage') and self._device.sabotage: return True if self._device.windowState is None: return None @@ -96,7 +96,7 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self): """Return true if motion is detected.""" - if self._device.sabotage: + if hasattr(self._device, 'sabotage') and self._device.sabotage: return True return self._device.motionDetected From 9d67c9feb6d1e23e4150ae15fa0b8073073cbc22 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Fri, 26 Apr 2019 00:13:07 +0200 Subject: [PATCH 129/346] Add Types to Homematic IP (#23401) --- .../components/homematicip_cloud/__init__.py | 7 +- .../homematicip_cloud/alarm_control_panel.py | 10 ++- .../homematicip_cloud/binary_sensor.py | 59 +++++++++------- .../components/homematicip_cloud/climate.py | 28 ++++---- .../homematicip_cloud/config_flow.py | 6 +- .../components/homematicip_cloud/cover.py | 10 ++- .../components/homematicip_cloud/device.py | 15 ++-- .../components/homematicip_cloud/hap.py | 10 +-- .../components/homematicip_cloud/light.py | 39 ++++++----- .../components/homematicip_cloud/sensor.py | 70 ++++++++++--------- .../components/homematicip_cloud/switch.py | 26 ++++--- .../components/homematicip_cloud/weather.py | 24 ++++--- 12 files changed, 174 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 4a24120be95..550ba43950b 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -4,9 +4,12 @@ import logging import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .config_flow import configured_haps from .const import ( @@ -26,7 +29,7 @@ CONFIG_SCHEMA = vol.Schema({ }, extra=vol.ALLOW_EXTRA) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" hass.data[DOMAIN] = {} @@ -46,7 +49,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" hap = HomematicipHAP(hass, entry) hapid = entry.data[HMIPC_HAPID].replace('-', '').upper() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 1326e46d7d3..c2ad23700f3 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -2,12 +2,15 @@ import logging from homematicip.aio.group import AsyncSecurityZoneGroup +from homematicip.aio.home import AsyncHome from homematicip.base.enums import WindowState from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -20,7 +23,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -35,14 +39,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel): """Representation of an HomematicIP Cloud security zone group.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the security zone group.""" device.modelType = 'Group-SecurityZone' device.windowState = None super().__init__(home, device) @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self._device.active: if (self._device.sabotage or self._device.motionDetected or diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index dee43e3f367..19d35c47cdb 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -7,9 +7,12 @@ from homematicip.aio.device import ( AsyncShutterContact, AsyncSmokeDetector, AsyncWaterSensor, AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup +from homematicip.aio.home import AsyncHome from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE @@ -32,7 +35,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -71,12 +75,12 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud shutter contact.""" @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'door' @property - def is_on(self): + def is_on(self) -> bool: """Return true if the shutter contact is on/open.""" if hasattr(self._device, 'sabotage') and self._device.sabotage: return True @@ -89,12 +93,12 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud motion detector.""" @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'motion' @property - def is_on(self): + def is_on(self) -> bool: """Return true if motion is detected.""" if hasattr(self._device, 'sabotage') and self._device.sabotage: return True @@ -105,12 +109,12 @@ class HomematicipSmokeDetector(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud smoke detector.""" @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'smoke' @property - def is_on(self): + def is_on(self) -> bool: """Return true if smoke is detected.""" return (self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF) @@ -120,12 +124,12 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud water detector.""" @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'moisture' @property - def is_on(self): + def is_on(self) -> bool: """Return true, if moisture or waterlevel is detected.""" return self._device.moistureDetected or self._device.waterlevelDetected @@ -133,17 +137,17 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud storm sensor.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize storm sensor.""" super().__init__(home, device, "Storm") @property - def icon(self): + def icon(self) -> str: """Return the icon.""" return 'mdi:weather-windy' if self.is_on else 'mdi:pinwheel-outline' @property - def is_on(self): + def is_on(self) -> bool: """Return true, if storm is detected.""" return self._device.storm @@ -151,17 +155,17 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud rain sensor.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize rain sensor.""" super().__init__(home, device, "Raining") @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'moisture' @property - def is_on(self): + def is_on(self) -> bool: """Return true, if it is raining.""" return self._device.raining @@ -169,17 +173,17 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud sunshine sensor.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize sunshine sensor.""" super().__init__(home, device, 'Sunshine') @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'light' @property - def is_on(self): + def is_on(self) -> bool: """Return true if sun is shining.""" return self._device.sunshine @@ -197,17 +201,17 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud low battery sensor.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize battery sensor.""" super().__init__(home, device, 'Battery') @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'battery' @property - def is_on(self): + def is_on(self) -> bool: """Return true if battery is low.""" return self._device.lowBat @@ -216,18 +220,19 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" - def __init__(self, home, device, post='SecurityZone'): + def __init__(self, home: AsyncHome, device, + post: str = 'SecurityZone') -> None: """Initialize security zone group.""" device.modelType = 'HmIP-{}'.format(post) super().__init__(home, device, post) @property - def device_class(self): + def device_class(self) -> str: """Return the class of this sensor.""" return 'safety' @property - def available(self): + def available(self) -> bool: """Security-Group available.""" # A security-group must be available, and should not be affected by # the individual availability of group members. @@ -251,7 +256,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, return attr @property - def is_on(self): + def is_on(self) -> bool: """Return true if security issue detected.""" if self._device.motionDetected or \ self._device.presenceDetected or \ @@ -269,7 +274,7 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, BinarySensorDevice): """Representation of a HomematicIP security group.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize security group.""" super().__init__(home, device, 'Sensors') @@ -294,7 +299,7 @@ class HomematicipSecuritySensorGroup(HomematicipSecurityZoneSensorGroup, return attr @property - def is_on(self): + def is_on(self) -> bool: """Return true if safety issue detected.""" parent_is_on = super().is_on if parent_is_on or \ diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 8a2ad8738df..3170fc149d5 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -1,12 +1,15 @@ """Support for HomematicIP Cloud climate devices.""" import logging -from homematicip.group import HeatingGroup +from homematicip.aio.group import AsyncHeatingGroup +from homematicip.aio.home import AsyncHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( STATE_AUTO, STATE_MANUAL, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -26,12 +29,13 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP climate from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.groups: - if isinstance(device, HeatingGroup): + if isinstance(device, AsyncHeatingGroup): devices.append(HomematicipHeatingGroup(home, device)) if devices: @@ -41,48 +45,48 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize heating group.""" device.modelType = 'Group-Heating' super().__init__(home, device) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_TARGET_TEMPERATURE @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" return self._device.setPointTemperature @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._device.actualTemperature @property - def current_humidity(self): + def current_humidity(self) -> int: """Return the current humidity.""" return self._device.humidity @property - def current_operation(self): + def current_operation(self) -> str: """Return current operation ie. automatic or manual.""" return HMIP_STATE_TO_HA.get(self._device.controlMode) @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._device.minTemperature @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._device.maxTemperature diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index 458186bcce1..696425df5b5 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -1,8 +1,10 @@ """Config flow to configure the HomematicIP Cloud component.""" +from typing import Set + import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from .const import ( _LOGGER, DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, @@ -11,7 +13,7 @@ from .hap import HomematicipAuth @callback -def configured_haps(hass): +def configured_haps(hass: HomeAssistant) -> Set[str]: """Return a set of the configured access points.""" return set(entry.data[HMIPC_HAPID] for entry in hass.config_entries.async_entries(HMIPC_DOMAIN)) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 381bcf1980e..fc75d78119d 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,9 +1,12 @@ """Support for HomematicIP Cloud cover devices.""" import logging +from typing import Optional from homematicip.aio.device import AsyncFullFlushShutter from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -19,7 +22,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP cover from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -35,7 +39,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): """Representation of a HomematicIP Cloud cover device.""" @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return current position of cover.""" return int((1 - self._device.shutterLevel) * 100) @@ -47,7 +51,7 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): await self._device.set_shutter_level(level) @property - def is_closed(self): + def is_closed(self) -> Optional[bool]: """Return if the cover is closed.""" if self._device.shutterLevel is not None: return self._device.shutterLevel == HMIP_COVER_CLOSED diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index f6da8b27cf7..6bbbb8b4fab 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -1,7 +1,9 @@ """Generic device for the HomematicIP Cloud component.""" import logging +from typing import Optional from homematicip.aio.device import AsyncDevice +from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud from homeassistant.helpers.entity import Entity @@ -21,7 +23,8 @@ ATTR_GROUP_MEMBER_UNREACHABLE = 'group_member_unreachable' class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home, device, post=None): + def __init__(self, home: AsyncHome, device, + post: Optional[str] = None) -> None: """Initialize the generic device.""" self._home = home self._device = device @@ -56,7 +59,7 @@ class HomematicipGenericDevice(Entity): self.async_schedule_update_ha_state() @property - def name(self): + def name(self) -> str: """Return the name of the generic device.""" name = self._device.label if self._home.name is not None and self._home.name != '': @@ -66,22 +69,22 @@ class HomematicipGenericDevice(Entity): return name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed.""" return False @property - def available(self): + def available(self) -> bool: """Device available.""" return not self._device.unreach @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return "{}_{}".format(self.__class__.__name__, self._device.id) @property - def icon(self): + def icon(self) -> Optional[str]: """Return the icon.""" if hasattr(self._device, 'lowBat') and self._device.lowBat: return 'mdi:battery-outline' diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 99e98b5a1d2..b3731bc9f1a 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -6,7 +6,8 @@ from homematicip.aio.auth import AsyncAuth from homematicip.aio.home import AsyncHome from homematicip.base.base_connection import HmipConnectionError -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -70,7 +71,7 @@ class HomematicipAuth: class HomematicipHAP: """Manages HomematicIP HTTP and WebSocket connection.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry @@ -81,7 +82,7 @@ class HomematicipHAP: self._tries = 0 self._accesspoint_connected = True - async def async_setup(self, tries=0): + async def async_setup(self, tries: int = 0): """Initialize connection.""" try: self.home = await self.get_hap( @@ -196,7 +197,8 @@ class HomematicipHAP: self.config_entry, component) return True - async def get_hap(self, hass, hapid, authtoken, name) -> AsyncHome: + async def get_hap(self, hass: HomeAssistant, hapid: str, authtoken: str, + name: str) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index f4f73104f7c..7cfbae95a33 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -2,14 +2,18 @@ import logging from homematicip.aio.device import ( - AsyncBrandSwitchMeasuring, AsyncDimmer, AsyncPluggableDimmer, - AsyncBrandDimmer, AsyncFullFlushDimmer, - AsyncBrandSwitchNotificationLight) + AsyncBrandDimmer, AsyncBrandSwitchMeasuring, + AsyncBrandSwitchNotificationLight, AsyncDimmer, AsyncFullFlushDimmer, + AsyncPluggableDimmer) +from homematicip.aio.home import AsyncHome from homematicip.base.enums import RGBColorState +from homematicip.base.functionalChannels import NotificationLightChannel from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_NAME, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -25,7 +29,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -50,12 +55,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomematicipLight(HomematicipGenericDevice, Light): """Representation of a HomematicIP Cloud light device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the light device.""" super().__init__(home, device) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device.on @@ -85,25 +90,25 @@ class HomematicipLightMeasuring(HomematicipLight): class HomematicipDimmer(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the dimmer light device.""" super().__init__(home, device) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device.dimLevel is not None and \ self._device.dimLevel > 0.0 @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" if self._device.dimLevel: return int(self._device.dimLevel*255) return 0 @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS @@ -122,7 +127,7 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home, device, channel): + def __init__(self, home: AsyncHome, device, channel: int) -> None: """Initialize the dimmer light device.""" self.channel = channel if self.channel == 2: @@ -141,24 +146,24 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): } @property - def _func_channel(self): + def _func_channel(self) -> NotificationLightChannel: return self._device.functionalChannels[self.channel] @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._func_channel.dimLevel is not None and \ self._func_channel.dimLevel > 0.0 @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" if self._func_channel.dimLevel: return int(self._func_channel.dimLevel * 255) return 0 @property - def hs_color(self): + def hs_color(self) -> tuple: """Return the hue and saturation color value [float, float].""" simple_rgb_color = self._func_channel.simpleRGBColorState return self._color_switcher.get(simple_rgb_color, [0.0, 0.0]) @@ -172,12 +177,12 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): return attr @property - def name(self): + def name(self) -> str: """Return the name of the generic device.""" return "{} {}".format(super().name, 'Notification') @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 4816eacd08f..3d91b25c2bd 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -10,11 +10,14 @@ from homematicip.aio.device import ( AsyncTemperatureHumiditySensorOutdoor, AsyncTemperatureHumiditySensorWithoutDisplay, AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) +from homematicip.aio.home import AsyncHome from homematicip.base.enums import ValveState +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, POWER_WATT, TEMP_CELSIUS) +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -31,7 +34,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [HomematicipAccesspointStatus(home)] @@ -74,7 +78,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP Cloud access point.""" - def __init__(self, home): + def __init__(self, home: AsyncHome) -> None: """Initialize access point device.""" super().__init__(home, home) @@ -90,22 +94,22 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): } @property - def icon(self): + def icon(self) -> str: """Return the icon of the access point device.""" return 'mdi:access-point-network' @property - def state(self): + def state(self) -> float: """Return the state of the access point.""" return self._home.dutyCycle @property - def available(self): + def available(self) -> bool: """Device available.""" return self._home.connected @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return '%' @@ -113,12 +117,12 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): class HomematicipHeatingThermostat(HomematicipGenericDevice): """Represenation of a HomematicIP heating thermostat device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize heating thermostat device.""" super().__init__(home, device, 'Heating') @property - def icon(self): + def icon(self) -> str: """Return the icon.""" if super().icon: return super().icon @@ -127,14 +131,14 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): return 'mdi:radiator' @property - def state(self): + def state(self) -> int: """Return the state of the radiator valve.""" if self._device.valveState != ValveState.ADAPTION_DONE: return self._device.valveState return round(self._device.valvePosition*100) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return '%' @@ -142,22 +146,22 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipHumiditySensor(HomematicipGenericDevice): """Represenation of a HomematicIP Cloud humidity device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the thermometer device.""" super().__init__(home, device, 'Humidity') @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_HUMIDITY @property - def state(self): + def state(self) -> int: """Return the state.""" return self._device.humidity @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return '%' @@ -165,17 +169,17 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): class HomematicipTemperatureSensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud thermometer device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the thermometer device.""" super().__init__(home, device, 'Temperature') @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_TEMPERATURE @property - def state(self): + def state(self) -> float: """Return the state.""" if hasattr(self._device, 'valveActualTemperature'): return self._device.valveActualTemperature @@ -183,7 +187,7 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): return self._device.actualTemperature @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return TEMP_CELSIUS @@ -200,17 +204,17 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): class HomematicipIlluminanceSensor(HomematicipGenericDevice): """Represenation of a HomematicIP Illuminance device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" super().__init__(home, device, 'Illuminance') @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_ILLUMINANCE @property - def state(self): + def state(self) -> float: """Return the state.""" if hasattr(self._device, 'averageIllumination'): return self._device.averageIllumination @@ -218,7 +222,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): return self._device.illumination @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return 'lx' @@ -226,22 +230,22 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): class HomematicipPowerSensor(HomematicipGenericDevice): """Represenation of a HomematicIP power measuring device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" super().__init__(home, device, 'Power') @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_POWER @property - def state(self): + def state(self) -> float: """Represenation of the HomematicIP power comsumption value.""" return self._device.currentPowerConsumption @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return POWER_WATT @@ -249,17 +253,17 @@ class HomematicipPowerSensor(HomematicipGenericDevice): class HomematicipWindspeedSensor(HomematicipGenericDevice): """Represenation of a HomematicIP wind speed sensor.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" super().__init__(home, device, 'Windspeed') @property - def state(self): + def state(self) -> float: """Represenation of the HomematicIP wind speed value.""" return self._device.windSpeed @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return 'km/h' @@ -281,22 +285,22 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): class HomematicipTodayRainSensor(HomematicipGenericDevice): """Represenation of a HomematicIP rain counter of a day sensor.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" super().__init__(home, device, 'Today Rain') @property - def state(self): + def state(self) -> float: """Represenation of the HomematicIP todays rain value.""" return round(self._device.todayRainCounter, 2) @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit this state is expressed in.""" return 'mm' -def _get_wind_direction(wind_direction_degree) -> str: +def _get_wind_direction(wind_direction_degree: float) -> str: """Convert wind direction degree to named direction.""" if 11.25 <= wind_direction_degree < 33.75: return 'NNE' diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 9a0d48ac253..7b87f6c740e 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -6,8 +6,11 @@ from homematicip.aio.device import ( AsyncOpenCollector8Module, AsyncPlugableSwitch, AsyncPlugableSwitchMeasuring) from homematicip.aio.group import AsyncSwitchingGroup +from homematicip.aio.home import AsyncHome from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE @@ -21,7 +24,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP switch from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -55,12 +59,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP Cloud switch device.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the switch device.""" super().__init__(home, device) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device.on @@ -76,18 +80,18 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP switching group.""" - def __init__(self, home, device, post='Group'): + def __init__(self, home: AsyncHome, device, post: str = 'Group') -> None: """Initialize switching group.""" device.modelType = 'HmIP-{}'.format(post) super().__init__(home, device, post) @property - def is_on(self): + def is_on(self) -> bool: """Return true if group is on.""" return self._device.on @property - def available(self): + def available(self) -> bool: """Switch-Group available.""" # A switch-group must be available, and should not be affected by the # individual availability of group members. @@ -116,12 +120,12 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): """Representation of a HomematicIP measuring switch device.""" @property - def current_power_w(self): + def current_power_w(self) -> float: """Return the current power usage in W.""" return self._device.currentPowerConsumption @property - def today_energy_kwh(self): + def today_energy_kwh(self) -> int: """Return the today total energy usage in kWh.""" if self._device.energyCounter is None: return 0 @@ -131,19 +135,19 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, home, device, channel): + def __init__(self, home: AsyncHome, device, channel: int): """Initialize the multi switch device.""" self.channel = channel super().__init__(home, device, 'Channel{}'.format(channel)) @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device.functionalChannels[self.channel].on diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 9c7d843b448..b97948b2d9f 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -4,9 +4,12 @@ import logging from homematicip.aio.device import ( AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) +from homematicip.aio.home import AsyncHome from homeassistant.components.weather import WeatherEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -19,7 +22,8 @@ async def async_setup_platform( pass -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, + async_add_entities) -> None: """Set up the HomematicIP weather sensor from a config entry.""" home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] @@ -36,42 +40,42 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud weather sensor plus & basic.""" - def __init__(self, home, device): + def __init__(self, home: AsyncHome, device) -> None: """Initialize the weather sensor.""" super().__init__(home, device) @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._device.label @property - def temperature(self): + def temperature(self) -> float: """Return the platform temperature.""" return self._device.actualTemperature @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def humidity(self): + def humidity(self) -> int: """Return the humidity.""" return self._device.humidity @property - def wind_speed(self): + def wind_speed(self) -> float: """Return the wind speed.""" return self._device.windSpeed @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return "Powered by Homematic IP" @property - def condition(self): + def condition(self) -> str: """Return the current condition.""" if hasattr(self._device, "raining") and self._device.raining: return 'rainy' @@ -86,6 +90,6 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): """representation of a HomematicIP weather sensor pro.""" @property - def wind_bearing(self): + def wind_bearing(self) -> float: """Return the wind bearing.""" return self._device.windDirection From 7a6acca6bb1f2aca2f7f977c20cb513f5f331328 Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Fri, 26 Apr 2019 08:21:05 +1000 Subject: [PATCH 130/346] Add device info for Logi Circle camera and sensor entities (#23373) --- homeassistant/components/logi_circle/camera.py | 15 ++++++++++++++- homeassistant/components/logi_circle/sensor.py | 16 +++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 09baaa5ba0b..8d68a4c33b7 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -11,7 +11,7 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( - ATTRIBUTION, DOMAIN as LOGI_CIRCLE_DOMAIN, LED_MODE_KEY, + ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN, LED_MODE_KEY, RECORDING_MODE_KEY, SIGNAL_LOGI_CIRCLE_RECONFIGURE, SIGNAL_LOGI_CIRCLE_RECORD, SIGNAL_LOGI_CIRCLE_SNAPSHOT) @@ -98,6 +98,19 @@ class LogiCam(Camera): """Logi Circle camera's support turning on and off ("soft" switch).""" return SUPPORT_ON_OFF + @property + def device_info(self): + """Return information about the device.""" + return { + 'name': self._camera.name, + 'identifiers': { + (LOGI_CIRCLE_DOMAIN, self._camera.id) + }, + 'model': self._camera.model_name, + 'sw_version': self._camera.firmware, + 'manufacturer': DEVICE_BRAND + } + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index 6efd5065ba6..a66c68a694c 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -9,7 +9,8 @@ from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util.dt import as_local from .const import ( - ATTRIBUTION, DOMAIN as LOGI_CIRCLE_DOMAIN, LOGI_SENSORS as SENSOR_TYPES) + ATTRIBUTION, DEVICE_BRAND, DOMAIN as LOGI_CIRCLE_DOMAIN, + LOGI_SENSORS as SENSOR_TYPES) _LOGGER = logging.getLogger(__name__) @@ -66,6 +67,19 @@ class LogiSensor(Entity): """Return the state of the sensor.""" return self._state + @property + def device_info(self): + """Return information about the device.""" + return { + 'name': self._camera.name, + 'identifiers': { + (LOGI_CIRCLE_DOMAIN, self._camera.id) + }, + 'model': self._camera.model_name, + 'sw_version': self._camera.firmware, + 'manufacturer': DEVICE_BRAND + } + @property def device_state_attributes(self): """Return the state attributes.""" From 7d5c1ede72c36ce39643e8c3c39afa5c08f564f0 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 26 Apr 2019 04:33:05 +0200 Subject: [PATCH 131/346] Broadlink fixup unintended breakage from service refactor (#23408) * Allow host/ipv6 address for broadlink service This matches switch config and is a regression fix * Restore padding of packets for broadlink * Drop unused import * Fix comment on test --- .../components/broadlink/__init__.py | 19 ++++++-------- tests/components/broadlink/test_init.py | 25 ++++++------------- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 3404bdef99b..a1cc0a0caa3 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -2,7 +2,6 @@ import asyncio from base64 import b64decode, b64encode import logging -import re import socket from datetime import timedelta @@ -19,26 +18,22 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_RETRY = 3 -def ipv4_address(value): - """Validate an ipv4 address.""" - regex = re.compile(r'^\d+\.\d+\.\d+\.\d+$') - if not regex.match(value): - raise vol.Invalid('Invalid Ipv4 address, expected a.b.c.d') - return value - - def data_packet(value): """Decode a data packet given for broadlink.""" - return b64decode(cv.string(value)) + value = cv.string(value) + extra = len(value) % 4 + if extra > 0: + value = value + ('=' * (4 - extra)) + return b64decode(value) SERVICE_SEND_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): ipv4_address, + vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PACKET): vol.All(cv.ensure_list, [data_packet]) }) SERVICE_LEARN_SCHEMA = vol.Schema({ - vol.Required(CONF_HOST): ipv4_address, + vol.Required(CONF_HOST): cv.string, }) diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index 5dca559cb0e..44ae3d7612a 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -4,10 +4,9 @@ from base64 import b64decode from unittest.mock import MagicMock, patch, call import pytest -import voluptuous as vol from homeassistant.util.dt import utcnow -from homeassistant.components.broadlink import async_setup_service +from homeassistant.components.broadlink import async_setup_service, data_packet from homeassistant.components.broadlink.const import ( DOMAIN, SERVICE_LEARN, SERVICE_SEND) @@ -26,6 +25,13 @@ def dummy_broadlink(): yield broadlink +async def test_padding(hass): + """Verify that non padding strings are allowed.""" + assert data_packet('Jg') == b'&' + assert data_packet('Jg=') == b'&' + assert data_packet('Jg==') == b'&' + + async def test_send(hass): """Test send service.""" mock_device = MagicMock() @@ -100,18 +106,3 @@ async def test_learn_timeout(hass): assert mock_create.call_args == call( "No signal was received", title='Broadlink switch') - - -async def test_ipv4(): - """Test ipv4 parsing.""" - from homeassistant.components.broadlink import ipv4_address - - schema = vol.Schema(ipv4_address) - - for value in ('invalid', '1', '192', '192.168', - '192.168.0', '192.168.0.A'): - with pytest.raises(vol.MultipleInvalid): - schema(value) - - for value in ('192.168.0.1', '10.0.0.1'): - schema(value) From c229a314c6da9e7213bcf750d6d0173df46afe1d Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 26 Apr 2019 05:42:07 +0200 Subject: [PATCH 132/346] Bump requirement to v55 (#23410) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c68da4b566c..588bd23410c 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "Deconz", "documentation": "https://www.home-assistant.io/components/deconz", "requirements": [ - "pydeconz==54" + "pydeconz==55" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a0c9397f2b3..1f13b1edf79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1006,7 +1006,7 @@ pydaikin==1.4.0 pydanfossair==0.0.7 # homeassistant.components.deconz -pydeconz==54 +pydeconz==55 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd44ba61575..99861b3ce88 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -217,7 +217,7 @@ pyHS100==0.3.5 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==54 +pydeconz==55 # homeassistant.components.zwave pydispatcher==2.0.5 From eefb9406c2bf8254c6692604ddf5c7eda5c94747 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Fri, 26 Apr 2019 05:44:38 +0200 Subject: [PATCH 133/346] restore battery_quantity for zha devices (#23320) --- homeassistant/components/zha/core/channels/general.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index b5509b1d559..031eb2464da 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -219,3 +219,5 @@ class PowerConfigurationChannel(ZigbeeChannel): 'battery_percentage_remaining', from_cache=from_cache) await self.get_attribute_value( 'battery_voltage', from_cache=from_cache) + await self.get_attribute_value( + 'battery_quantity', from_cache=from_cache) From b5725f8f19987daba3fed20540b2822d7c7a6831 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Thu, 25 Apr 2019 23:42:39 -0500 Subject: [PATCH 134/346] Fix supported features gates in media_player volume up/down services (#23419) * Correct media player feature gates * Fix failing test * Lint... --- .../components/media_player/__init__.py | 47 ++++++++++--------- homeassistant/helpers/service.py | 3 +- .../media_player/test_async_helpers.py | 14 ++++++ tests/helpers/test_service.py | 2 +- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index b23d95ab625..ccfa968fa9a 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -45,7 +45,8 @@ from .const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP) from .reproduce_state import async_reproduce_states # noqa _LOGGER = logging.getLogger(__name__) @@ -174,77 +175,77 @@ async def async_setup(hass, config): component.async_register_entity_service( SERVICE_TURN_ON, MEDIA_PLAYER_SCHEMA, - 'async_turn_on', SUPPORT_TURN_ON + 'async_turn_on', [SUPPORT_TURN_ON] ) component.async_register_entity_service( SERVICE_TURN_OFF, MEDIA_PLAYER_SCHEMA, - 'async_turn_off', SUPPORT_TURN_OFF + 'async_turn_off', [SUPPORT_TURN_OFF] ) component.async_register_entity_service( SERVICE_TOGGLE, MEDIA_PLAYER_SCHEMA, - 'async_toggle', SUPPORT_TURN_OFF | SUPPORT_TURN_ON + 'async_toggle', [SUPPORT_TURN_OFF | SUPPORT_TURN_ON] ) component.async_register_entity_service( SERVICE_VOLUME_UP, MEDIA_PLAYER_SCHEMA, - 'async_volume_up', SUPPORT_VOLUME_SET + 'async_volume_up', [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP] ) component.async_register_entity_service( SERVICE_VOLUME_DOWN, MEDIA_PLAYER_SCHEMA, - 'async_volume_down', SUPPORT_VOLUME_SET + 'async_volume_down', [SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP] ) component.async_register_entity_service( SERVICE_MEDIA_PLAY_PAUSE, MEDIA_PLAYER_SCHEMA, - 'async_media_play_pause', SUPPORT_PLAY | SUPPORT_PAUSE + 'async_media_play_pause', [SUPPORT_PLAY | SUPPORT_PAUSE] ) component.async_register_entity_service( SERVICE_MEDIA_PLAY, MEDIA_PLAYER_SCHEMA, - 'async_media_play', SUPPORT_PLAY + 'async_media_play', [SUPPORT_PLAY] ) component.async_register_entity_service( SERVICE_MEDIA_PAUSE, MEDIA_PLAYER_SCHEMA, - 'async_media_pause', SUPPORT_PAUSE + 'async_media_pause', [SUPPORT_PAUSE] ) component.async_register_entity_service( SERVICE_MEDIA_STOP, MEDIA_PLAYER_SCHEMA, - 'async_media_stop', SUPPORT_STOP + 'async_media_stop', [SUPPORT_STOP] ) component.async_register_entity_service( SERVICE_MEDIA_NEXT_TRACK, MEDIA_PLAYER_SCHEMA, - 'async_media_next_track', SUPPORT_NEXT_TRACK + 'async_media_next_track', [SUPPORT_NEXT_TRACK] ) component.async_register_entity_service( SERVICE_MEDIA_PREVIOUS_TRACK, MEDIA_PLAYER_SCHEMA, - 'async_media_previous_track', SUPPORT_PREVIOUS_TRACK + 'async_media_previous_track', [SUPPORT_PREVIOUS_TRACK] ) component.async_register_entity_service( SERVICE_CLEAR_PLAYLIST, MEDIA_PLAYER_SCHEMA, - 'async_clear_playlist', SUPPORT_CLEAR_PLAYLIST + 'async_clear_playlist', [SUPPORT_CLEAR_PLAYLIST] ) component.async_register_entity_service( SERVICE_VOLUME_SET, MEDIA_PLAYER_SET_VOLUME_SCHEMA, lambda entity, call: entity.async_set_volume_level( volume=call.data[ATTR_MEDIA_VOLUME_LEVEL]), - SUPPORT_VOLUME_SET + [SUPPORT_VOLUME_SET] ) component.async_register_entity_service( SERVICE_VOLUME_MUTE, MEDIA_PLAYER_MUTE_VOLUME_SCHEMA, lambda entity, call: entity.async_mute_volume( mute=call.data[ATTR_MEDIA_VOLUME_MUTED]), - SUPPORT_VOLUME_MUTE + [SUPPORT_VOLUME_MUTE] ) component.async_register_entity_service( SERVICE_MEDIA_SEEK, MEDIA_PLAYER_MEDIA_SEEK_SCHEMA, lambda entity, call: entity.async_media_seek( position=call.data[ATTR_MEDIA_SEEK_POSITION]), - SUPPORT_SEEK + [SUPPORT_SEEK] ) component.async_register_entity_service( SERVICE_SELECT_SOURCE, MEDIA_PLAYER_SELECT_SOURCE_SCHEMA, - 'async_select_source', SUPPORT_SELECT_SOURCE + 'async_select_source', [SUPPORT_SELECT_SOURCE] ) component.async_register_entity_service( SERVICE_SELECT_SOUND_MODE, MEDIA_PLAYER_SELECT_SOUND_MODE_SCHEMA, - 'async_select_sound_mode', SUPPORT_SELECT_SOUND_MODE + 'async_select_sound_mode', [SUPPORT_SELECT_SOUND_MODE] ) component.async_register_entity_service( SERVICE_PLAY_MEDIA, MEDIA_PLAYER_PLAY_MEDIA_SCHEMA, @@ -252,11 +253,11 @@ async def async_setup(hass, config): media_type=call.data[ATTR_MEDIA_CONTENT_TYPE], media_id=call.data[ATTR_MEDIA_CONTENT_ID], enqueue=call.data.get(ATTR_MEDIA_ENQUEUE) - ), SUPPORT_PLAY_MEDIA + ), [SUPPORT_PLAY_MEDIA] ) component.async_register_entity_service( SERVICE_SHUFFLE_SET, MEDIA_PLAYER_SET_SHUFFLE_SCHEMA, - 'async_set_shuffle', SUPPORT_SHUFFLE_SET + 'async_set_shuffle', [SUPPORT_SHUFFLE_SET] ) return True @@ -701,7 +702,8 @@ class MediaPlayerDevice(Entity): await self.hass.async_add_job(self.volume_up) return - if self.volume_level < 1: + if self.volume_level < 1 \ + and self.supported_features & SUPPORT_VOLUME_SET: await self.async_set_volume_level(min(1, self.volume_level + .1)) async def async_volume_down(self): @@ -714,7 +716,8 @@ class MediaPlayerDevice(Entity): await self.hass.async_add_job(self.volume_down) return - if self.volume_level > 0: + if self.volume_level > 0 \ + and self.supported_features & SUPPORT_VOLUME_SET: await self.async_set_volume_level( max(0, self.volume_level - .1)) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 8c576f58c14..7eb72a66c8b 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -327,7 +327,8 @@ async def _handle_service_platform_call(func, data, entities, context, # Skip entities that don't have the required feature. if required_features is not None \ - and not entity.supported_features & required_features: + and not any(entity.supported_features & feature_set + for feature_set in required_features): continue entity.async_set_context(context) diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 1c4a2fa84a2..aa3d1eff209 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -29,6 +29,13 @@ class AsyncMediaPlayer(mp.MediaPlayerDevice): """Volume level of the media player (0..1).""" return self._volume + @property + def supported_features(self): + """Flag media player features that are supported.""" + return mp.const.SUPPORT_VOLUME_SET | mp.const.SUPPORT_PLAY \ + | mp.const.SUPPORT_PAUSE | mp.const.SUPPORT_TURN_OFF \ + | mp.const.SUPPORT_TURN_ON + @asyncio.coroutine def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" @@ -74,6 +81,13 @@ class SyncMediaPlayer(mp.MediaPlayerDevice): """Volume level of the media player (0..1).""" return self._volume + @property + def supported_features(self): + """Flag media player features that are supported.""" + return mp.const.SUPPORT_VOLUME_SET | mp.const.SUPPORT_VOLUME_STEP \ + | mp.const.SUPPORT_PLAY | mp.const.SUPPORT_PAUSE \ + | mp.const.SUPPORT_TURN_OFF | mp.const.SUPPORT_TURN_ON + def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._volume = volume diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 647ca981da3..81cdd097855 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -280,7 +280,7 @@ async def test_call_with_required_features(hass, mock_entities): Mock(entities=mock_entities) ], test_service_mock, ha.ServiceCall('test_domain', 'test_service', { 'entity_id': 'all' - }), required_features=1) + }), required_features=[1]) assert len(mock_entities) == 2 # Called once because only one of the entities had the required features assert test_service_mock.call_count == 1 From d038d2426b7248179945141e48680e49516ceb7f Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 26 Apr 2019 01:47:40 -0500 Subject: [PATCH 135/346] Add missing feature support flag (#23417) --- homeassistant/components/soundtouch/media_player.py | 9 +++++---- tests/components/soundtouch/test_media_player.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index a2a6c315eda..74c614c03a6 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -5,11 +5,12 @@ import re import voluptuous as vol from homeassistant.components.media_player import ( - MediaPlayerDevice, PLATFORM_SCHEMA) + PLATFORM_SCHEMA, MediaPlayerDevice) from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP) + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP) from homeassistant.const import ( CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE) @@ -56,7 +57,7 @@ DEFAULT_PORT = 8090 SUPPORT_SOUNDTOUCH = SUPPORT_PAUSE | SUPPORT_VOLUME_STEP | \ SUPPORT_VOLUME_MUTE | SUPPORT_PREVIOUS_TRACK | \ SUPPORT_NEXT_TRACK | SUPPORT_TURN_OFF | \ - SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_PLAY + SUPPORT_VOLUME_SET | SUPPORT_TURN_ON | SUPPORT_PLAY | SUPPORT_PLAY_MEDIA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 87f41b11d95..432229a482c 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -372,7 +372,7 @@ class TestSoundtouchMediaPlayer(unittest.TestCase): mock.MagicMock()) assert mocked_soundtouch_device.call_count == 1 all_devices = self.hass.data[soundtouch.DATA_SOUNDTOUCH] - assert all_devices[0].supported_features == 17853 + assert all_devices[0].supported_features == 18365 @mock.patch('libsoundtouch.device.SoundTouchDevice.power_off') @mock.patch('libsoundtouch.device.SoundTouchDevice.volume') From 5dbf58d67f733128db6afe6e1def5c8c413bd606 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 26 Apr 2019 08:56:43 +0200 Subject: [PATCH 136/346] Remove support for deprecated Sonos configuration (#23385) --- homeassistant/components/sonos/__init__.py | 22 +- .../components/sonos/media_player.py | 94 ++--- tests/components/sonos/conftest.py | 77 ++++ tests/components/sonos/test_init.py | 4 +- tests/components/sonos/test_media_player.py | 370 +----------------- 5 files changed, 148 insertions(+), 419 deletions(-) create mode 100644 tests/components/sonos/conftest.py diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index b661fa26fe7..d68e87914ec 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,10 +1,26 @@ """Support to embed Sonos.""" -from homeassistant import config_entries -from homeassistant.helpers import config_entry_flow +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import CONF_HOSTS +from homeassistant.helpers import config_entry_flow, config_validation as cv DOMAIN = 'sonos' +CONF_ADVERTISE_ADDR = 'advertise_addr' +CONF_INTERFACE_ADDR = 'interface_addr' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + MP_DOMAIN: vol.Schema({ + vol.Optional(CONF_ADVERTISE_ADDR): cv.string, + vol.Optional(CONF_INTERFACE_ADDR): cv.string, + vol.Optional(CONF_HOSTS): vol.All(cv.ensure_list_csv, [cv.string]), + }), + }), +}, extra=vol.ALLOW_EXTRA) + async def async_setup(hass, config): """Set up the Sonos component.""" @@ -22,7 +38,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Sonos from a config entry.""" hass.async_create_task(hass.config_entries.async_forward_entry_setup( - entry, 'media_player')) + entry, MP_DOMAIN)) return True diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 7c2e5fec843..2e7d09be334 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -10,20 +10,21 @@ import async_timeout import requests import voluptuous as vol -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, MediaPlayerDevice) +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS, STATE_IDLE, STATE_OFF, STATE_PAUSED, + ATTR_ENTITY_ID, ATTR_TIME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING) import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -from . import DOMAIN as SONOS_DOMAIN +from . import ( + CONF_ADVERTISE_ADDR, CONF_HOSTS, CONF_INTERFACE_ADDR, + DOMAIN as SONOS_DOMAIN) DEPENDENCIES = ('sonos',) @@ -54,9 +55,6 @@ DATA_SONOS = 'sonos_media_player' SOURCE_LINEIN = 'Line-in' SOURCE_TV = 'TV' -CONF_ADVERTISE_ADDR = 'advertise_addr' -CONF_INTERFACE_ADDR = 'interface_addr' - # Service call validation schemas ATTR_SLEEP_TIME = 'sleep_time' ATTR_ALARM_ID = 'alarm_id' @@ -72,12 +70,6 @@ ATTR_SONOS_GROUP = 'sonos_group' UPNP_ERRORS_TO_IGNORE = ['701', '711', '712'] -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]), -}) - SONOS_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) @@ -119,57 +111,34 @@ class SonosData: self.topology_condition = asyncio.Condition(loop=hass.loop) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Sonos platform. - - Deprecated. - """ - _LOGGER.warning('Loading Sonos via platform config is deprecated.') - _setup_platform(hass, config, add_entities, discovery_info) +async def async_setup_platform(hass, + config, + async_add_entities, + discovery_info=None): + """Set up the Sonos platform. Obsolete.""" + _LOGGER.error( + 'Loading Sonos by media_player platform config is no longer supported') async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - def add_entities(entities, update_before_add=False): - """Sync version of async add entities.""" - hass.add_job(async_add_entities, entities, update_before_add) - - hass.async_add_executor_job( - _setup_platform, hass, hass.data[SONOS_DOMAIN].get('media_player', {}), - add_entities, None) - - -def _setup_platform(hass, config, add_entities, discovery_info): - """Set up the Sonos platform.""" import pysonos if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData(hass) + config = hass.data[SONOS_DOMAIN].get('media_player', {}) + advertise_addr = config.get(CONF_ADVERTISE_ADDR) if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr - players = [] - if discovery_info: - player = pysonos.SoCo(discovery_info.get('host')) - - # If host already exists by config - if player.uid in hass.data[DATA_SONOS].uids: - return - - # If invisible, such as a stereo slave - if not player.is_visible: - return - - players.append(player) - else: + def _create_sonos_entities(): + """Discover players and return a list of SonosEntity objects.""" + players = [] 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 for host in hosts: try: players.append(pysonos.SoCo(socket.gethostbyname(host))) @@ -182,11 +151,14 @@ def _setup_platform(hass, config, add_entities, discovery_info): if not players: _LOGGER.warning("No Sonos speakers found") - return - hass.data[DATA_SONOS].uids.update(p.uid for p in players) - add_entities(SonosEntity(p) for p in players) - _LOGGER.debug("Added %s Sonos speakers", len(players)) + return [SonosEntity(p) for p in players] + + entities = await hass.async_add_executor_job(_create_sonos_entities) + hass.data[DATA_SONOS].uids.update(e.unique_id for e in entities) + + async_add_entities(entities) + _LOGGER.debug("Added %s Sonos speakers", len(entities)) def _service_to_entities(service): """Extract and return entities from service call.""" @@ -216,19 +188,19 @@ def _setup_platform(hass, config, add_entities, discovery_info): await SonosEntity.restore_multi( hass, entities, service.data[ATTR_WITH_GROUP]) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_JOIN, async_service_handle, schema=SONOS_JOIN_SCHEMA) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_UNJOIN, async_service_handle, schema=SONOS_SCHEMA) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SNAPSHOT, async_service_handle, schema=SONOS_STATES_SCHEMA) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_RESTORE, async_service_handle, schema=SONOS_STATES_SCHEMA) @@ -244,19 +216,19 @@ def _setup_platform(hass, config, add_entities, discovery_info): elif service.service == SERVICE_SET_OPTION: entity.set_option(**service.data) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_TIMER, service_handle, schema=SONOS_SET_TIMER_SCHEMA) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_CLEAR_TIMER, service_handle, schema=SONOS_SCHEMA) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_UPDATE_ALARM, service_handle, schema=SONOS_UPDATE_ALARM_SCHEMA) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_SET_OPTION, service_handle, schema=SONOS_SET_OPTION_SCHEMA) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py new file mode 100644 index 00000000000..95bc66fe317 --- /dev/null +++ b/tests/components/sonos/conftest.py @@ -0,0 +1,77 @@ +"""Configuration for Sonos tests.""" +from asynctest.mock import Mock, patch as patch +import pytest + +from homeassistant.components.sonos import DOMAIN +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import CONF_HOSTS + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(): + """Create a mock Sonos config entry.""" + return MockConfigEntry(domain=DOMAIN, title='Sonos') + + +@pytest.fixture(name="soco") +def soco_fixture(music_library, speaker_info, dummy_soco_service): + """Create a mock pysonos SoCo fixture.""" + with patch('pysonos.SoCo', autospec=True) as mock, \ + patch('socket.gethostbyname', return_value='192.168.42.2'): + mock_soco = mock.return_value + mock_soco.uid = 'RINCON_test' + mock_soco.music_library = music_library + mock_soco.get_speaker_info.return_value = speaker_info + mock_soco.avTransport = dummy_soco_service + mock_soco.renderingControl = dummy_soco_service + mock_soco.zoneGroupTopology = dummy_soco_service + mock_soco.contentDirectory = dummy_soco_service + + yield mock_soco + + +@pytest.fixture(name="discover") +def discover_fixture(soco): + """Create a mock pysonos discover fixture.""" + with patch('pysonos.discover') as mock: + mock.return_value = {soco} + yield mock + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return { + DOMAIN: { + MP_DOMAIN: { + CONF_HOSTS: ['192.168.42.1'] + } + } + } + + +@pytest.fixture(name="dummy_soco_service") +def dummy_soco_service_fixture(): + """Create dummy_soco_service fixture.""" + service = Mock() + service.subscribe = Mock() + return service + + +@pytest.fixture(name="music_library") +def music_library_fixture(): + """Create music_library fixture.""" + music_library = Mock() + music_library.get_sonos_favorites.return_value = [] + return music_library + + +@pytest.fixture(name="speaker_info") +def speaker_info_fixture(): + """Create speaker_info fixture.""" + return { + 'zone_name': 'Zone A', + 'model_name': 'Model Name', + } diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index a09fa7d2615..3cdeeb08f02 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -35,7 +35,9 @@ async def test_configuring_sonos_creates_entry(hass): patch('pysonos.discover', return_value=True): await async_setup_component(hass, sonos.DOMAIN, { 'sonos': { - 'some_config': 'to_trigger_import' + 'media_player': { + 'interface_addr': '127.0.0.1', + } } }) await hass.async_block_till_done() diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 4cb4a291b16..a06a6160400 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,360 +1,22 @@ -"""The tests for the Demo Media player platform.""" -import datetime -import socket -import unittest -import pysonos.snapshot -from unittest import mock -import pysonos -from pysonos import alarms - -from homeassistant.setup import setup_component -from homeassistant.components.sonos import media_player as sonos -from homeassistant.components.media_player.const import DOMAIN -from homeassistant.components.sonos.media_player import CONF_INTERFACE_ADDR -from homeassistant.const import CONF_HOSTS, CONF_PLATFORM -from homeassistant.util.async_ import run_coroutine_threadsafe - -from tests.common import get_test_home_assistant - -ENTITY_ID = 'media_player.kitchen' +"""Tests for the Sonos Media Player platform.""" +from homeassistant.components.sonos import media_player, DOMAIN +from homeassistant.setup import async_setup_component -class pysonosDiscoverMock(): - """Mock class for the pysonos.discover method.""" - - def discover(interface_addr, all_households=False): - """Return tuple of pysonos.SoCo objects representing found speakers.""" - return {SoCoMock('192.0.2.1')} +async def setup_platform(hass, config_entry, config): + """Set up the media player platform for testing.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() -class AvTransportMock(): - """Mock class for the avTransport property on pysonos.SoCo object.""" - - def __init__(self): - """Initialize ethe Transport mock.""" - pass - - def GetMediaInfo(self, _): - """Get the media details.""" - return { - 'CurrentURI': '', - 'CurrentURIMetaData': '' - } +async def test_async_setup_entry_hosts(hass, config_entry, config, soco): + """Test static setup.""" + await setup_platform(hass, config_entry, config) + assert hass.data[media_player.DATA_SONOS].entities[0].soco == soco -class MusicLibraryMock(): - """Mock class for the music_library property on pysonos.SoCo object.""" - - def get_sonos_favorites(self): - """Return favorites.""" - return [] - - -class CacheMock(): - """Mock class for the _zgs_cache property on pysonos.SoCo object.""" - - def clear(self): - """Clear cache.""" - pass - - -class SoCoMock(): - """Mock class for the pysonos.SoCo object.""" - - def __init__(self, ip): - """Initialize SoCo object.""" - self.ip_address = ip - self.is_visible = True - self.volume = 50 - self.mute = False - self.shuffle = False - self.night_mode = False - self.dialog_mode = False - self.music_library = MusicLibraryMock() - self.avTransport = AvTransportMock() - self._zgs_cache = CacheMock() - - def get_sonos_favorites(self): - """Get favorites list from sonos.""" - return {'favorites': []} - - def get_speaker_info(self, force): - """Return a dict with various data points about the speaker.""" - return {'serial_number': 'B8-E9-37-BO-OC-BA:2', - 'software_version': '32.11-30071', - 'uid': 'RINCON_B8E937BOOCBA02500', - 'zone_icon': 'x-rincon-roomicon:kitchen', - 'mac_address': 'B8:E9:37:BO:OC:BA', - 'zone_name': 'Kitchen', - 'model_name': 'Sonos PLAY:1', - 'hardware_version': '1.8.1.2-1'} - - def get_current_transport_info(self): - """Return a dict with the current state of the speaker.""" - return {'current_transport_speed': '1', - 'current_transport_state': 'STOPPED', - 'current_transport_status': 'OK'} - - def get_current_track_info(self): - """Return a dict with the current track information.""" - return {'album': '', - 'uri': '', - 'title': '', - 'artist': '', - 'duration': '0:00:00', - 'album_art': '', - 'position': '0:00:00', - 'playlist_position': '0', - 'metadata': ''} - - def is_coordinator(self): - """Return true if coordinator.""" - return True - - def join(self, master): - """Join speaker to a group.""" - return - - def set_sleep_timer(self, sleep_time_seconds): - """Set the sleep timer.""" - return - - def unjoin(self): - """Cause the speaker to separate itself from other speakers.""" - return - - def uid(self): - """Return a player uid.""" - return "RINCON_XXXXXXXXXXXXXXXXX" - - def group(self): - """Return all group data of this player.""" - return - - -def add_entities_factory(hass): - """Add entities factory.""" - def add_entities(entities, update_befor_add=False): - """Fake add entity.""" - hass.data[sonos.DATA_SONOS].entities = list(entities) - - return add_entities - - -class TestSonosMediaPlayer(unittest.TestCase): - """Test the media_player module.""" - - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - def monkey_available(self): - """Make a monkey available.""" - return True - - # Monkey patches - self.real_available = sonos.SonosEntity.available - sonos.SonosEntity.available = monkey_available - - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - # Monkey patches - sonos.SonosEntity.available = self.real_available - self.hass.stop() - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_ensure_setup_discovery(self, *args): - """Test a single device using the autodiscovery provided by HASS.""" - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { - 'host': '192.0.2.1' - }) - - entities = self.hass.data[sonos.DATA_SONOS].entities - assert len(entities) == 1 - assert entities[0].name == 'Kitchen' - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch('pysonos.discover') - def test_ensure_setup_config_interface_addr(self, discover_mock, *args): - """Test an interface address config'd by the HASS config file.""" - discover_mock.return_value = {SoCoMock('192.0.2.1')} - - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_INTERFACE_ADDR: '192.0.1.1', - } - } - - assert setup_component(self.hass, DOMAIN, config) - - assert len(self.hass.data[sonos.DATA_SONOS].entities) == 1 - assert discover_mock.call_count == 1 - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_ensure_setup_config_hosts_string_single(self, *args): - """Test a single address config'd by the HASS config file.""" - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_HOSTS: ['192.0.2.1'], - } - } - - assert setup_component(self.hass, DOMAIN, config) - - entities = self.hass.data[sonos.DATA_SONOS].entities - assert len(entities) == 1 - assert entities[0].name == 'Kitchen' - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_ensure_setup_config_hosts_string_multiple(self, *args): - """Test multiple address string config'd by the HASS config file.""" - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_HOSTS: ['192.0.2.1,192.168.2.2'], - } - } - - assert setup_component(self.hass, DOMAIN, config) - - entities = self.hass.data[sonos.DATA_SONOS].entities - assert len(entities) == 2 - assert entities[0].name == 'Kitchen' - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_ensure_setup_config_hosts_list(self, *args): - """Test a multiple address list config'd by the HASS config file.""" - config = { - DOMAIN: { - CONF_PLATFORM: 'sonos', - CONF_HOSTS: ['192.0.2.1', '192.168.2.2'], - } - } - - assert setup_component(self.hass, DOMAIN, config) - - entities = self.hass.data[sonos.DATA_SONOS].entities - assert len(entities) == 2 - assert entities[0].name == 'Kitchen' - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch.object(pysonos, 'discover', new=pysonosDiscoverMock.discover) - @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_ensure_setup_sonos_discovery(self, *args): - """Test a single device using the autodiscovery provided by Sonos.""" - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass)) - entities = self.hass.data[sonos.DATA_SONOS].entities - assert len(entities) == 1 - assert entities[0].name == 'Kitchen' - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch.object(SoCoMock, 'set_sleep_timer') - def test_sonos_set_sleep_timer(self, set_sleep_timerMock, *args): - """Ensure pysonos methods called for sonos_set_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { - 'host': '192.0.2.1' - }) - entity = self.hass.data[sonos.DATA_SONOS].entities[-1] - entity.hass = self.hass - - entity.set_sleep_timer(30) - set_sleep_timerMock.assert_called_once_with(30) - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch.object(SoCoMock, 'set_sleep_timer') - def test_sonos_clear_sleep_timer(self, set_sleep_timerMock, *args): - """Ensure pysonos method called for sonos_clear_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { - 'host': '192.0.2.1' - }) - entity = self.hass.data[sonos.DATA_SONOS].entities[-1] - entity.hass = self.hass - - entity.set_sleep_timer(None) - set_sleep_timerMock.assert_called_once_with(None) - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('pysonos.alarms.Alarm') - @mock.patch('socket.create_connection', side_effect=socket.error()) - def test_set_alarm(self, pysonos_mock, alarm_mock, *args): - """Ensure pysonos methods called for sonos_set_sleep_timer service.""" - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { - 'host': '192.0.2.1' - }) - entity = self.hass.data[sonos.DATA_SONOS].entities[-1] - entity.hass = self.hass - alarm1 = alarms.Alarm(pysonos_mock) - alarm1.configure_mock(_alarm_id="1", start_time=None, enabled=False, - include_linked_zones=False, volume=100) - with mock.patch('pysonos.alarms.get_alarms', return_value=[alarm1]): - attrs = { - 'time': datetime.time(12, 00), - 'enabled': True, - 'include_linked_zones': True, - 'volume': 0.30, - } - entity.set_alarm(alarm_id=2) - alarm1.save.assert_not_called() - entity.set_alarm(alarm_id=1, **attrs) - assert alarm1.enabled == attrs['enabled'] - assert alarm1.start_time == attrs['time'] - assert alarm1.include_linked_zones == \ - attrs['include_linked_zones'] - assert alarm1.volume == 30 - alarm1.save.assert_called_once_with() - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch.object(pysonos.snapshot.Snapshot, 'snapshot') - def test_sonos_snapshot(self, snapshotMock, *args): - """Ensure pysonos methods called for sonos_snapshot service.""" - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { - 'host': '192.0.2.1' - }) - entities = self.hass.data[sonos.DATA_SONOS].entities - entity = entities[-1] - entity.hass = self.hass - - snapshotMock.return_value = True - entity.soco.group = mock.MagicMock() - entity.soco.group.members = [e.soco for e in entities] - run_coroutine_threadsafe( - sonos.SonosEntity.snapshot_multi(self.hass, entities, True), - self.hass.loop).result() - assert snapshotMock.call_count == 1 - assert snapshotMock.call_args == mock.call() - - @mock.patch('pysonos.SoCo', new=SoCoMock) - @mock.patch('socket.create_connection', side_effect=socket.error()) - @mock.patch.object(pysonos.snapshot.Snapshot, 'restore') - def test_sonos_restore(self, restoreMock, *args): - """Ensure pysonos methods called for sonos_restore service.""" - from pysonos.snapshot import Snapshot - - sonos.setup_platform(self.hass, {}, add_entities_factory(self.hass), { - 'host': '192.0.2.1' - }) - entities = self.hass.data[sonos.DATA_SONOS].entities - entity = entities[-1] - entity.hass = self.hass - - restoreMock.return_value = True - entity._snapshot_group = mock.MagicMock() - entity._snapshot_group.members = [e.soco for e in entities] - entity._soco_snapshot = Snapshot(entity.soco) - run_coroutine_threadsafe( - sonos.SonosEntity.restore_multi(self.hass, entities, True), - self.hass.loop).result() - assert restoreMock.call_count == 1 - assert restoreMock.call_args == mock.call() +async def test_async_setup_entry_discover(hass, config_entry, discover): + """Test discovery setup.""" + await setup_platform(hass, config_entry, {}) + assert hass.data[media_player.DATA_SONOS].uids == {'RINCON_test'} From b84ba93c42f990c7d5a259bdf2dc267fdc4e6bbf Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 26 Apr 2019 17:15:37 +0200 Subject: [PATCH 137/346] Refactor netatmo to use hass.data (#23429) * Refactor NETATMO_AUTH to use hass.data * Minor cleanup * Rename conf to auth and other suggestions by Martin * Revert webhook name change * Rename constant * Move auth * Don't use hass.data.get() * Fix auth string --- homeassistant/components/netatmo/__init__.py | 19 +++++++++++-------- .../components/netatmo/binary_sensor.py | 8 ++++++-- homeassistant/components/netatmo/camera.py | 8 ++++++-- homeassistant/components/netatmo/climate.py | 9 ++++++--- homeassistant/components/netatmo/const.py | 5 +++++ homeassistant/components/netatmo/sensor.py | 17 ++++++++++------- 6 files changed, 44 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/netatmo/const.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index f56ffbfffd2..8e556e4b6c9 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -12,6 +12,8 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +from .const import DOMAIN, DATA_NETATMO_AUTH + _LOGGER = logging.getLogger(__name__) DATA_PERSONS = 'netatmo_persons' @@ -20,8 +22,6 @@ DATA_WEBHOOK_URL = 'netatmo_webhook_url' CONF_SECRET_KEY = 'secret_key' CONF_WEBHOOKS = 'webhooks' -DOMAIN = 'netatmo' - SERVICE_ADDWEBHOOK = 'addwebhook' SERVICE_DROPWEBHOOK = 'dropwebhook' @@ -83,10 +83,9 @@ def setup(hass, config): """Set up the Netatmo devices.""" import pyatmo - global NETATMO_AUTH hass.data[DATA_PERSONS] = {} try: - NETATMO_AUTH = pyatmo.ClientAuth( + auth = pyatmo.ClientAuth( config[DOMAIN][CONF_API_KEY], config[DOMAIN][CONF_SECRET_KEY], config[DOMAIN][CONF_USERNAME], config[DOMAIN][CONF_PASSWORD], 'read_station read_camera access_camera ' @@ -96,6 +95,9 @@ def setup(hass, config): _LOGGER.error("Unable to connect to Netatmo API") return False + # Store config to be used during entry setup + hass.data[DATA_NETATMO_AUTH] = auth + if config[DOMAIN][CONF_DISCOVERY]: for component in 'camera', 'sensor', 'binary_sensor', 'climate': discovery.load_platform(hass, component, DOMAIN, {}, config) @@ -107,7 +109,7 @@ def setup(hass, config): webhook_id) hass.components.webhook.async_register( DOMAIN, 'Netatmo', webhook_id, handle_webhook) - NETATMO_AUTH.addwebhook(hass.data[DATA_WEBHOOK_URL]) + auth.addwebhook(hass.data[DATA_WEBHOOK_URL]) hass.bus.listen_once( EVENT_HOMEASSISTANT_STOP, dropwebhook) @@ -117,7 +119,7 @@ def setup(hass, config): if url is None: url = hass.data[DATA_WEBHOOK_URL] _LOGGER.info("Adding webhook for URL: %s", url) - NETATMO_AUTH.addwebhook(url) + auth.addwebhook(url) hass.services.register( DOMAIN, SERVICE_ADDWEBHOOK, _service_addwebhook, @@ -126,7 +128,7 @@ def setup(hass, config): def _service_dropwebhook(service): """Service to drop webhooks during runtime.""" _LOGGER.info("Dropping webhook") - NETATMO_AUTH.dropwebhook() + auth.dropwebhook() hass.services.register( DOMAIN, SERVICE_DROPWEBHOOK, _service_dropwebhook, @@ -137,7 +139,8 @@ def setup(hass, config): def dropwebhook(hass): """Drop the webhook subscription.""" - NETATMO_AUTH.dropwebhook() + auth = hass.data[DATA_NETATMO_AUTH] + auth.dropwebhook() async def handle_webhook(hass, webhook_id, request): diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index f282faf82c8..432820d6dbd 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,7 +8,8 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv -from . import CameraData, NETATMO_AUTH +from .const import DATA_NETATMO_AUTH +from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -59,8 +60,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): module_name = None import pyatmo + + auth = hass.data[DATA_NETATMO_AUTH] + try: - data = CameraData(hass, NETATMO_AUTH, home) + data = CameraData(hass, auth, home) if not data.get_camera_names(): return None except pyatmo.NoDevice: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index b74dce4b262..976e0794938 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -9,7 +9,8 @@ from homeassistant.components.camera import ( from homeassistant.const import CONF_VERIFY_SSL from homeassistant.helpers import config_validation as cv -from . import CameraData, NETATMO_AUTH +from .const import DATA_NETATMO_AUTH +from . import CameraData _LOGGER = logging.getLogger(__name__) @@ -37,8 +38,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL, True) quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) import pyatmo + + auth = hass.data[DATA_NETATMO_AUTH] + try: - data = CameraData(hass, NETATMO_AUTH, home) + data = CameraData(hass, auth, home) for camera_name in data.get_camera_names(): camera_type = data.get_camera_type(camera=camera_name, home=home) if CONF_CAMERAS in config: diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 00c08c654ef..33ad34b25ff 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -14,7 +14,7 @@ from homeassistant.const import ( STATE_OFF, TEMP_CELSIUS, ATTR_TEMPERATURE, CONF_NAME) from homeassistant.util import Throttle -from . import NETATMO_AUTH +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) @@ -68,8 +68,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" import pyatmo homes_conf = config.get(CONF_HOMES) + + auth = hass.data[DATA_NETATMO_AUTH] + try: - home_data = HomeData(NETATMO_AUTH) + home_data = HomeData(auth) except pyatmo.NoDevice: return @@ -88,7 +91,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for home in homes: _LOGGER.debug("Setting up %s ...", home) try: - room_data = ThermostatData(NETATMO_AUTH, home) + room_data = ThermostatData(auth, home) except pyatmo.NoDevice: continue for room_id in room_data.get_room_ids(): diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py new file mode 100644 index 00000000000..ea547aaa52b --- /dev/null +++ b/homeassistant/components/netatmo/const.py @@ -0,0 +1,5 @@ +"""Constants used by the Netatmo component.""" +DOMAIN = 'netatmo' + +DATA_NETATMO = 'netatmo' +DATA_NETATMO_AUTH = 'netatmo_auth' diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index c9c1101c2a2..161177c9c76 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from . import NETATMO_AUTH +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) @@ -68,23 +68,26 @@ MODULE_TYPE_INDOOR = 'NAModule4' def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the available Netatmo weather sensors.""" dev = [] + auth = hass.data[DATA_NETATMO_AUTH] + if CONF_MODULES in config: - manual_config(config, dev) + manual_config(auth, config, dev) else: - auto_config(config, dev) + auto_config(auth, config, dev) if dev: add_entities(dev, True) -def manual_config(config, dev): +def manual_config(auth, config, dev): """Handle manual configuration.""" import pyatmo all_classes = all_product_classes() not_handled = {} + for data_class in all_classes: - data = NetAtmoData(NETATMO_AUTH, data_class, + data = NetAtmoData(auth, data_class, config.get(CONF_STATION)) try: # Iterate each module @@ -107,12 +110,12 @@ def manual_config(config, dev): _LOGGER.error('Module name: "%s" not found', module_name) -def auto_config(config, dev): +def auto_config(auth, config, dev): """Handle auto configuration.""" import pyatmo for data_class in all_product_classes(): - data = NetAtmoData(NETATMO_AUTH, data_class, config.get(CONF_STATION)) + data = NetAtmoData(auth, data_class, config.get(CONF_STATION)) try: for module_name in data.get_module_names(): for variable in \ From 606dbb85d22ac9f4d34a5d3b8a027a32aa506d48 Mon Sep 17 00:00:00 2001 From: Toon Willems Date: Fri, 26 Apr 2019 17:55:30 +0200 Subject: [PATCH 138/346] Fix Flux component (#23431) * Fix Flux component * Update manifest.json * Update manifest.json --- homeassistant/components/flux/manifest.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index d4d67edbd35..9bf3ba09ce7 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -3,8 +3,7 @@ "name": "Flux", "documentation": "https://www.home-assistant.io/components/flux", "requirements": [], - "dependencies": [ - "light" - ], + "dependencies": [], + "after_dependencies": ["light"], "codeowners": [] } From 8fe95f4babbd5022bed41f3f3b4b08f9cf3486d6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 26 Apr 2019 11:06:46 -0600 Subject: [PATCH 139/346] Additional cleanup of IQVIA integration (#23403) * Additional cleanup of IQVIA integration * Task * Moved import * Fixed exception * Member comments (round 1) * Member comments (round 2) * Member comments --- homeassistant/components/iqvia/__init__.py | 151 ++++++++++----------- homeassistant/components/iqvia/const.py | 31 ++--- homeassistant/components/iqvia/sensor.py | 55 +++++--- 3 files changed, 115 insertions(+), 122 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 5806d7ea487..b6bd85fca53 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -1,18 +1,22 @@ """Support for IQVIA.""" +import asyncio from datetime import timedelta import logging +from pyiqvia import Client +from pyiqvia.errors import IQVIAError, InvalidZipError + import voluptuous as vol -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.core import callback -from homeassistant.helpers import ( - aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.decorator import Registry from .const import ( DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE, @@ -24,6 +28,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) + CONF_ZIP_CODE = 'zip_code' DATA_CONFIG = 'config' @@ -31,8 +36,18 @@ DATA_CONFIG = 'config' DEFAULT_ATTRIBUTION = 'Data provided by IQVIA™' DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) -NOTIFICATION_ID = 'iqvia_setup' -NOTIFICATION_TITLE = 'IQVIA Setup' +FETCHER_MAPPING = { + (TYPE_ALLERGY_FORECAST,): (TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK), + (TYPE_ALLERGY_HISTORIC,): (TYPE_ALLERGY_HISTORIC,), + (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY): ( + TYPE_ALLERGY_INDEX,), + (TYPE_ASTHMA_FORECAST,): (TYPE_ASTHMA_FORECAST,), + (TYPE_ASTHMA_HISTORIC,): (TYPE_ASTHMA_HISTORIC,), + (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY): ( + TYPE_ASTHMA_INDEX,), + (TYPE_DISEASE_FORECAST,): (TYPE_DISEASE_FORECAST,), +} + CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -45,16 +60,10 @@ CONFIG_SCHEMA = vol.Schema({ async def async_setup(hass, config): """Set up the IQVIA component.""" - from pyiqvia import Client - from pyiqvia.errors import IQVIAError - hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_CLIENT] = {} hass.data[DOMAIN][DATA_LISTENER] = {} - if DOMAIN not in config: - return True - conf = config[DOMAIN] websession = aiohttp_client.async_get_clientsession(hass) @@ -66,17 +75,12 @@ async def async_setup(hass, config): await iqvia.async_update() except IQVIAError as err: _LOGGER.error('Unable to set up IQVIA: %s', err) - hass.components.persistent_notification.create( - 'Error: {0}
' - 'You will need to restart hass after fixing.' - ''.format(err), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) return False hass.data[DOMAIN][DATA_CLIENT] = iqvia - discovery.load_platform(hass, 'sensor', DOMAIN, {}, conf) + hass.async_create_task( + async_load_platform(hass, 'sensor', DOMAIN, {}, config)) async def refresh(event_time): """Refresh IQVIA data.""" @@ -86,9 +90,7 @@ async def async_setup(hass, config): hass.data[DOMAIN][DATA_LISTENER] = async_track_time_interval( hass, refresh, - timedelta( - seconds=conf.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL.seconds))) + DEFAULT_SCAN_INTERVAL) return True @@ -103,94 +105,81 @@ class IQVIAData: self.sensor_types = sensor_types self.zip_code = client.zip_code - async def _get_data(self, method, key): - """Return API data from a specific call.""" - from pyiqvia.errors import IQVIAError - - try: - data = await method() - self.data[key] = data - except IQVIAError as err: - _LOGGER.error('Unable to get "%s" data: %s', key, err) - self.data[key] = {} + self.fetchers = Registry() + self.fetchers.register(TYPE_ALLERGY_FORECAST)( + self._client.allergens.extended) + self.fetchers.register(TYPE_ALLERGY_HISTORIC)( + self._client.allergens.historic) + self.fetchers.register(TYPE_ALLERGY_OUTLOOK)( + self._client.allergens.outlook) + self.fetchers.register(TYPE_ALLERGY_INDEX)( + self._client.allergens.current) + self.fetchers.register(TYPE_ASTHMA_FORECAST)( + self._client.asthma.extended) + self.fetchers.register(TYPE_ASTHMA_HISTORIC)( + self._client.asthma.historic) + self.fetchers.register(TYPE_ASTHMA_INDEX)(self._client.asthma.current) + self.fetchers.register(TYPE_DISEASE_FORECAST)( + self._client.disease.extended) async def async_update(self): """Update IQVIA data.""" - from pyiqvia.errors import InvalidZipError + tasks = {} + + for conditions, fetcher_types in FETCHER_MAPPING.items(): + if not any(c in self.sensor_types for c in conditions): + continue + + for fetcher_type in fetcher_types: + tasks[fetcher_type] = self.fetchers[fetcher_type]() + + results = await asyncio.gather(*tasks.values(), return_exceptions=True) # IQVIA sites require a bit more complicated error handling, given that - # it sometimes has parts (but not the whole thing) go down: - # - # 1. If `InvalidZipError` is thrown, quit everything immediately. - # 2. If an individual request throws any other error, try the others. - try: - if TYPE_ALLERGY_FORECAST in self.sensor_types: - await self._get_data( - self._client.allergens.extended, TYPE_ALLERGY_FORECAST) - await self._get_data( - self._client.allergens.outlook, TYPE_ALLERGY_OUTLOOK) + # they sometimes have parts (but not the whole thing) go down: + # 1. If `InvalidZipError` is thrown, quit everything immediately. + # 2. If a single request throws any other error, try the others. + for key, result in zip(tasks, results): + if isinstance(result, InvalidZipError): + _LOGGER.error("No data for ZIP: %s", self._client.zip_code) + self.data = {} + return - if TYPE_ALLERGY_HISTORIC in self.sensor_types: - await self._get_data( - self._client.allergens.historic, TYPE_ALLERGY_HISTORIC) + if isinstance(result, IQVIAError): + _LOGGER.error('Unable to get %s data: %s', key, result) + self.data[key] = {} + continue - if any(s in self.sensor_types - for s in [TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY]): - await self._get_data( - self._client.allergens.current, TYPE_ALLERGY_INDEX) - - if TYPE_ASTHMA_FORECAST in self.sensor_types: - await self._get_data( - self._client.asthma.extended, TYPE_ASTHMA_FORECAST) - - if TYPE_ASTHMA_HISTORIC in self.sensor_types: - await self._get_data( - self._client.asthma.historic, TYPE_ASTHMA_HISTORIC) - - if any(s in self.sensor_types - for s in [TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY]): - await self._get_data( - self._client.asthma.current, TYPE_ASTHMA_INDEX) - - if TYPE_DISEASE_FORECAST in self.sensor_types: - await self._get_data( - self._client.disease.extended, TYPE_DISEASE_FORECAST) - - _LOGGER.debug("New data retrieved: %s", self.data) - except InvalidZipError: - _LOGGER.error( - "Cannot retrieve data for ZIP code: %s", self._client.zip_code) - self.data = {} + _LOGGER.debug('Loaded new %s data', key) + self.data[key] = result class IQVIAEntity(Entity): """Define a base IQVIA entity.""" - def __init__(self, iqvia, kind, name, icon, zip_code): + def __init__(self, iqvia, sensor_type, name, icon, zip_code): """Initialize the sensor.""" self._async_unsub_dispatcher_connect = None self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._icon = icon self._iqvia = iqvia - self._kind = kind self._name = name self._state = None + self._type = sensor_type self._zip_code = zip_code @property def available(self): """Return True if entity is available.""" - if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY): return self._iqvia.data.get(TYPE_ALLERGY_INDEX) is not None - if self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + if self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY): return self._iqvia.data.get(TYPE_ASTHMA_INDEX) is not None - return self._iqvia.data.get(self._kind) is not None + return self._iqvia.data.get(self._type) is not None @property def device_state_attributes(self): @@ -215,7 +204,7 @@ class IQVIAEntity(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}'.format(self._zip_code, self._kind) + return '{0}_{1}'.format(self._zip_code, self._type) @property def unit_of_measurement(self): diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index cd2d85a25a4..0ba9d7a0f1e 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -22,24 +22,15 @@ TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday' TYPE_DISEASE_FORECAST = 'disease_average_forecasted' SENSORS = { - TYPE_ALLERGY_FORECAST: ( - 'ForecastSensor', 'Allergy Index: Forecasted Average', 'mdi:flower'), - TYPE_ALLERGY_HISTORIC: ( - 'HistoricalSensor', 'Allergy Index: Historical Average', 'mdi:flower'), - TYPE_ALLERGY_TODAY: ('IndexSensor', 'Allergy Index: Today', 'mdi:flower'), - TYPE_ALLERGY_TOMORROW: ( - 'IndexSensor', 'Allergy Index: Tomorrow', 'mdi:flower'), - TYPE_ALLERGY_YESTERDAY: ( - 'IndexSensor', 'Allergy Index: Yesterday', 'mdi:flower'), - TYPE_ASTHMA_TODAY: ('IndexSensor', 'Asthma Index: Today', 'mdi:flower'), - TYPE_ASTHMA_TOMORROW: ( - 'IndexSensor', 'Asthma Index: Tomorrow', 'mdi:flower'), - TYPE_ASTHMA_YESTERDAY: ( - 'IndexSensor', 'Asthma Index: Yesterday', 'mdi:flower'), - TYPE_ASTHMA_FORECAST: ( - 'ForecastSensor', 'Asthma Index: Forecasted Average', 'mdi:flower'), - TYPE_ASTHMA_HISTORIC: ( - 'HistoricalSensor', 'Asthma Index: Historical Average', 'mdi:flower'), - TYPE_DISEASE_FORECAST: ( - 'ForecastSensor', 'Cold & Flu: Forecasted Average', 'mdi:snowflake') + TYPE_ALLERGY_FORECAST: ('Allergy Index: Forecasted Average', 'mdi:flower'), + TYPE_ALLERGY_HISTORIC: ('Allergy Index: Historical Average', 'mdi:flower'), + TYPE_ALLERGY_TODAY: ('Allergy Index: Today', 'mdi:flower'), + TYPE_ALLERGY_TOMORROW: ('Allergy Index: Tomorrow', 'mdi:flower'), + TYPE_ALLERGY_YESTERDAY: ('Allergy Index: Yesterday', 'mdi:flower'), + TYPE_ASTHMA_TODAY: ('Asthma Index: Today', 'mdi:flower'), + TYPE_ASTHMA_TOMORROW: ('Asthma Index: Tomorrow', 'mdi:flower'), + TYPE_ASTHMA_YESTERDAY: ('Asthma Index: Yesterday', 'mdi:flower'), + TYPE_ASTHMA_FORECAST: ('Asthma Index: Forecasted Average', 'mdi:flower'), + TYPE_ASTHMA_HISTORIC: ('Asthma Index: Historical Average', 'mdi:flower'), + TYPE_DISEASE_FORECAST: ('Cold & Flu: Forecasted Average', 'mdi:snowflake') } diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 1a139c51bf0..252007de21e 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -2,11 +2,15 @@ import logging from statistics import mean +import numpy as np + from homeassistant.components.iqvia import ( - DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK, - TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, - TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, IQVIAEntity) + DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_HISTORIC, + TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, + TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_FORECAST, + TYPE_ASTHMA_HISTORIC, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, + TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, TYPE_DISEASE_FORECAST, + IQVIAEntity) from homeassistant.const import ATTR_STATE _LOGGER = logging.getLogger(__name__) @@ -53,11 +57,25 @@ async def async_setup_platform( """Configure the platform and add the sensors.""" iqvia = hass.data[DOMAIN][DATA_CLIENT] + sensor_class_mapping = { + TYPE_ALLERGY_FORECAST: ForecastSensor, + TYPE_ALLERGY_HISTORIC: HistoricalSensor, + TYPE_ALLERGY_TODAY: IndexSensor, + TYPE_ALLERGY_TOMORROW: IndexSensor, + TYPE_ALLERGY_YESTERDAY: IndexSensor, + TYPE_ASTHMA_FORECAST: ForecastSensor, + TYPE_ASTHMA_HISTORIC: HistoricalSensor, + TYPE_ASTHMA_TODAY: IndexSensor, + TYPE_ASTHMA_TOMORROW: IndexSensor, + TYPE_ASTHMA_YESTERDAY: IndexSensor, + TYPE_DISEASE_FORECAST: ForecastSensor, + } + sensors = [] - for kind in iqvia.sensor_types: - sensor_class, name, icon = SENSORS[kind] - sensors.append( - globals()[sensor_class](iqvia, kind, name, icon, iqvia.zip_code)) + for sensor_type in iqvia.sensor_types: + klass = sensor_class_mapping[sensor_type] + name, icon = SENSORS[sensor_type] + sensors.append(klass(iqvia, sensor_type, name, icon, iqvia.zip_code)) async_add_entities(sensors, True) @@ -72,8 +90,6 @@ def calculate_average_rating(indices): def calculate_trend(indices): """Calculate the "moving average" of a set of indices.""" - import numpy as np - def moving_average(data, samples): """Determine the "moving average" (http://tinyurl.com/yaereb3c).""" ret = np.cumsum(data, dtype=float) @@ -92,11 +108,10 @@ class ForecastSensor(IQVIAEntity): async def async_update(self): """Update the sensor.""" - await self._iqvia.async_update() if not self._iqvia.data: return - data = self._iqvia.data[self._kind].get('Location') + data = self._iqvia.data[self._type].get('Location') if not data: return @@ -115,7 +130,7 @@ class ForecastSensor(IQVIAEntity): ATTR_ZIP_CODE: data['ZIP'] }) - if self._kind == TYPE_ALLERGY_FORECAST: + if self._type == TYPE_ALLERGY_FORECAST: outlook = self._iqvia.data[TYPE_ALLERGY_OUTLOOK] self._attrs[ATTR_OUTLOOK] = outlook.get('Outlook') self._attrs[ATTR_SEASON] = outlook.get('Season') @@ -128,11 +143,10 @@ class HistoricalSensor(IQVIAEntity): async def async_update(self): """Update the sensor.""" - await self._iqvia.async_update() if not self._iqvia.data: return - data = self._iqvia.data[self._kind].get('Location') + data = self._iqvia.data[self._type].get('Location') if not data: return @@ -155,22 +169,21 @@ class IndexSensor(IQVIAEntity): async def async_update(self): """Update the sensor.""" - await self._iqvia.async_update() if not self._iqvia.data: return data = {} - if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY): data = self._iqvia.data[TYPE_ALLERGY_INDEX].get('Location') - elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY): data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location') if not data: return - key = self._kind.split('_')[-1].title() + key = self._type.split('_')[-1].title() [period] = [p for p in data['periods'] if p['Type'] == key] [rating] = [ i['label'] for i in RATING_MAPPING @@ -184,7 +197,7 @@ class IndexSensor(IQVIAEntity): ATTR_ZIP_CODE: data['ZIP'] }) - if self._kind in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY): for idx, attrs in enumerate(period['Triggers']): index = idx + 1 @@ -196,7 +209,7 @@ class IndexSensor(IQVIAEntity): '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): attrs['PlantType'], }) - elif self._kind in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, + elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY): for idx, attrs in enumerate(period['Triggers']): index = idx + 1 From 08c36e008999cbb0fd3366901abb90c135ede474 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 26 Apr 2019 20:24:02 +0200 Subject: [PATCH 140/346] Fix daikin setup (#23440) Fix daikin setup --- homeassistant/components/daikin/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index fc15ebea772..edc447fe721 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -63,10 +63,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): if not daikin_api: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - await asyncio.wait([ - hass.config_entries.async_forward_entry_setup(entry, component) - for component in COMPONENT_TYPES - ]) + for component in COMPONENT_TYPES: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup( + entry, component)) return True From 61ea6256c63690cc3bd5c6b5da5ceb1d2499b442 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Fri, 26 Apr 2019 20:56:55 +0200 Subject: [PATCH 141/346] Fix point setup (#23441) Fix point setup --- homeassistant/components/point/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index c0b2f7acd0f..2ed83fe1d9b 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -88,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): await async_setup_webhook(hass, entry, session) client = MinutPointClient(hass, entry, session) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: client}) - await client.update() + hass.async_create_task(client.update()) return True From d6f6273ac2c76e6eb1b3609623b9e1a2c163cfb0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Apr 2019 12:41:30 -0700 Subject: [PATCH 142/346] Make setup more robust (#23414) * Make setup more robust * Fix typing --- homeassistant/setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 05e3307299a..ee362ad130f 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -151,9 +151,12 @@ async def _async_setup_component(hass: core.HomeAssistant, if hasattr(component, 'async_setup'): result = await component.async_setup( # type: ignore hass, processed_config) - else: + elif hasattr(component, 'setup'): result = await hass.async_add_executor_job( component.setup, hass, processed_config) # type: ignore + else: + log_error("No setup function defined.") + return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, True) @@ -176,7 +179,7 @@ async def _async_setup_component(hass: core.HomeAssistant, for entry in hass.config_entries.async_entries(domain): await entry.async_setup(hass, integration=integration) - hass.config.components.add(component.DOMAIN) # type: ignore + hass.config.components.add(domain) # Cleanup if domain in hass.data[DATA_SETUP]: @@ -184,7 +187,7 @@ async def _async_setup_component(hass: core.HomeAssistant, hass.bus.async_fire( EVENT_COMPONENT_LOADED, - {ATTR_COMPONENT: component.DOMAIN} # type: ignore + {ATTR_COMPONENT: domain} ) return True From f25183ba30f0277e9218a596b9930270310fb292 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 26 Apr 2019 23:18:30 +0100 Subject: [PATCH 143/346] update geniushub client library to fix issue #23444 (#23450) --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 4546be8078b..78efeca7311 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.3.6" + "geniushub-client==0.4.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 1f13b1edf79..d841baa54f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -465,7 +465,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.3.6 +geniushub-client==0.4.5 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From c2e7445271cf2cc60e5c0faba2f115bf533ed8f9 Mon Sep 17 00:00:00 2001 From: Ian Date: Fri, 26 Apr 2019 17:51:58 -0700 Subject: [PATCH 144/346] Add new nextbus sensor (#20197) * Added new nextbus sensor * Fix order in requirements_all after merge * Add more flexible parsing of JSON lists * Undo tox change --- CODEOWNERS | 1 + homeassistant/components/nextbus/__init__.py | 1 + .../components/nextbus/manifest.json | 8 + homeassistant/components/nextbus/sensor.py | 268 ++++++++++++++ requirements_all.txt | 3 + tests/components/nextbus/__init__.py | 1 + tests/components/nextbus/test_sensor.py | 329 ++++++++++++++++++ 7 files changed, 611 insertions(+) create mode 100644 homeassistant/components/nextbus/__init__.py create mode 100644 homeassistant/components/nextbus/manifest.json create mode 100644 homeassistant/components/nextbus/sensor.py create mode 100644 tests/components/nextbus/__init__.py create mode 100644 tests/components/nextbus/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 700d68b9449..38de5b1fe6f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -153,6 +153,7 @@ homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff +homeassistant/components/nextbus/* @vividboarder homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek homeassistant/components/no_ip/* @fabaff diff --git a/homeassistant/components/nextbus/__init__.py b/homeassistant/components/nextbus/__init__.py new file mode 100644 index 00000000000..4891af77b28 --- /dev/null +++ b/homeassistant/components/nextbus/__init__.py @@ -0,0 +1 @@ +"""NextBus sensor.""" diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json new file mode 100644 index 00000000000..63bdbf8a928 --- /dev/null +++ b/homeassistant/components/nextbus/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "nextbus", + "name": "NextBus", + "documentation": "https://www.home-assistant.io/components/nextbus", + "dependencies": [], + "codeowners": ["@vividboarder"], + "requirements": ["py_nextbus==0.1.2"] +} diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py new file mode 100644 index 00000000000..acf8028e31f --- /dev/null +++ b/homeassistant/components/nextbus/sensor.py @@ -0,0 +1,268 @@ +"""NextBus sensor.""" +import logging +from itertools import chain + +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.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.helpers.entity import Entity +from homeassistant.util.dt import utc_from_timestamp + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'nextbus' + +CONF_AGENCY = 'agency' +CONF_ROUTE = 'route' +CONF_STOP = 'stop' + +ICON = 'mdi:bus' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_AGENCY): cv.string, + vol.Required(CONF_ROUTE): cv.string, + vol.Required(CONF_STOP): cv.string, + vol.Optional(CONF_NAME): cv.string, +}) + + +def listify(maybe_list): + """Return list version of whatever value is passed in. + + This is used to provide a consistent way of interacting with the JSON + results from the API. There are several attributes that will either missing + if there are no values, a single dictionary if there is only one value, and + a list if there are multiple. + """ + if maybe_list is None: + return [] + if isinstance(maybe_list, list): + return maybe_list + return [maybe_list] + + +def maybe_first(maybe_list): + """Return the first item out of a list or returns back the input.""" + if isinstance(maybe_list, list) and maybe_list: + return maybe_list[0] + + return maybe_list + + +def validate_value(value_name, value, value_list): + """Validate tag value is in the list of items and logs error if not.""" + valid_values = { + v['tag']: v['title'] + for v in value_list + } + if value not in valid_values: + _LOGGER.error( + 'Invalid %s tag `%s`. Please use one of the following: %s', + value_name, + value, + ', '.join( + '{}: {}'.format(title, tag) + for tag, title in valid_values.items() + ) + ) + return False + + return True + + +def validate_tags(client, agency, route, stop): + """Validate provided tags.""" + # Validate agencies + if not validate_value( + 'agency', + agency, + client.get_agency_list()['agency'], + ): + return False + + # Validate the route + if not validate_value( + 'route', + route, + client.get_route_list(agency)['route'], + ): + return False + + # Validate the stop + route_config = client.get_route_config(route, agency)['route'] + if not validate_value( + 'stop', + stop, + route_config['stop'], + ): + return False + + return True + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Load values from configuration and initialize the platform.""" + agency = config[CONF_AGENCY] + route = config[CONF_ROUTE] + stop = config[CONF_STOP] + name = config.get(CONF_NAME) + + from py_nextbus import NextBusClient + client = NextBusClient(output_format='json') + + # Ensures that the tags provided are valid, also logs out valid values + if not validate_tags(client, agency, route, stop): + _LOGGER.error('Invalid config value(s)') + return + + add_entities([ + NextBusDepartureSensor( + client, + agency, + route, + stop, + name, + ), + ], True) + + +class NextBusDepartureSensor(Entity): + """Sensor class that displays upcoming NextBus times. + + To function, this requires knowing the agency tag as well as the tags for + both the route and the stop. + + This is possibly a little convoluted to provide as it requires making a + request to the service to get these values. Perhaps it can be simplifed in + the future using fuzzy logic and matching. + """ + + def __init__(self, client, agency, route, stop, name=None): + """Initialize sensor with all required config.""" + self.agency = agency + self.route = route + self.stop = stop + self._custom_name = name + # Maybe pull a more user friendly name from the API here + self._name = '{} {}'.format(agency, route) + self._client = client + + # set up default state attributes + self._state = None + self._attributes = {} + + def _log_debug(self, message, *args): + """Log debug message with prefix.""" + _LOGGER.debug(':'.join(( + self.agency, + self.route, + self.stop, + message, + )), *args) + + @property + def name(self): + """Return sensor name. + + Uses an auto generated name based on the data from the API unless a + custom name is provided in the configuration. + """ + if self._custom_name: + return self._custom_name + + return self._name + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_TIMESTAMP + + @property + def state(self): + """Return current state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return additional state attributes.""" + return self._attributes + + @property + def icon(self): + """Return icon to be used for this sensor.""" + # Would be nice if we could determine if the line is a train or bus + # however that doesn't seem to be available to us. Using bus for now. + return ICON + + def update(self): + """Update sensor with new departures times.""" + # Note: using Multi because there is a bug with the single stop impl + results = self._client.get_predictions_for_multi_stops( + [{ + 'stop_tag': int(self.stop), + 'route_tag': self.route, + }], + self.agency, + ) + + self._log_debug('Predictions results: %s', results) + + if 'Error' in results: + self._log_debug('Could not get predictions: %s', results) + + if not results.get('predictions'): + self._log_debug('No predictions available') + self._state = None + # Remove attributes that may now be outdated + self._attributes.pop('upcoming', None) + return + + results = results['predictions'] + + # Set detailed attributes + self._attributes.update({ + 'agency': results.get('agencyTitle'), + 'route': results.get('routeTitle'), + 'stop': results.get('stopTitle'), + }) + + # List all messages in the attributes + messages = listify(results.get('message', [])) + self._log_debug('Messages: %s', messages) + self._attributes['message'] = ' -- '.join(( + message.get('text', '') + for message in messages + )) + + # List out all directions in the attributes + directions = listify(results.get('direction', [])) + self._attributes['direction'] = ', '.join(( + direction.get('title', '') + for direction in directions + )) + + # Chain all predictions together + predictions = list(chain(*[ + listify(direction.get('prediction', [])) + for direction in directions + ])) + + # Short circuit if we don't have any actual bus predictions + if not predictions: + self._log_debug('No upcoming predictions available') + self._state = None + self._attributes['upcoming'] = 'No upcoming predictions' + return + + # Generate list of upcoming times + self._attributes['upcoming'] = ', '.join( + p['minutes'] for p in predictions + ) + + latest_prediction = maybe_first(predictions) + self._state = utc_from_timestamp( + int(latest_prediction['epochTime']) / 1000 + ).isoformat() diff --git a/requirements_all.txt b/requirements_all.txt index d841baa54f9..1540a936b5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -936,6 +936,9 @@ pyW215==0.6.0 # homeassistant.components.w800rf32 pyW800rf32==0.1 +# homeassistant.components.nextbus +py_nextbus==0.1.2 + # homeassistant.components.noaa_tides # py_noaa==0.3.0 diff --git a/tests/components/nextbus/__init__.py b/tests/components/nextbus/__init__.py new file mode 100644 index 00000000000..609e0bb574b --- /dev/null +++ b/tests/components/nextbus/__init__.py @@ -0,0 +1 @@ +"""The tests for the nexbus component.""" diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py new file mode 100644 index 00000000000..ece2a1d8092 --- /dev/null +++ b/tests/components/nextbus/test_sensor.py @@ -0,0 +1,329 @@ +"""The tests for the nexbus sensor component.""" +from copy import deepcopy + +import pytest + +import homeassistant.components.sensor as sensor +import homeassistant.components.nextbus.sensor as nextbus + +from tests.common import (assert_setup_component, + async_setup_component, + MockDependency) + + +VALID_AGENCY = 'sf-muni' +VALID_ROUTE = 'F' +VALID_STOP = '5650' +VALID_AGENCY_TITLE = 'San Francisco Muni' +VALID_ROUTE_TITLE = 'F-Market & Wharves' +VALID_STOP_TITLE = 'Market St & 7th St' +SENSOR_ID_SHORT = 'sensor.sf_muni_f' + +CONFIG_BASIC = { + 'sensor': { + 'platform': 'nextbus', + 'agency': VALID_AGENCY, + 'route': VALID_ROUTE, + 'stop': VALID_STOP, + } +} + +CONFIG_INVALID_MISSING = { + 'sensor': { + 'platform': 'nextbus', + } +} + +BASIC_RESULTS = { + 'predictions': { + 'agencyTitle': VALID_AGENCY_TITLE, + 'routeTitle': VALID_ROUTE_TITLE, + 'stopTitle': VALID_STOP_TITLE, + 'direction': { + 'title': 'Outbound', + 'prediction': [ + {'minutes': '1', 'epochTime': '1553807371000'}, + {'minutes': '2', 'epochTime': '1553807372000'}, + {'minutes': '3', 'epochTime': '1553807373000'}, + ], + } + } +} + + +async def assert_setup_sensor(hass, config, count=1): + """Set up the sensor and assert it's been created.""" + with assert_setup_component(count): + assert await async_setup_component(hass, sensor.DOMAIN, config) + + +@pytest.fixture +def mock_nextbus(): + """Create a mock py_nextbus module.""" + with MockDependency('py_nextbus') as py_nextbus: + yield py_nextbus + + +@pytest.fixture +def mock_nextbus_predictions(mock_nextbus): + """Create a mock of NextBusClient predictions.""" + instance = mock_nextbus.NextBusClient.return_value + instance.get_predictions_for_multi_stops.return_value = BASIC_RESULTS + + yield instance.get_predictions_for_multi_stops + + +@pytest.fixture +def mock_nextbus_lists(mock_nextbus): + """Mock all list functions in nextbus to test validate logic.""" + instance = mock_nextbus.NextBusClient.return_value + instance.get_agency_list.return_value = { + 'agency': [ + {'tag': 'sf-muni', 'title': 'San Francisco Muni'}, + ] + } + instance.get_route_list.return_value = { + 'route': [ + {'tag': 'F', 'title': 'F - Market & Wharves'}, + ] + } + instance.get_route_config.return_value = { + 'route': { + 'stop': [ + {'tag': '5650', 'title': 'Market St & 7th St'}, + ] + } + } + + +async def test_valid_config(hass, mock_nextbus, mock_nextbus_lists): + """Test that sensor is set up properly with valid config.""" + await assert_setup_sensor(hass, CONFIG_BASIC) + + +async def test_invalid_config(hass, mock_nextbus, mock_nextbus_lists): + """Checks that component is not setup when missing information.""" + await assert_setup_sensor(hass, CONFIG_INVALID_MISSING, count=0) + + +async def test_validate_tags(hass, mock_nextbus, mock_nextbus_lists): + """Test that additional validation against the API is successful.""" + client = mock_nextbus.NextBusClient() + # with self.subTest('Valid everything'): + assert nextbus.validate_tags( + client, + VALID_AGENCY, + VALID_ROUTE, + VALID_STOP, + ) + # with self.subTest('Invalid agency'): + assert not nextbus.validate_tags( + client, + 'not-valid', + VALID_ROUTE, + VALID_STOP, + ) + + # with self.subTest('Invalid route'): + assert not nextbus.validate_tags( + client, + VALID_AGENCY, + '0', + VALID_STOP, + ) + + # with self.subTest('Invalid stop'): + assert not nextbus.validate_tags( + client, + VALID_AGENCY, + VALID_ROUTE, + 0, + ) + + +async def test_verify_valid_state( + hass, + mock_nextbus, + mock_nextbus_lists, + mock_nextbus_predictions, +): + """Verify all attributes are set from a valid response.""" + await assert_setup_sensor(hass, CONFIG_BASIC) + mock_nextbus_predictions.assert_called_once_with( + [{'stop_tag': int(VALID_STOP), 'route_tag': VALID_ROUTE}], + VALID_AGENCY, + ) + + state = hass.states.get(SENSOR_ID_SHORT) + assert state is not None + assert state.state == '2019-03-28T21:09:31+00:00' + assert state.attributes['agency'] == VALID_AGENCY_TITLE + assert state.attributes['route'] == VALID_ROUTE_TITLE + assert state.attributes['stop'] == VALID_STOP_TITLE + assert state.attributes['direction'] == 'Outbound' + assert state.attributes['upcoming'] == '1, 2, 3' + + +async def test_message_dict( + hass, + mock_nextbus, + mock_nextbus_lists, + mock_nextbus_predictions, +): + """Verify that a single dict message is rendered correctly.""" + mock_nextbus_predictions.return_value = { + 'predictions': { + 'agencyTitle': VALID_AGENCY_TITLE, + 'routeTitle': VALID_ROUTE_TITLE, + 'stopTitle': VALID_STOP_TITLE, + 'message': {'text': 'Message'}, + 'direction': { + 'title': 'Outbound', + 'prediction': [ + {'minutes': '1', 'epochTime': '1553807371000'}, + {'minutes': '2', 'epochTime': '1553807372000'}, + {'minutes': '3', 'epochTime': '1553807373000'}, + ], + } + } + } + + await assert_setup_sensor(hass, CONFIG_BASIC) + + state = hass.states.get(SENSOR_ID_SHORT) + assert state is not None + assert state.attributes['message'] == 'Message' + + +async def test_message_list( + hass, + mock_nextbus, + mock_nextbus_lists, + mock_nextbus_predictions, +): + """Verify that a list of messages are rendered correctly.""" + mock_nextbus_predictions.return_value = { + 'predictions': { + 'agencyTitle': VALID_AGENCY_TITLE, + 'routeTitle': VALID_ROUTE_TITLE, + 'stopTitle': VALID_STOP_TITLE, + 'message': [{'text': 'Message 1'}, {'text': 'Message 2'}], + 'direction': { + 'title': 'Outbound', + 'prediction': [ + {'minutes': '1', 'epochTime': '1553807371000'}, + {'minutes': '2', 'epochTime': '1553807372000'}, + {'minutes': '3', 'epochTime': '1553807373000'}, + ], + } + } + } + + await assert_setup_sensor(hass, CONFIG_BASIC) + + state = hass.states.get(SENSOR_ID_SHORT) + assert state is not None + assert state.attributes['message'] == 'Message 1 -- Message 2' + + +async def test_direction_list( + hass, + mock_nextbus, + mock_nextbus_lists, + mock_nextbus_predictions, +): + """Verify that a list of messages are rendered correctly.""" + mock_nextbus_predictions.return_value = { + 'predictions': { + 'agencyTitle': VALID_AGENCY_TITLE, + 'routeTitle': VALID_ROUTE_TITLE, + 'stopTitle': VALID_STOP_TITLE, + 'message': [{'text': 'Message 1'}, {'text': 'Message 2'}], + 'direction': [ + { + 'title': 'Outbound', + 'prediction': [ + {'minutes': '1', 'epochTime': '1553807371000'}, + {'minutes': '2', 'epochTime': '1553807372000'}, + {'minutes': '3', 'epochTime': '1553807373000'}, + ], + }, + { + 'title': 'Outbound 2', + 'prediction': { + 'minutes': '4', + 'epochTime': '1553807374000', + }, + }, + ], + } + } + + await assert_setup_sensor(hass, CONFIG_BASIC) + + state = hass.states.get(SENSOR_ID_SHORT) + assert state is not None + assert state.state == '2019-03-28T21:09:31+00:00' + assert state.attributes['agency'] == VALID_AGENCY_TITLE + assert state.attributes['route'] == VALID_ROUTE_TITLE + assert state.attributes['stop'] == VALID_STOP_TITLE + assert state.attributes['direction'] == 'Outbound, Outbound 2' + assert state.attributes['upcoming'] == '1, 2, 3, 4' + + +async def test_custom_name( + hass, + mock_nextbus, + mock_nextbus_lists, + mock_nextbus_predictions, +): + """Verify that a custom name can be set via config.""" + config = deepcopy(CONFIG_BASIC) + config['sensor']['name'] = 'Custom Name' + + await assert_setup_sensor(hass, config) + state = hass.states.get('sensor.custom_name') + assert state is not None + + +async def test_no_predictions( + hass, + mock_nextbus, + mock_nextbus_predictions, + mock_nextbus_lists, +): + """Verify there are no exceptions when no predictions are returned.""" + mock_nextbus_predictions.return_value = {} + + await assert_setup_sensor(hass, CONFIG_BASIC) + + state = hass.states.get(SENSOR_ID_SHORT) + assert state is not None + assert state.state == 'unknown' + + +async def test_verify_no_upcoming( + hass, + mock_nextbus, + mock_nextbus_lists, + mock_nextbus_predictions, +): + """Verify attributes are set despite no upcoming times.""" + mock_nextbus_predictions.return_value = { + 'predictions': { + 'agencyTitle': VALID_AGENCY_TITLE, + 'routeTitle': VALID_ROUTE_TITLE, + 'stopTitle': VALID_STOP_TITLE, + 'direction': { + 'title': 'Outbound', + 'prediction': [], + } + } + } + + await assert_setup_sensor(hass, CONFIG_BASIC) + + state = hass.states.get(SENSOR_ID_SHORT) + assert state is not None + assert state.state == 'unknown' + assert state.attributes['upcoming'] == 'No upcoming predictions' From bf050adcf358b9e8c44f089fd56e3d552aad4609 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 26 Apr 2019 23:28:55 -0600 Subject: [PATCH 145/346] Remove historical allergen and asthma sensors from IQVIA (#23258) * Remove historical sensors from IQVIA * Removed old imports * Removed "Yesterday" sensors --- homeassistant/components/iqvia/__init__.py | 23 +++------ homeassistant/components/iqvia/const.py | 8 --- homeassistant/components/iqvia/sensor.py | 60 +++------------------- 3 files changed, 15 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index b6bd85fca53..3cf80a9d75c 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -20,11 +20,10 @@ from homeassistant.util.decorator import Registry from .const import ( DATA_CLIENT, DATA_LISTENER, DOMAIN, SENSORS, TOPIC_DATA_UPDATE, - TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_HISTORIC, TYPE_ALLERGY_INDEX, - TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_HISTORIC, + TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, + TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY, TYPE_DISEASE_FORECAST) + TYPE_DISEASE_FORECAST) _LOGGER = logging.getLogger(__name__) @@ -38,12 +37,10 @@ DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) FETCHER_MAPPING = { (TYPE_ALLERGY_FORECAST,): (TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK), - (TYPE_ALLERGY_HISTORIC,): (TYPE_ALLERGY_HISTORIC,), - (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY): ( + (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): ( TYPE_ALLERGY_INDEX,), (TYPE_ASTHMA_FORECAST,): (TYPE_ASTHMA_FORECAST,), - (TYPE_ASTHMA_HISTORIC,): (TYPE_ASTHMA_HISTORIC,), - (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY): ( + (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): ( TYPE_ASTHMA_INDEX,), (TYPE_DISEASE_FORECAST,): (TYPE_DISEASE_FORECAST,), } @@ -108,16 +105,12 @@ class IQVIAData: self.fetchers = Registry() self.fetchers.register(TYPE_ALLERGY_FORECAST)( self._client.allergens.extended) - self.fetchers.register(TYPE_ALLERGY_HISTORIC)( - self._client.allergens.historic) self.fetchers.register(TYPE_ALLERGY_OUTLOOK)( self._client.allergens.outlook) self.fetchers.register(TYPE_ALLERGY_INDEX)( self._client.allergens.current) self.fetchers.register(TYPE_ASTHMA_FORECAST)( self._client.asthma.extended) - self.fetchers.register(TYPE_ASTHMA_HISTORIC)( - self._client.asthma.historic) self.fetchers.register(TYPE_ASTHMA_INDEX)(self._client.asthma.current) self.fetchers.register(TYPE_DISEASE_FORECAST)( self._client.disease.extended) @@ -171,12 +164,10 @@ class IQVIAEntity(Entity): @property def available(self): """Return True if entity is available.""" - if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY): + if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): return self._iqvia.data.get(TYPE_ALLERGY_INDEX) is not None - if self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY): + if self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): return self._iqvia.data.get(TYPE_ASTHMA_INDEX) is not None return self._iqvia.data.get(self._type) is not None diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index 0ba9d7a0f1e..af8e2861a02 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -7,30 +7,22 @@ DATA_LISTENER = 'listener' TOPIC_DATA_UPDATE = 'data_update' TYPE_ALLERGY_FORECAST = 'allergy_average_forecasted' -TYPE_ALLERGY_HISTORIC = 'allergy_average_historical' TYPE_ALLERGY_INDEX = 'allergy_index' TYPE_ALLERGY_OUTLOOK = 'allergy_outlook' TYPE_ALLERGY_TODAY = 'allergy_index_today' TYPE_ALLERGY_TOMORROW = 'allergy_index_tomorrow' -TYPE_ALLERGY_YESTERDAY = 'allergy_index_yesterday' TYPE_ASTHMA_FORECAST = 'asthma_average_forecasted' -TYPE_ASTHMA_HISTORIC = 'asthma_average_historical' TYPE_ASTHMA_INDEX = 'asthma_index' TYPE_ASTHMA_TODAY = 'asthma_index_today' TYPE_ASTHMA_TOMORROW = 'asthma_index_tomorrow' -TYPE_ASTHMA_YESTERDAY = 'asthma_index_yesterday' TYPE_DISEASE_FORECAST = 'disease_average_forecasted' SENSORS = { TYPE_ALLERGY_FORECAST: ('Allergy Index: Forecasted Average', 'mdi:flower'), - TYPE_ALLERGY_HISTORIC: ('Allergy Index: Historical Average', 'mdi:flower'), TYPE_ALLERGY_TODAY: ('Allergy Index: Today', 'mdi:flower'), TYPE_ALLERGY_TOMORROW: ('Allergy Index: Tomorrow', 'mdi:flower'), - TYPE_ALLERGY_YESTERDAY: ('Allergy Index: Yesterday', 'mdi:flower'), TYPE_ASTHMA_TODAY: ('Asthma Index: Today', 'mdi:flower'), TYPE_ASTHMA_TOMORROW: ('Asthma Index: Tomorrow', 'mdi:flower'), - TYPE_ASTHMA_YESTERDAY: ('Asthma Index: Yesterday', 'mdi:flower'), TYPE_ASTHMA_FORECAST: ('Asthma Index: Forecasted Average', 'mdi:flower'), - TYPE_ASTHMA_HISTORIC: ('Asthma Index: Historical Average', 'mdi:flower'), TYPE_DISEASE_FORECAST: ('Cold & Flu: Forecasted Average', 'mdi:snowflake') } diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 252007de21e..9dbea448e1c 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -5,12 +5,10 @@ from statistics import mean import numpy as np from homeassistant.components.iqvia import ( - DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_HISTORIC, - TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, - TYPE_ALLERGY_TOMORROW, TYPE_ALLERGY_YESTERDAY, TYPE_ASTHMA_FORECAST, - TYPE_ASTHMA_HISTORIC, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, - TYPE_ASTHMA_TOMORROW, TYPE_ASTHMA_YESTERDAY, TYPE_DISEASE_FORECAST, - IQVIAEntity) + DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK, + TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, + TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, + TYPE_ASTHMA_TOMORROW, TYPE_DISEASE_FORECAST, IQVIAEntity) from homeassistant.const import ATTR_STATE _LOGGER = logging.getLogger(__name__) @@ -59,15 +57,11 @@ async def async_setup_platform( sensor_class_mapping = { TYPE_ALLERGY_FORECAST: ForecastSensor, - TYPE_ALLERGY_HISTORIC: HistoricalSensor, TYPE_ALLERGY_TODAY: IndexSensor, TYPE_ALLERGY_TOMORROW: IndexSensor, - TYPE_ALLERGY_YESTERDAY: IndexSensor, TYPE_ASTHMA_FORECAST: ForecastSensor, - TYPE_ASTHMA_HISTORIC: HistoricalSensor, TYPE_ASTHMA_TODAY: IndexSensor, TYPE_ASTHMA_TOMORROW: IndexSensor, - TYPE_ASTHMA_YESTERDAY: IndexSensor, TYPE_DISEASE_FORECAST: ForecastSensor, } @@ -80,14 +74,6 @@ async def async_setup_platform( async_add_entities(sensors, True) -def calculate_average_rating(indices): - """Calculate the human-friendly historical allergy average.""" - ratings = list( - r['label'] for n in indices for r in RATING_MAPPING - if r['minimum'] <= n <= r['maximum']) - return max(set(ratings), key=ratings.count) - - def calculate_trend(indices): """Calculate the "moving average" of a set of indices.""" def moving_average(data, samples): @@ -138,32 +124,6 @@ class ForecastSensor(IQVIAEntity): self._state = average -class HistoricalSensor(IQVIAEntity): - """Define sensor related to historical data.""" - - async def async_update(self): - """Update the sensor.""" - if not self._iqvia.data: - return - - data = self._iqvia.data[self._type].get('Location') - if not data: - return - - indices = [p['Index'] for p in data['periods']] - average = round(mean(indices), 1) - - self._attrs.update({ - ATTR_CITY: data['City'].title(), - ATTR_RATING: calculate_average_rating(indices), - ATTR_STATE: data['State'], - ATTR_TREND: calculate_trend(indices), - ATTR_ZIP_CODE: data['ZIP'] - }) - - self._state = average - - class IndexSensor(IQVIAEntity): """Define sensor related to indices.""" @@ -173,11 +133,9 @@ class IndexSensor(IQVIAEntity): return data = {} - if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY): + if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): data = self._iqvia.data[TYPE_ALLERGY_INDEX].get('Location') - elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY): + elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location') if not data: @@ -197,8 +155,7 @@ class IndexSensor(IQVIAEntity): ATTR_ZIP_CODE: data['ZIP'] }) - if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, - TYPE_ALLERGY_YESTERDAY): + if self._type in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): for idx, attrs in enumerate(period['Triggers']): index = idx + 1 self._attrs.update({ @@ -209,8 +166,7 @@ class IndexSensor(IQVIAEntity): '{0}_{1}'.format(ATTR_ALLERGEN_TYPE, index): attrs['PlantType'], }) - elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_ASTHMA_YESTERDAY): + elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): for idx, attrs in enumerate(period['Triggers']): index = idx + 1 self._attrs.update({ From c174b83f5408124fc7834e8282969a1e8f9cca16 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 27 Apr 2019 12:30:24 +0200 Subject: [PATCH 146/346] Return of travis (#23409) * Return .travis.yml This reverts commit a5b03541e90a7ff7aff9f4b26573879c9c1de3fa. * Remove coveralls * Remove deploy * Support only the extreme Python versions * Ignore bleeding edge * Remove docker service --- .travis.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..8e0162f7fff --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +sudo: false +dist: xenial +addons: + apt: + sources: + - sourceline: "ppa:jonathonf/ffmpeg-4" + packages: + - libudev-dev + - libavformat-dev + - libavcodec-dev + - libavdevice-dev + - libavutil-dev + - libswscale-dev + - libswresample-dev + - libavfilter-dev +matrix: + fast_finish: true + include: + - python: "3.5.3" + env: TOXENV=lint + - python: "3.5.3" + env: TOXENV=pylint + - python: "3.5.3" + env: TOXENV=typing + - python: "3.5.3" + env: TOXENV=py35 + - python: "3.7" + env: TOXENV=py37 + +cache: + directories: + - $HOME/.cache/pip +install: pip install -U tox +language: python +script: travis_wait 40 tox --develop From bdc95e76d0646fa295416d8e7c11026f31eaab65 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 27 Apr 2019 09:17:57 -0600 Subject: [PATCH 147/346] Fix broken forecast trend attribute in IQVIA (#23454) --- homeassistant/components/iqvia/sensor.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 9dbea448e1c..f7e899b5d7d 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -46,6 +46,7 @@ RATING_MAPPING = [{ 'maximum': 12 }] +TREND_FLAT = 'Flat' TREND_INCREASING = 'Increasing' TREND_SUBSIDING = 'Subsiding' @@ -76,17 +77,18 @@ async def async_setup_platform( def calculate_trend(indices): """Calculate the "moving average" of a set of indices.""" - def moving_average(data, samples): - """Determine the "moving average" (http://tinyurl.com/yaereb3c).""" - ret = np.cumsum(data, dtype=float) - ret[samples:] = ret[samples:] - ret[:-samples] - return ret[samples - 1:] / samples + index_range = np.arange(0, len(indices)) + index_array = np.array(indices) + linear_fit = np.polyfit(index_range, index_array, 1) + slope = round(linear_fit[0], 2) - increasing = np.all(np.diff(moving_average(np.array(indices), 4)) > 0) - - if increasing: + if slope > 0: return TREND_INCREASING - return TREND_SUBSIDING + + if slope < 0: + return TREND_SUBSIDING + + return TREND_FLAT class ForecastSensor(IQVIAEntity): From 96fb311f6b2a2f92ce3ed23183ed68b98b6141fe Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 27 Apr 2019 17:18:55 +0200 Subject: [PATCH 148/346] Fix netatmo_public sensor to use netatmo authentication (#23455) --- homeassistant/components/netatmo_public/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo_public/sensor.py b/homeassistant/components/netatmo_public/sensor.py index 814675ca8b7..8295c0c0688 100644 --- a/homeassistant/components/netatmo_public/sensor.py +++ b/homeassistant/components/netatmo_public/sensor.py @@ -12,6 +12,8 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +from homeassistant.components.netatmo.const import DATA_NETATMO_AUTH + _LOGGER = logging.getLogger(__name__) CONF_AREAS = 'areas' @@ -54,12 +56,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the access to Netatmo binary sensor.""" - netatmo = hass.components.netatmo + auth = hass.data[DATA_NETATMO_AUTH] sensors = [] areas = config.get(CONF_AREAS) for area_conf in areas: - data = NetatmoPublicData(netatmo.NETATMO_AUTH, + data = NetatmoPublicData(auth, lat_ne=area_conf.get(CONF_LAT_NE), lon_ne=area_conf.get(CONF_LON_NE), lat_sw=area_conf.get(CONF_LAT_SW), From 1dbfa8f3bee1dea510d5de1aa3ea1c2e1247c631 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Sat, 27 Apr 2019 10:24:07 -0500 Subject: [PATCH 149/346] Bump amcrest to 1.4.0 and use new storage_all method (#23446) Improve sdcard sensor so it only sends one command to camera per update as opposed to the four it used to send to reduce network traffic and make data consistent. Also better handle returned values of 'unknown'. --- homeassistant/components/amcrest/manifest.json | 2 +- homeassistant/components/amcrest/sensor.py | 18 +++++++++++++----- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index e05fdcf4bd4..a2eb8c24e21 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -3,7 +3,7 @@ "name": "Amcrest", "documentation": "https://www.home-assistant.io/components/amcrest", "requirements": [ - "amcrest==1.3.0" + "amcrest==1.4.0" ], "dependencies": [ "ffmpeg" diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index 4d2cd88c5ae..718d08358c4 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -84,8 +84,16 @@ class AmcrestSensor(Entity): self._state = self._api.ptz_presets_count elif self._sensor_type == 'sdcard': - sd_used = self._api.storage_used - sd_total = self._api.storage_total - self._attrs['Total'] = '{0} {1}'.format(*sd_total) - self._attrs['Used'] = '{0} {1}'.format(*sd_used) - self._state = self._api.storage_used_percent + storage = self._api.storage_all + try: + self._attrs['Total'] = '{:.2f} {}'.format(*storage['total']) + except ValueError: + self._attrs['Total'] = '{} {}'.format(*storage['total']) + try: + self._attrs['Used'] = '{:.2f} {}'.format(*storage['used']) + except ValueError: + self._attrs['Used'] = '{} {}'.format(*storage['used']) + try: + self._state = '{:.2f}'.format(storage['used_percent']) + except ValueError: + self._state = storage['used_percent'] diff --git a/requirements_all.txt b/requirements_all.txt index 1540a936b5b..b4a8d1bd75e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -161,7 +161,7 @@ alarmdecoder==1.13.2 alpha_vantage==2.1.0 # homeassistant.components.amcrest -amcrest==1.3.0 +amcrest==1.4.0 # homeassistant.components.androidtv androidtv==0.0.15 From 6be798bffc78568b6c2c6e58d712fbe02cc8421c Mon Sep 17 00:00:00 2001 From: anrudolph <49680492+anrudolph@users.noreply.github.com> Date: Sat, 27 Apr 2019 17:40:20 +0200 Subject: [PATCH 150/346] Added option to use self-signed certificates (#23139) * Added option to use self-signed certificates I defined a new option for configuration.yaml, 'verify_ssl', which is set to 'True' by default for obvious security reasons. However, in order to work with self-signed certificates, it is now possible to disable the certificate validation.(eg, I use a nextcloud instance with self-signed certificates) Credit for code in line 57 goes to user 'gen2' on the homeassistant community, who suggested this solution. https://community.home-assistant.io/t/caldav-configuration/38198/25 I only took it a step further and made it a config option. * Update calendar.py * Added the import of CONF_VERIFY_SSL I hope this passes the test now... * Update homeassistant/components/caldav/calendar.py Cool! Didn't know about that possibility, my coding experience is literally two weeks. Thanks for sharing! Co-Authored-By: anrudolph <49680492+anrudolph@users.noreply.github.com> * Removed some lines of code I think, in order for the last commit to work, these lines have to be removed. Correct? * Trying to get this passing the checks Trying around to get the simplified code to work --- homeassistant/components/caldav/calendar.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 446473c7f40..594a6473877 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.calendar import ( PLATFORM_SCHEMA, CalendarEventDevice, get_date) from homeassistant.const import ( - CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME) + CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle, dt @@ -36,7 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SEARCH): cv.string, }) - ])) + ])), + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean }) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) @@ -50,7 +51,8 @@ def setup_platform(hass, config, add_entities, disc_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - client = caldav.DAVClient(url, None, username, password) + client = caldav.DAVClient(url, None, username, password, + ssl_verify_cert=config.get(CONF_VERIFY_SSL)) calendars = client.principal().calendars() From 148860587cf2213df4612fe21cde94a06c193459 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 27 Apr 2019 18:16:44 +0200 Subject: [PATCH 151/346] No longer promote imports inside methods (#23471) --- .github/PULL_REQUEST_TEMPLATE.md | 2 -- script/gen_requirements_all.py | 1 - 2 files changed, 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ebebf487275..b91540c5f8e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -25,7 +25,6 @@ If user exposed functionality or configuration variables are added/changed: If the code communicates with devices, web services, or third-party tools: - [ ] [_The manifest file_][manifest-docs] has all fields filled out correctly ([example][ex-manifest]). - [ ] New dependencies have been added to `requirements` in the manifest ([example][ex-requir]). - - [ ] New dependencies are only imported inside functions that use them ([example][ex-import]). - [ ] New or updated dependencies have been added to `requirements_all.txt` by running `script/gen_requirements_all.py`. - [ ] New files were added to `.coveragerc`. @@ -34,5 +33,4 @@ If the code does not interact with devices: [ex-manifest]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json [ex-requir]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/mobile_app/manifest.json#L5 -[ex-import]: https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/keyboard/__init__.py#L23 [manifest-docs]: https://developers.home-assistant.io/docs/en/development_checklist.html#_the-manifest-file_ diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9586dc17947..dc8ed652d11 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -232,7 +232,6 @@ def gather_modules(): if errors: print("******* ERROR") print("Errors while importing: ", ', '.join(errors)) - print("Make sure you import 3rd party libraries inside methods.") return None return reqs From b6a13262da966ddfc592a1ff8d502439d2b8e22b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 27 Apr 2019 18:17:50 +0200 Subject: [PATCH 152/346] Bump pychromecast (#23463) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index c506dba8cf1..dd189ac91e7 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Cast", "documentation": "https://www.home-assistant.io/components/cast", "requirements": [ - "pychromecast==3.2.0" + "pychromecast==3.2.1" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index b4a8d1bd75e..6b1ec122c94 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -985,7 +985,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==3.2.0 +pychromecast==3.2.1 # homeassistant.components.cmus pycmus==0.1.1 From 5efe089699ea4a119984005f6fa3910dfa9fa35c Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 27 Apr 2019 19:05:50 +0200 Subject: [PATCH 153/346] Improve handling of unavailable Sonos speakers (#23472) --- homeassistant/components/sonos/manifest.json | 2 +- .../components/sonos/media_player.py | 34 ++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index b2598bc5be9..338d6454b28 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "documentation": "https://www.home-assistant.io/components/sonos", "requirements": [ - "pysonos==0.0.10" + "pysonos==0.0.11" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 2e7d09be334..68bd81a6dc7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -7,9 +7,12 @@ import socket import urllib import async_timeout -import requests import voluptuous as vol +import pysonos +import pysonos.snapshot +from pysonos.exceptions import SoCoUPnPException, SoCoException + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, @@ -122,8 +125,6 @@ async def async_setup_platform(hass, async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Sonos from a config entry.""" - import pysonos - if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData(hass) @@ -142,7 +143,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for host in hosts: try: players.append(pysonos.SoCo(socket.gethostbyname(host))) - except OSError: + except (OSError, SoCoException): _LOGGER.warning("Failed to initialize '%s'", host) else: players = pysonos.discover( @@ -260,8 +261,6 @@ def soco_error(errorcodes=None): @ft.wraps(funct) def wrapper(*args, **kwargs): """Wrap for all soco UPnP exception.""" - from pysonos.exceptions import SoCoUPnPException, SoCoException - try: return funct(*args, **kwargs) except SoCoUPnPException as err: @@ -472,7 +471,7 @@ class SonosEntity(MediaPlayerDevice): self._subscribe_to_player_events() else: for subscription in self._subscriptions: - self.hass.async_add_executor_job(subscription.unsubscribe) + subscription.unsubscribe() self._subscriptions = [] self._player_volume = None @@ -488,10 +487,13 @@ class SonosEntity(MediaPlayerDevice): 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() + try: + self.update_groups() + self.update_volume() + if self.is_coordinator: + self.update_media() + except SoCoException: + pass def update_media(self, event=None): """Update information about currently playing media.""" @@ -574,7 +576,6 @@ class SonosEntity(MediaPlayerDevice): current_uri_metadata = media_info["CurrentURIMetaData"] if current_uri_metadata not in ('', 'NOT_IMPLEMENTED', None): # currently soco does not have an API for this - import pysonos current_uri_metadata = pysonos.xml.XML.fromstring( pysonos.utils.really_utf8(current_uri_metadata)) @@ -678,7 +679,7 @@ class SonosEntity(MediaPlayerDevice): coordinator_uid = self.soco.group.coordinator.uid slave_uids = [p.uid for p in self.soco.group.members if p.uid != coordinator_uid] - except requests.exceptions.RequestException: + except SoCoException: pass return [coordinator_uid] + slave_uids @@ -933,7 +934,6 @@ class SonosEntity(MediaPlayerDevice): If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if kwargs.get(ATTR_MEDIA_ENQUEUE): - from pysonos.exceptions import SoCoUPnPException try: self.soco.add_uri_to_queue(media_id) except SoCoUPnPException: @@ -994,9 +994,7 @@ class SonosEntity(MediaPlayerDevice): @soco_error() def snapshot(self, with_group): """Snapshot the state of a player.""" - from pysonos.snapshot import Snapshot - - self._soco_snapshot = Snapshot(self.soco) + self._soco_snapshot = pysonos.snapshot.Snapshot(self.soco) self._soco_snapshot.snapshot() if with_group: self._snapshot_group = self._sonos_group.copy() @@ -1025,8 +1023,6 @@ class SonosEntity(MediaPlayerDevice): @soco_error() def restore(self): """Restore a snapshotted state to a player.""" - from pysonos.exceptions import SoCoException - try: # pylint: disable=protected-access self._soco_snapshot.restore() diff --git a/requirements_all.txt b/requirements_all.txt index 6b1ec122c94..c826f257be0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1288,7 +1288,7 @@ pysmartthings==0.6.7 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.10 +pysonos==0.0.11 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99861b3ce88..5589625e956 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.7 # homeassistant.components.sonos -pysonos==0.0.10 +pysonos==0.0.11 # homeassistant.components.spc pyspcwebgw==0.4.0 From f1b2622d78e3c1cd74cf09ef2131a27cbd5a7b28 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Sat, 27 Apr 2019 15:02:42 -0500 Subject: [PATCH 154/346] Use remote image for HEOS (#23420) --- homeassistant/components/heos/media_player.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index ae1b1c32003..595c9b7008c 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -278,6 +278,11 @@ class HeosMediaPlayer(MediaPlayerDevice): return None return self._media_position_updated_at + @property + def media_image_remotely_accessible(self) -> bool: + """If the image url is remotely accessible.""" + return True + @property def media_image_url(self) -> str: """Image url of current playing media.""" From 26bf1b2173ca95be6f5c20127d3b347fe371e912 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Apr 2019 13:27:17 -0700 Subject: [PATCH 155/346] Fix Hue sensors returning None value (#23478) --- homeassistant/components/hue/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 30a439f92e9..7664bd38d97 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -32,6 +32,9 @@ class HueLightLevel(GenericHueGaugeSensorEntity): @property def state(self): """Return the state of the device.""" + if self.sensor.lightlevel is None: + return None + # https://developers.meethue.com/develop/hue-api/supported-devices/#clip_zll_lightlevel # Light level in 10000 log10 (lux) +1 measured by sensor. Logarithm # scale used because the human eye adjusts to light levels and small @@ -59,4 +62,7 @@ class HueTemperature(GenericHueGaugeSensorEntity): @property def state(self): """Return the state of the device.""" + if self.sensor.temperature is None: + return None + return self.sensor.temperature / 100 From 7458f1f6efda6614fcf110300d26591076143951 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 27 Apr 2019 21:27:41 +0100 Subject: [PATCH 156/346] Add a warning that honeywell/EU is to be deprecated (#23469) * Add a warning that honeywell/EU is to be deprecated * improve warning --- homeassistant/components/honeywell/climate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index df19f67a876..5a07b094e24 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -53,6 +53,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if region == 'us': return _setup_us(username, password, config, add_entities) + _LOGGER.warning( + "The honeywell component is deprecated for EU (i.e. non-US) systems, " + "this functionality will be removed in version 0.96.") + _LOGGER.warning( + "Please switch to the evohome component, " + "see: https://home-assistant.io/components/evohome") + return _setup_round(username, password, config, add_entities) From 300d1f44a67329242aec1c2f90c65b93800ba194 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 27 Apr 2019 22:18:05 -0700 Subject: [PATCH 157/346] Updated frontend to 20190427.0 --- homeassistant/components/frontend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 608687610e4..0a82a36536f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190424.0" + "home-assistant-frontend==20190427.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index c826f257be0..b510ebbc0de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -554,7 +554,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190424.0 +home-assistant-frontend==20190427.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5589625e956..27550726f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -142,7 +142,7 @@ hdate==0.8.7 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190424.0 +home-assistant-frontend==20190427.0 # homeassistant.components.homekit_controller homekit[IP]==0.13.0 From b09f5b67436d8db44825d146b78ddce391d4469c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 28 Apr 2019 00:42:06 -0700 Subject: [PATCH 158/346] Only include agent user ID in SYNC responses (#23497) --- homeassistant/components/cloud/client.py | 14 ++++---------- .../components/google_assistant/helpers.py | 6 +++++- .../components/google_assistant/smart_home.py | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 5bbd7bb48fa..f47eae74986 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -100,10 +100,13 @@ class CloudClient(Interface): return google_conf['filter'](entity.entity_id) + username = self._hass.data[DOMAIN].claims["cognito:username"] + self._google_config = ga_h.Config( should_expose=should_expose, secure_devices_pin=self._prefs.google_secure_devices_pin, entity_config=google_conf.get(CONF_ENTITY_CONFIG), + agent_user_id=username, ) # Set it to the latest. @@ -149,19 +152,10 @@ class CloudClient(Interface): if not self._prefs.google_enabled: return ga.turned_off_response(payload) - answer = await ga.async_handle_message( + return await ga.async_handle_message( self._hass, self.google_config, self.prefs.cloud_user, payload ) - # Fix AgentUserId - try: - cloud = self._hass.data[DOMAIN] - answer['payload']['agentUserId'] = cloud.claims['cognito:username'] - except (TypeError, KeyError): - return ga.turned_off_response(payload) - - return answer - async def async_webhook_message( self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud webhook message to client.""" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 71cce9de500..4d3f2855b31 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -20,12 +20,16 @@ class Config: """Hold the configuration for Google Assistant.""" def __init__(self, should_expose, - entity_config=None, secure_devices_pin=None): + entity_config=None, secure_devices_pin=None, + agent_user_id=None): """Initialize the configuration.""" self.should_expose = should_expose self.entity_config = entity_config or {} self.secure_devices_pin = secure_devices_pin + # Agent User Id to use for query responses + self.agent_user_id = agent_user_id + class RequestData: """Hold data associated with a particular request.""" diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 37f35edf645..1ec47bbedd6 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -99,7 +99,7 @@ async def async_devices_sync(hass, data, payload): devices.append(serialized) response = { - 'agentUserId': data.context.user_id, + 'agentUserId': data.config.agent_user_id or data.context.user_id, 'devices': devices, } From 37badbbf09cde7ea24c21e3120e9057a30edac6b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 28 Apr 2019 13:58:19 +0200 Subject: [PATCH 159/346] Skip flaky stream tests (#23493) * Skip flaky test_hls tests * Skip flaky test_recorder tests --- tests/components/stream/test_hls.py | 5 +++++ tests/components/stream/test_recorder.py | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 9d898d96d78..7e7c04c6100 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -2,6 +2,8 @@ from datetime import timedelta from urllib.parse import urlparse +import pytest + from homeassistant.setup import async_setup_component from homeassistant.components.stream import request_stream import homeassistant.util.dt as dt_util @@ -11,6 +13,7 @@ from tests.components.stream.common import ( generate_h264_video, preload_stream) +@pytest.mark.skip("Flaky in CI") async def test_hls_stream(hass, hass_client): """ Test hls stream. @@ -52,6 +55,7 @@ async def test_hls_stream(hass, hass_client): assert fail_response.status == 404 +@pytest.mark.skip("Flaky in CI") async def test_stream_timeout(hass, hass_client): """Test hls stream timeout.""" await async_setup_component(hass, 'stream', { @@ -90,6 +94,7 @@ async def test_stream_timeout(hass, hass_client): assert fail_response.status == 404 +@pytest.mark.skip("Flaky in CI") async def test_stream_ended(hass): """Test hls stream packets ended.""" await async_setup_component(hass, 'stream', { diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 8e4a69e28ff..2f8d638baed 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -2,6 +2,7 @@ from datetime import timedelta from io import BytesIO from unittest.mock import patch +import pytest from homeassistant.setup import async_setup_component from homeassistant.components.stream.core import Segment @@ -13,6 +14,7 @@ from tests.components.stream.common import ( generate_h264_video, preload_stream) +@pytest.mark.skip("Flaky in CI") async def test_record_stream(hass, hass_client): """ Test record stream. @@ -44,6 +46,7 @@ async def test_record_stream(hass, hass_client): assert segments > 1 +@pytest.mark.skip("Flaky in CI") async def test_recorder_timeout(hass, hass_client): """Test recorder timeout.""" await async_setup_component(hass, 'stream', { @@ -69,6 +72,7 @@ async def test_recorder_timeout(hass, hass_client): assert mock_cleanup.called +@pytest.mark.skip("Flaky in CI") async def test_recorder_save(): """Test recorder save.""" # Setup From 54c34bb2242f391ae9ae8b51e5a28efdb5aebf7c Mon Sep 17 00:00:00 2001 From: chmielowiec Date: Sun, 28 Apr 2019 18:38:21 +0200 Subject: [PATCH 160/346] Display person component as occupancy sensor HomeKit (#23451) --- homeassistant/components/homekit/__init__.py | 2 +- tests/components/homekit/test_get_accessories.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index f524455fede..76ad5e3f12c 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -110,7 +110,7 @@ def get_accessory(hass, driver, state, aid, config): if state.domain == 'alarm_control_panel': a_type = 'SecuritySystem' - elif state.domain == 'binary_sensor' or state.domain == 'device_tracker': + elif state.domain in ('binary_sensor', 'device_tracker', 'person'): a_type = 'BinarySensor' elif state.domain == 'climate': diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index e395402b958..fb46cf33404 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -105,6 +105,7 @@ 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', {}), + ('BinarySensor', 'person.someone', 'home', {}), ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), ('AirQualitySensor', 'sensor.air_quality', '40', {ATTR_DEVICE_CLASS: 'pm25'}), From 687bbce90074d7958043249473f015b4d5c30bab Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 28 Apr 2019 19:32:02 +0100 Subject: [PATCH 161/346] Fixes local_file camera service (#23479) * Fixes service Fixes service so only the target camera is updated * Update test_camera.py * fix lint --- homeassistant/components/local_file/camera.py | 14 +++++++++++-- tests/components/local_file/test_camera.py | 21 ++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 444f4109e98..5f17716abbb 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -5,7 +5,7 @@ import os import voluptuous as vol -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, ATTR_ENTITY_ID from homeassistant.components.camera import ( Camera, CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA) from homeassistant.components.camera.const import DOMAIN @@ -14,6 +14,7 @@ from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) CONF_FILE_PATH = 'file_path' +DATA_LOCAL_FILE = 'local_file_cameras' DEFAULT_NAME = 'Local File' SERVICE_UPDATE_FILE_PATH = 'local_file_update_file_path' @@ -29,13 +30,22 @@ CAMERA_SERVICE_UPDATE_FILE_PATH = CAMERA_SERVICE_SCHEMA.extend({ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Camera that works with local files.""" + if DATA_LOCAL_FILE not in hass.data: + hass.data[DATA_LOCAL_FILE] = [] + file_path = config[CONF_FILE_PATH] camera = LocalFile(config[CONF_NAME], file_path) + hass.data[DATA_LOCAL_FILE].append(camera) def update_file_path_service(call): """Update the file path.""" file_path = call.data.get(CONF_FILE_PATH) - camera.update_file_path(file_path) + entity_ids = call.data.get(ATTR_ENTITY_ID) + cameras = hass.data[DATA_LOCAL_FILE] + + for camera in cameras: + if camera.entity_id in entity_ids: + camera.update_file_path(file_path) return True hass.services.register( diff --git a/tests/components/local_file/test_camera.py b/tests/components/local_file/test_camera.py index a96f9768be4..ade5eb4add3 100644 --- a/tests/components/local_file/test_camera.py +++ b/tests/components/local_file/test_camera.py @@ -124,12 +124,19 @@ async def test_update_file_path(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' + + camera_1 = { + 'platform': 'local_file', + 'file_path': 'mock/path.jpg' } - }) + camera_2 = { + 'platform': 'local_file', + 'name': 'local_file_camera_2', + 'file_path': 'mock/path_2.jpg' + } + await async_setup_component(hass, 'camera', { + 'camera': [camera_1, camera_2] + }) # Fetch state and check motion detection attribute state = hass.states.get('camera.local_file') @@ -148,3 +155,7 @@ async def test_update_file_path(hass): state = hass.states.get('camera.local_file') assert state.attributes.get('file_path') == 'new/path.jpg' + + # Check that local_file_camera_2 file_path is still as configured + state = hass.states.get('camera.local_file_camera_2') + assert state.attributes.get('file_path') == 'mock/path_2.jpg' From 2fecc7d5a4d55621766675be37d72322915b4aa9 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 28 Apr 2019 14:46:49 -0400 Subject: [PATCH 162/346] Update aiolifx_effects to 0.2.2 (#23473) --- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 6b811b01f51..a8b1fd58afe 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/lifx", "requirements": [ "aiolifx==0.6.7", - "aiolifx_effects==0.2.1" + "aiolifx_effects==0.2.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index b510ebbc0de..7203d967e53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,7 +140,7 @@ aioimaplib==0.7.15 aiolifx==0.6.7 # homeassistant.components.lifx -aiolifx_effects==0.2.1 +aiolifx_effects==0.2.2 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 From 41d9bd42af7a4ce0b728ad20c6f6bd4d607ebac1 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 28 Apr 2019 20:57:58 +0200 Subject: [PATCH 163/346] Catch RequestException in influxdb writer (#23508) --- homeassistant/components/influxdb/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index bf2ba1b8ecc..fbc1b16a4fe 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -321,7 +321,9 @@ class InfluxThread(threading.Thread): _LOGGER.debug("Wrote %d events", len(json)) break - except (exceptions.InfluxDBClientError, IOError): + except (exceptions.InfluxDBClientError, + requests.exceptions.RequestException, + IOError): if retry < self.max_tries: time.sleep(RETRY_DELAY) else: From b60c7ce479251369180f8b8a4abc6624e6f88b21 Mon Sep 17 00:00:00 2001 From: Wojciech Mamak Date: Sun, 28 Apr 2019 21:01:33 +0200 Subject: [PATCH 164/346] Fixed bug with max_result (#23507) The config option 'max_result' was assigned to not existing 'max_result' variable, it should be assigned to 'maxResult'. The current version causes an error when max_result option is used. --- homeassistant/components/google/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 36ab3459d5c..969b15e8c1f 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -76,7 +76,7 @@ class GoogleCalendarData: params = dict(DEFAULT_GOOGLE_SEARCH_PARAMS) params['calendarId'] = self.calendar_id if self.max_results: - params['max_results'] = self.max_results + params['maxResults'] = self.max_results if self.search: params['q'] = self.search From 2f45a7e3b9891147e80f01f30b68e182c1bafd3f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 28 Apr 2019 21:09:06 +0200 Subject: [PATCH 165/346] Fix cleanup of dynamic group (#23475) --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 0a1406adcee..653f3b5aed2 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -659,7 +659,7 @@ class CastDevice(MediaPlayerDevice): self.entity_id, self._cast_info.friendly_name, self._cast_info.host, self._cast_info.port, cast_info) - self.async_del_dynamic_group() + await self.async_del_dynamic_group() self._dynamic_group_cast_info = cast_info # pylint: disable=protected-access From 0f49a9cb7b1ea81328a6bec068ee1bd73b5e49d0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 28 Apr 2019 12:09:20 -0700 Subject: [PATCH 166/346] Return state when changing optimistic covers (#23498) --- .../components/google_assistant/trait.py | 29 +++-- .../components/google_assistant/test_trait.py | 115 +++++++++++++----- 2 files changed, 105 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index ac2f65af058..11bb4f28aeb 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1027,6 +1027,8 @@ class OpenCloseTrait(_Trait): COMMAND_OPENCLOSE ] + override_position = None + @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" @@ -1043,20 +1045,22 @@ class OpenCloseTrait(_Trait): def sync_attributes(self): """Return opening direction.""" - attrs = {} + response = {} if self.state.domain == binary_sensor.DOMAIN: - attrs['queryOnlyOpenClose'] = True - return attrs + response['queryOnlyOpenClose'] = True + return response def query_attributes(self): """Return state query attributes.""" domain = self.state.domain response = {} - if domain == cover.DOMAIN: - # When it's an assumed state, we will always report it as 50% - # Google will not issue an open command if the assumed state is - # open, even if that is currently incorrect. + if self.override_position is not None: + response['openPercent'] = self.override_position + + elif domain == cover.DOMAIN: + # When it's an assumed state, we will return that querying state + # is not supported. if self.state.attributes.get(ATTR_ASSUMED_STATE): raise SmartHomeError( ERR_NOT_SUPPORTED, @@ -1067,7 +1071,7 @@ class OpenCloseTrait(_Trait): ERR_NOT_SUPPORTED, 'Querying state is not supported') - position = self.state.attributes.get( + position = self.override_position or self.state.attributes.get( cover.ATTR_CURRENT_POSITION ) @@ -1096,7 +1100,6 @@ class OpenCloseTrait(_Trait): ): _verify_pin_challenge(data, challenge) - position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION) if params['openPercent'] == 0: await self.hass.services.async_call( cover.DOMAIN, cover.SERVICE_CLOSE_COVER, { @@ -1107,7 +1110,8 @@ class OpenCloseTrait(_Trait): cover.DOMAIN, cover.SERVICE_OPEN_COVER, { ATTR_ENTITY_ID: self.state.entity_id }, blocking=True, context=data.context) - elif position is not None: + elif (self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & + cover.SUPPORT_SET_POSITION): await self.hass.services.async_call( cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION, { ATTR_ENTITY_ID: self.state.entity_id, @@ -1118,6 +1122,11 @@ class OpenCloseTrait(_Trait): ERR_FUNCTION_NOT_SUPPORTED, 'Setting a position is not supported') + if (self.state.attributes.get(ATTR_ASSUMED_STATE) or + self.state.state == STATE_UNKNOWN): + print("YOO") + self.override_position = params['openPercent'] + @register_trait class VolumeTrait(_Trait): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 96ca8d82f5e..3be25b498b5 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1109,36 +1109,9 @@ async def test_openclose_cover(hass): assert trait.OpenCloseTrait.supported(cover.DOMAIN, cover.SUPPORT_SET_POSITION, None) - # No position trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { - }), BASIC_CONFIG) - - assert trt.sync_attributes() == {} - assert trt.query_attributes() == { - 'openPercent': 100 - } - - # No state - trt = trait.OpenCloseTrait(hass, State('cover.bla', STATE_UNKNOWN, { - }), BASIC_CONFIG) - - assert trt.sync_attributes() == {} - - with pytest.raises(helpers.SmartHomeError): - trt.query_attributes() - - # Assumed state - trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { - ATTR_ASSUMED_STATE: True, - }), BASIC_CONFIG) - - assert trt.sync_attributes() == {} - - with pytest.raises(helpers.SmartHomeError): - trt.query_attributes() - - trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { - cover.ATTR_CURRENT_POSITION: 75 + cover.ATTR_CURRENT_POSITION: 75, + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, }), BASIC_CONFIG) assert trt.sync_attributes() == {} @@ -1158,6 +1131,89 @@ async def test_openclose_cover(hass): } +async def test_openclose_cover_unknown_state(hass): + """Test OpenClose trait support for cover domain with unknown state.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None + assert trait.OpenCloseTrait.supported(cover.DOMAIN, + cover.SUPPORT_SET_POSITION, None) + + # No state + trt = trait.OpenCloseTrait(hass, State('cover.bla', STATE_UNKNOWN, { + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + + with pytest.raises(helpers.SmartHomeError): + trt.query_attributes() + + calls = async_mock_service( + hass, cover.DOMAIN, cover.SERVICE_OPEN_COVER) + await trt.execute( + trait.COMMAND_OPENCLOSE, BASIC_DATA, + {'openPercent': 100}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'cover.bla', + } + + assert trt.query_attributes() == {'openPercent': 100} + + +async def test_openclose_cover_assumed_state(hass): + """Test OpenClose trait support for cover domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None + assert trait.OpenCloseTrait.supported(cover.DOMAIN, + cover.SUPPORT_SET_POSITION, None) + + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + ATTR_ASSUMED_STATE: True, + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + + with pytest.raises(helpers.SmartHomeError): + trt.query_attributes() + + calls = async_mock_service( + hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION) + await trt.execute( + trait.COMMAND_OPENCLOSE, BASIC_DATA, + {'openPercent': 40}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'cover.bla', + cover.ATTR_POSITION: 40 + } + + assert trt.query_attributes() == {'openPercent': 40} + + +async def test_openclose_cover_no_position(hass): + """Test OpenClose trait support for cover domain.""" + assert helpers.get_google_type(cover.DOMAIN, None) is not None + assert trait.OpenCloseTrait.supported(cover.DOMAIN, + cover.SUPPORT_SET_POSITION, None) + + trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { + }), BASIC_CONFIG) + + assert trt.sync_attributes() == {} + assert trt.query_attributes() == { + 'openPercent': 100 + } + + calls = async_mock_service( + hass, cover.DOMAIN, cover.SERVICE_CLOSE_COVER) + await trt.execute( + trait.COMMAND_OPENCLOSE, BASIC_DATA, + {'openPercent': 0}, {}) + assert len(calls) == 1 + assert calls[0].data == { + ATTR_ENTITY_ID: 'cover.bla', + } + + @pytest.mark.parametrize('device_class', ( cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE, @@ -1170,6 +1226,7 @@ async def test_openclose_cover_secure(hass, device_class): trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, { ATTR_DEVICE_CLASS: device_class, + ATTR_SUPPORTED_FEATURES: cover.SUPPORT_SET_POSITION, cover.ATTR_CURRENT_POSITION: 75 }), PIN_CONFIG) From b4a7980084b5a2c7d2f0fdae293247b1f39f7385 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 29 Apr 2019 03:27:35 +0200 Subject: [PATCH 167/346] Update Philips js to v0.0.8 (#23462) * Don't send volume on volume set This needs updated lib * Bump version and avoid getting channels explicitly * Add myself as codeowner * Use version 0.0.8 instead which doesn't reuse session Some TV's doesn't play nice with HTTP/1.1 connection re-use. * Add new requirement to requirements_all.txt --- CODEOWNERS | 1 + homeassistant/components/philips_js/manifest.json | 4 ++-- homeassistant/components/philips_js/media_player.py | 4 +--- requirements_all.txt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 38de5b1fe6f..979805a65dd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/core homeassistant/components/panel_iframe/* @home-assistant/core homeassistant/components/persistent_notification/* @home-assistant/core +homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 16a3dbd119d..0b1579a139d 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -3,8 +3,8 @@ "name": "Philips js", "documentation": "https://www.home-assistant.io/components/philips_js", "requirements": [ - "ha-philipsjs==0.0.6" + "ha-philipsjs==0.0.8" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@elupus"] } diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 0b0b1de4275..743992990ca 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -197,7 +197,7 @@ class PhilipsTV(MediaPlayerDevice): def mute_volume(self, mute): """Send mute command.""" - self._tv.setVolume(self._tv.volume, mute) + self._tv.setVolume(None, mute) self._update_soon(DELAY_ACTION_DEFAULT) def set_volume_level(self, volume): @@ -270,8 +270,6 @@ class PhilipsTV(MediaPlayerDevice): def update(self): """Get the latest data and update device state.""" self._tv.update() - self._tv.getChannelId() - self._tv.getChannels() self._sources = { srcid: source['name'] or "Source {}".format(srcid) diff --git a/requirements_all.txt b/requirements_all.txt index 7203d967e53..0bd974f0de7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -515,7 +515,7 @@ gstreamer-player==1.1.2 ha-ffmpeg==2.0 # homeassistant.components.philips_js -ha-philipsjs==0.0.6 +ha-philipsjs==0.0.8 # homeassistant.components.habitica habitipy==0.2.0 From 5529bcc1149e60d5bcf93af47fec04e83ac755b3 Mon Sep 17 00:00:00 2001 From: mcc05 <38889743+mcc05@users.noreply.github.com> Date: Mon, 29 Apr 2019 02:29:12 +0100 Subject: [PATCH 168/346] Fixed AlexaPowerController to report power state for thermostats (#23468) Fixed AlexaPowerController to report power state for thermostats, to look if state is OFF return OFF, otherwise report ON as thermostats have multiple values for ON --- homeassistant/components/alexa/smart_home.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index e16a1d45ab7..21a65b1ede1 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -449,9 +449,9 @@ class _AlexaPowerController(_AlexaInterface): if name != 'powerState': raise _UnsupportedProperty(name) - if self.entity.state == STATE_ON: - return 'ON' - return 'OFF' + if self.entity.state == STATE_OFF: + return 'OFF' + return 'ON' class _AlexaLockController(_AlexaInterface): From 0ecf1521531b8d6fd9b98c75a392b43ee1789082 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 29 Apr 2019 10:20:09 +0200 Subject: [PATCH 169/346] Continuous discovery of Sonos speakers (#23484) --- .../components/sonos/media_player.py | 63 +++++++++++-------- tests/components/sonos/conftest.py | 6 +- tests/components/sonos/test_media_player.py | 8 ++- 3 files changed, 47 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 68bd81a6dc7..4aea88c6657 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -4,6 +4,7 @@ import datetime import functools as ft import logging import socket +import time import urllib import async_timeout @@ -35,6 +36,8 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 +DISCOVERY_INTERVAL = 60 + # Quiet down pysonos logging to just actual problems. logging.getLogger('pysonos').setLevel(logging.WARNING) logging.getLogger('pysonos.data_structures_entry').setLevel(logging.ERROR) @@ -109,7 +112,6 @@ class SonosData: def __init__(self, hass): """Initialize the data.""" - self.uids = set() self.entities = [] self.topology_condition = asyncio.Condition(loop=hass.loop) @@ -134,32 +136,41 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr - def _create_sonos_entities(): - """Discover players and return a list of SonosEntity objects.""" - players = [] + def _discovery(now=None): + """Discover players from network or configuration.""" hosts = config.get(CONF_HOSTS) + def _discovered_player(soco): + """Handle a (re)discovered player.""" + try: + # Make sure that the player is available + _ = soco.volume + + entity = _get_entity_from_soco_uid(hass, soco.uid) + if not entity: + hass.add_job(async_add_entities, [SonosEntity(soco)]) + else: + entity.seen() + except SoCoException: + pass + if hosts: for host in hosts: try: - players.append(pysonos.SoCo(socket.gethostbyname(host))) + player = pysonos.SoCo(socket.gethostbyname(host)) + if player.is_visible: + _discovered_player(player) except (OSError, SoCoException): - _LOGGER.warning("Failed to initialize '%s'", host) + if now is None: + _LOGGER.warning("Failed to initialize '%s'", host) else: - players = pysonos.discover( - interface_addr=config.get(CONF_INTERFACE_ADDR), - all_households=True) + pysonos.discover_thread( + _discovered_player, + interface_addr=config.get(CONF_INTERFACE_ADDR)) - if not players: - _LOGGER.warning("No Sonos speakers found") + hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery) - return [SonosEntity(p) for p in players] - - entities = await hass.async_add_executor_job(_create_sonos_entities) - hass.data[DATA_SONOS].uids.update(e.unique_id for e in entities) - - async_add_entities(entities) - _LOGGER.debug("Added %s Sonos speakers", len(entities)) + hass.async_add_executor_job(_discovery) def _service_to_entities(service): """Extract and return entities from service call.""" @@ -309,6 +320,7 @@ class SonosEntity(MediaPlayerDevice): def __init__(self, player): """Initialize the Sonos entity.""" + self._seen = None self._subscriptions = [] self._receives_events = False self._volume_increment = 2 @@ -338,6 +350,7 @@ class SonosEntity(MediaPlayerDevice): self._snapshot_group = None self._set_basic_information() + self.seen() async def async_added_to_hass(self): """Subscribe sonos events.""" @@ -397,20 +410,18 @@ class SonosEntity(MediaPlayerDevice): """Return coordinator of this player.""" return self._coordinator + def seen(self): + """Record that this player was seen right now.""" + self._seen = time.monotonic() + @property def available(self) -> bool: """Return True if entity is available.""" return self._available def _check_available(self): - """Check that we can still connect to the player.""" - try: - sock = socket.create_connection( - address=(self.soco.ip_address, 1443), timeout=3) - sock.close() - return True - except socket.error: - return False + """Check that we saw the player recently.""" + return self._seen > time.monotonic() - 2*DISCOVERY_INTERVAL def _set_basic_information(self): """Set initial entity information.""" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 95bc66fe317..2f7faf03f4d 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -35,8 +35,10 @@ def soco_fixture(music_library, speaker_info, dummy_soco_service): @pytest.fixture(name="discover") def discover_fixture(soco): """Create a mock pysonos discover fixture.""" - with patch('pysonos.discover') as mock: - mock.return_value = {soco} + def do_callback(callback, **kwargs): + callback(soco) + + with patch('pysonos.discover_thread', side_effect=do_callback) as mock: yield mock diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index a06a6160400..f46fe41c36e 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -13,10 +13,14 @@ async def setup_platform(hass, config_entry, config): async def test_async_setup_entry_hosts(hass, config_entry, config, soco): """Test static setup.""" await setup_platform(hass, config_entry, config) - assert hass.data[media_player.DATA_SONOS].entities[0].soco == soco + + entity = hass.data[media_player.DATA_SONOS].entities[0] + assert entity.soco == soco async def test_async_setup_entry_discover(hass, config_entry, discover): """Test discovery setup.""" await setup_platform(hass, config_entry, {}) - assert hass.data[media_player.DATA_SONOS].uids == {'RINCON_test'} + + entity = hass.data[media_player.DATA_SONOS].entities[0] + assert entity.unique_id == 'RINCON_test' From 2245ee98e31255477fbf412024f6de851d9b4e74 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 29 Apr 2019 10:53:27 +0200 Subject: [PATCH 170/346] Ignore stale directories (#23464) * Ignore stale directories * Remove redundant tests * Revert "Remove redundant tests" * Print warning when skipping directories * Suggest to remove stale directory --- script/hassfest/model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 059231cf954..de252715992 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -46,6 +46,13 @@ class Integration: if fil.is_file() or fil.name == '__pycache__': continue + init = fil / '__init__.py' + if not init.exists(): + print("Warning: {} missing, skipping directory. " + "If this is your development environment, " + "you can safely delete this folder.".format(init)) + continue + integration = cls(fil) integration.load_manifest() integrations[integration.domain] = integration From 471a26bde12cfe87670d9966f8425eb8719a362b Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Mon, 29 Apr 2019 13:39:45 +0200 Subject: [PATCH 171/346] Do not log tracebacks for influxdb write errors (#23522) * Do not log influxdb write exceptions * Log exception name --- homeassistant/components/influxdb/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index fbc1b16a4fe..0289dc63d88 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -321,14 +321,12 @@ class InfluxThread(threading.Thread): _LOGGER.debug("Wrote %d events", len(json)) break - except (exceptions.InfluxDBClientError, - requests.exceptions.RequestException, - IOError): + except (exceptions.InfluxDBClientError, IOError) as err: if retry < self.max_tries: time.sleep(RETRY_DELAY) else: if not self.write_errors: - _LOGGER.exception("Write error") + _LOGGER.error("Write error: %s", err) self.write_errors += len(json) def run(self): From 5e045f3df258f4d52e12a230d003e1b859e55e62 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Mon, 29 Apr 2019 20:38:59 +0800 Subject: [PATCH 172/346] Allow device_class for template covers * #23486. Allows device_class to be set for template covers --- homeassistant/components/template/cover.py | 17 ++++-- tests/components/template/test_cover.py | 62 ++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 2fdcc9f1036..9c5f242684b 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -5,14 +5,14 @@ import voluptuous as vol from homeassistant.core import callback from homeassistant.components.cover import ( - ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA, + ENTITY_ID_FORMAT, CoverDevice, PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, SUPPORT_OPEN_TILT, SUPPORT_CLOSE_TILT, SUPPORT_STOP_TILT, SUPPORT_SET_TILT_POSITION, SUPPORT_OPEN, SUPPORT_CLOSE, SUPPORT_STOP, SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION) from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, MATCH_ALL, - CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, + CONF_VALUE_TEMPLATE, CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, CONF_ENTITY_PICTURE_TEMPLATE, CONF_OPTIMISTIC, STATE_OPEN, STATE_CLOSED) from homeassistant.exceptions import TemplateError @@ -53,6 +53,7 @@ COVER_SCHEMA = vol.Schema({ vol.Optional(CONF_TILT_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_OPTIMISTIC): cv.boolean, vol.Optional(CONF_TILT_OPTIMISTIC): cv.boolean, vol.Optional(POSITION_ACTION): cv.SCRIPT_SCHEMA, @@ -79,6 +80,7 @@ async def async_setup_platform(hass, config, async_add_entities, icon_template = device_config.get(CONF_ICON_TEMPLATE) entity_picture_template = device_config.get( CONF_ENTITY_PICTURE_TEMPLATE) + device_class = device_config.get(CONF_DEVICE_CLASS) open_action = device_config.get(OPEN_ACTION) close_action = device_config.get(CLOSE_ACTION) stop_action = device_config.get(STOP_ACTION) @@ -125,7 +127,7 @@ async def async_setup_platform(hass, config, async_add_entities, covers.append( CoverTemplate( hass, - device, friendly_name, state_template, + device, friendly_name, device_class, state_template, position_template, tilt_template, icon_template, entity_picture_template, open_action, close_action, stop_action, position_action, tilt_action, @@ -143,7 +145,8 @@ async def async_setup_platform(hass, config, async_add_entities, class CoverTemplate(CoverDevice): """Representation of a Template cover.""" - def __init__(self, hass, device_id, friendly_name, state_template, + def __init__(self, hass, device_id, friendly_name, device_class, + state_template, position_template, tilt_template, icon_template, entity_picture_template, open_action, close_action, stop_action, position_action, tilt_action, @@ -157,6 +160,7 @@ class CoverTemplate(CoverDevice): self._position_template = position_template self._tilt_template = tilt_template self._icon_template = icon_template + self._device_class = device_class self._entity_picture_template = entity_picture_template self._open_script = None if open_action is not None: @@ -249,6 +253,11 @@ class CoverTemplate(CoverDevice): """Return the entity picture to use in the frontend, if any.""" return self._entity_picture + @property + def device_class(self): + """Return the device class of the cover.""" + return self._device_class + @property def supported_features(self): """Flag supported features.""" diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 4d46882c9ea..703ef787ec7 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -756,3 +756,65 @@ async def test_entity_picture_template(hass, calls): state = hass.states.get('cover.test_template_cover') assert state.attributes['entity_picture'] == '/local/cover.png' + + +async def test_device_class(hass, calls): + """Test device class.""" + with assert_setup_component(1, 'cover'): + assert await setup.async_setup_component(hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ states.cover.test_state.state }}", + 'device_class': "door", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get('cover.test_template_cover') + assert state.attributes.get('device_class') == 'door' + + +async def test_invalid_device_class(hass, calls): + """Test device class.""" + with assert_setup_component(0, 'cover'): + assert await setup.async_setup_component(hass, 'cover', { + 'cover': { + 'platform': 'template', + 'covers': { + 'test_template_cover': { + 'value_template': + "{{ states.cover.test_state.state }}", + 'device_class': "barnacle_bill", + 'open_cover': { + 'service': 'cover.open_cover', + 'entity_id': 'cover.test_state' + }, + 'close_cover': { + 'service': 'cover.close_cover', + 'entity_id': 'cover.test_state' + }, + } + } + } + }) + + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get('cover.test_template_cover') + assert not state From 5aa9a1a7c2772999f925f5f02f717a4d64589d4f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Apr 2019 09:39:37 -0400 Subject: [PATCH 173/346] limit concurrent updates to avoid network flood (#23534) --- homeassistant/components/zha/light.py | 1 + homeassistant/components/zha/sensor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index ec840d5edb3..8395c2317e8 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -24,6 +24,7 @@ CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) +PARALLEL_UPDATES = 5 async def async_setup_platform(hass, config, async_add_entities, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b6ac70fa187..94f57ed9a0b 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -14,6 +14,7 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR) from .entity import ZhaEntity +PARALLEL_UPDATES = 5 _LOGGER = logging.getLogger(__name__) From e08f2ad18d825ecd51d38ff85fed086b2770111b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 29 Apr 2019 09:06:23 -0600 Subject: [PATCH 174/346] Add current disease data to IQVIA (#23052) * Add current and historical disease data to IQVIA * Added fetcher * Added disease sensor mapping * Changed incorrect key * Removed other extraneous const usage --- homeassistant/components/iqvia/__init__.py | 8 +++++++- homeassistant/components/iqvia/const.py | 5 ++++- homeassistant/components/iqvia/sensor.py | 10 +++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 3cf80a9d75c..978d84c65c6 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -23,7 +23,7 @@ from .const import ( TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW, - TYPE_DISEASE_FORECAST) + TYPE_DISEASE_FORECAST, TYPE_DISEASE_INDEX, TYPE_DISEASE_TODAY) _LOGGER = logging.getLogger(__name__) @@ -43,6 +43,7 @@ FETCHER_MAPPING = { (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): ( TYPE_ASTHMA_INDEX,), (TYPE_DISEASE_FORECAST,): (TYPE_DISEASE_FORECAST,), + (TYPE_DISEASE_TODAY,): (TYPE_DISEASE_INDEX,), } @@ -114,6 +115,8 @@ class IQVIAData: self.fetchers.register(TYPE_ASTHMA_INDEX)(self._client.asthma.current) self.fetchers.register(TYPE_DISEASE_FORECAST)( self._client.disease.extended) + self.fetchers.register(TYPE_DISEASE_INDEX)( + self._client.disease.current) async def async_update(self): """Update IQVIA data.""" @@ -170,6 +173,9 @@ class IQVIAEntity(Entity): if self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): return self._iqvia.data.get(TYPE_ASTHMA_INDEX) is not None + if self._type == TYPE_DISEASE_TODAY: + return self._iqvia.data.get(TYPE_DISEASE_INDEX) is not None + return self._iqvia.data.get(self._type) is not None @property diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index af8e2861a02..025fa8a9505 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -16,6 +16,8 @@ TYPE_ASTHMA_INDEX = 'asthma_index' TYPE_ASTHMA_TODAY = 'asthma_index_today' TYPE_ASTHMA_TOMORROW = 'asthma_index_tomorrow' TYPE_DISEASE_FORECAST = 'disease_average_forecasted' +TYPE_DISEASE_INDEX = 'disease_index' +TYPE_DISEASE_TODAY = 'disease_index_today' SENSORS = { TYPE_ALLERGY_FORECAST: ('Allergy Index: Forecasted Average', 'mdi:flower'), @@ -24,5 +26,6 @@ SENSORS = { TYPE_ASTHMA_TODAY: ('Asthma Index: Today', 'mdi:flower'), TYPE_ASTHMA_TOMORROW: ('Asthma Index: Tomorrow', 'mdi:flower'), TYPE_ASTHMA_FORECAST: ('Asthma Index: Forecasted Average', 'mdi:flower'), - TYPE_DISEASE_FORECAST: ('Cold & Flu: Forecasted Average', 'mdi:snowflake') + TYPE_DISEASE_FORECAST: ('Cold & Flu: Forecasted Average', 'mdi:snowflake'), + TYPE_DISEASE_TODAY: ('Cold & Flu Index: Today', 'mdi:pill'), } diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index f7e899b5d7d..b0b09c3f977 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -8,7 +8,8 @@ from homeassistant.components.iqvia import ( DATA_CLIENT, DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, TYPE_ASTHMA_INDEX, TYPE_ASTHMA_TODAY, - TYPE_ASTHMA_TOMORROW, TYPE_DISEASE_FORECAST, IQVIAEntity) + TYPE_ASTHMA_TOMORROW, TYPE_DISEASE_FORECAST, TYPE_DISEASE_INDEX, + TYPE_DISEASE_TODAY, IQVIAEntity) from homeassistant.const import ATTR_STATE _LOGGER = logging.getLogger(__name__) @@ -64,6 +65,7 @@ async def async_setup_platform( TYPE_ASTHMA_TODAY: IndexSensor, TYPE_ASTHMA_TOMORROW: IndexSensor, TYPE_DISEASE_FORECAST: ForecastSensor, + TYPE_DISEASE_TODAY: IndexSensor, } sensors = [] @@ -139,6 +141,8 @@ class IndexSensor(IQVIAEntity): data = self._iqvia.data[TYPE_ALLERGY_INDEX].get('Location') elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): data = self._iqvia.data[TYPE_ASTHMA_INDEX].get('Location') + elif self._type == TYPE_DISEASE_TODAY: + data = self._iqvia.data[TYPE_DISEASE_INDEX].get('Location') if not data: return @@ -177,5 +181,9 @@ class IndexSensor(IQVIAEntity): '{0}_{1}'.format(ATTR_ALLERGEN_AMOUNT, index): attrs['PPM'], }) + elif self._type == TYPE_DISEASE_TODAY: + for attrs in period['Triggers']: + self._attrs['{0}_index'.format( + attrs['Name'].lower())] = attrs['Index'] self._state = period['Index'] From 75f53b2799ce68caef9fda40f37dcb451e9c0737 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Apr 2019 11:35:19 -0400 Subject: [PATCH 175/346] Allow direct binding via ZHA for the ZLL profile (#23536) * allow binding for zll profile * update check - review comment --- homeassistant/components/zha/core/device.py | 11 +++++++---- homeassistant/components/zha/core/helpers.py | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 74e3c7bcc46..1a619dff981 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -321,15 +321,18 @@ class ZHADevice: } @callback - def async_get_zha_clusters(self): - """Get zigbee home automation clusters for this device.""" - from zigpy.profiles.zha import PROFILE_ID + def async_get_std_clusters(self): + """Get ZHA and ZLL clusters for this device.""" + from zigpy.profiles import zha, zll return { ep_id: { IN: endpoint.in_clusters, OUT: endpoint.out_clusters } for (ep_id, endpoint) in self._zigpy_device.endpoints.items() - if ep_id != 0 and endpoint.profile_id == PROFILE_ID + if ep_id != 0 and endpoint.profile_id in ( + zha.PROFILE_ID, + zll.PROFILE_ID + ) } @callback diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index ef7c2df6ce0..ed9f3e9c86a 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -170,8 +170,8 @@ def get_attr_id_by_name(cluster, attr_name): async def get_matched_clusters(source_zha_device, target_zha_device): """Get matched input/output cluster pairs for 2 devices.""" - source_clusters = source_zha_device.async_get_zha_clusters() - target_clusters = target_zha_device.async_get_zha_clusters() + source_clusters = source_zha_device.async_get_std_clusters() + target_clusters = target_zha_device.async_get_std_clusters() clusters_to_bind = [] for endpoint_id in source_clusters: @@ -193,8 +193,8 @@ async def get_matched_clusters(source_zha_device, target_zha_device): @callback def async_is_bindable_target(source_zha_device, target_zha_device): """Determine if target is bindable to source.""" - source_clusters = source_zha_device.async_get_zha_clusters() - target_clusters = target_zha_device.async_get_zha_clusters() + source_clusters = source_zha_device.async_get_std_clusters() + target_clusters = target_zha_device.async_get_std_clusters() bindables = set(BINDABLE_CLUSTERS) for endpoint_id in source_clusters: From 84f778d23cd275448c208597f15561e725ac1c85 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Apr 2019 18:53:22 +0200 Subject: [PATCH 176/346] Improve logging of exceptions in async_create_task (#22689) * Improve logging of exceptions in async_create_task * Move wrapping+logging to util.logging * Minor refactor, fix typing * Add test * Remove useless @wraps, fix confusing parameter name * Review comment --- homeassistant/components/cast/media_player.py | 21 +++++---- homeassistant/util/logging.py | 44 ++++++++++++++++++- tests/util/test_logging.py | 13 ++++++ 3 files changed, 68 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 653f3b5aed2..ee10f06c985 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -24,6 +24,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, dispatcher_send) from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util +from homeassistant.util.logging import async_create_catching_coro from . import DOMAIN as CAST_DOMAIN @@ -522,8 +523,8 @@ class CastDevice(MediaPlayerDevice): if _is_matching_dynamic_group(self._cast_info, discover): _LOGGER.debug("Discovered matching dynamic group: %s", discover) - self.hass.async_create_task( - self.async_set_dynamic_group(discover)) + self.hass.async_create_task(async_create_catching_coro( + self.async_set_dynamic_group(discover))) return if self._cast_info.uuid != discover.uuid: @@ -536,7 +537,8 @@ class CastDevice(MediaPlayerDevice): self._cast_info.host, self._cast_info.port) return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_create_task(self.async_set_cast_info(discover)) + self.hass.async_create_task(async_create_catching_coro( + self.async_set_cast_info(discover))) def async_cast_removed(discover: ChromecastInfo): """Handle removal of Chromecast.""" @@ -546,13 +548,15 @@ class CastDevice(MediaPlayerDevice): if (self._dynamic_group_cast_info is not None and self._dynamic_group_cast_info.uuid == discover.uuid): _LOGGER.debug("Removed matching dynamic group: %s", discover) - self.hass.async_create_task(self.async_del_dynamic_group()) + self.hass.async_create_task(async_create_catching_coro( + self.async_del_dynamic_group())) return if self._cast_info.uuid != discover.uuid: # Removed is not our device. return _LOGGER.debug("Removed chromecast with same UUID: %s", discover) - self.hass.async_create_task(self.async_del_cast_info(discover)) + self.hass.async_create_task(async_create_catching_coro( + self.async_del_cast_info(discover))) async def async_stop(event): """Disconnect socket on Home Assistant stop.""" @@ -565,14 +569,15 @@ class CastDevice(MediaPlayerDevice): self.hass, SIGNAL_CAST_REMOVED, async_cast_removed) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) - self.hass.async_create_task(self.async_set_cast_info(self._cast_info)) + self.hass.async_create_task(async_create_catching_coro( + self.async_set_cast_info(self._cast_info))) for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]: if _is_matching_dynamic_group(self._cast_info, info): _LOGGER.debug("[%s %s (%s:%s)] Found dynamic group: %s", self.entity_id, self._cast_info.friendly_name, self._cast_info.host, self._cast_info.port, info) - self.hass.async_create_task( - self.async_set_dynamic_group(info)) + self.hass.async_create_task(async_create_catching_coro( + self.async_set_dynamic_group(info))) break async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 214d9417e2a..317a30d9d56 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -6,7 +6,7 @@ import inspect import logging import threading import traceback -from typing import Any, Callable, Optional +from typing import Any, Callable, Coroutine, Optional from .async_ import run_coroutine_threadsafe @@ -130,7 +130,7 @@ def catch_log_exception( func: Callable[..., Any], format_err: Callable[..., Any], *args: Any) -> Callable[[], None]: - """Decorate an callback to catch and log exceptions.""" + """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # Do not print the wrapper in the traceback @@ -164,3 +164,43 @@ def catch_log_exception( log_exception(*args) wrapper_func = wrapper return wrapper_func + + +def catch_log_coro_exception( + target: Coroutine[Any, Any, Any], + format_err: Callable[..., Any], + *args: Any) -> Coroutine[Any, Any, Any]: + """Decorate a coroutine to catch and log exceptions.""" + async def coro_wrapper(*args: Any) -> Any: + """Catch and log exception.""" + try: + return await target + except Exception: # pylint: disable=broad-except + module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ + # Do not print the wrapper in the traceback + frames = len(inspect.trace()) - 1 + exc_msg = traceback.format_exc(-frames) + friendly_msg = format_err(*args) + logging.getLogger(module_name).error('%s\n%s', + friendly_msg, exc_msg) + return None + return coro_wrapper() + + +def async_create_catching_coro( + target: Coroutine) -> Coroutine: + """Wrap a coroutine to catch and log exceptions. + + The exception will be logged together with a stacktrace of where the + coroutine was wrapped. + + target: target coroutine. + """ + trace = traceback.extract_stack() + wrapped_target = catch_log_coro_exception( + target, lambda *args: + "Exception in {} called from\n {}".format( + target.__name__, # type: ignore + "".join(traceback.format_list(trace[:-1])))) + + return wrapped_target diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index c67b2aea448..92a06587fda 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -65,3 +65,16 @@ def test_async_handler_thread_log(loop): assert queue.get_nowait() == log_record assert queue.empty() + + +async def test_async_create_catching_coro(hass, caplog): + """Test exception logging of wrapped coroutine.""" + async def job(): + raise Exception('This is a bad coroutine') + pass + + hass.async_create_task(logging_util.async_create_catching_coro(job())) + await hass.async_block_till_done() + assert 'This is a bad coroutine' in caplog.text + assert ('hass.async_create_task(' + 'logging_util.async_create_catching_coro(job()))' in caplog.text) From 5e3e7304960a53b848e40cc377a0c2e9b3dadb35 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 29 Apr 2019 09:54:42 -0700 Subject: [PATCH 177/346] Install requirements when checking config (#23500) * Install requirements when checking config * PyLint --- homeassistant/scripts/check_config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 4bdda85bc07..5fe4e95a480 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -11,7 +11,7 @@ from unittest.mock import patch import attr import voluptuous as vol -from homeassistant import bootstrap, core, loader +from homeassistant import bootstrap, core, loader, requirements from homeassistant.config import ( get_default_config_dir, CONF_CORE, CORE_CONFIG_SCHEMA, CONF_PACKAGES, merge_packages_config, _format_config_error, @@ -344,6 +344,13 @@ async def check_ha_config_file(hass): result.add_error("Component not found: {}".format(domain)) continue + if (not hass.config.skip_pip and integration.requirements and + not await requirements.async_process_requirements( + hass, integration.domain, integration.requirements)): + result.add_error("Unable to install all requirements: {}".format( + ', '.join(integration.requirements))) + continue + if hasattr(component, 'CONFIG_SCHEMA'): try: config = component.CONFIG_SCHEMA(config) From 4c4f0e38d42e92b979d7b2e6e5106b62582f3340 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 29 Apr 2019 22:40:55 +0200 Subject: [PATCH 178/346] Fix media_player alexa power control bug (#23537) --- homeassistant/components/alexa/smart_home.py | 6 ++++- tests/components/alexa/test_smart_home.py | 26 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 21a65b1ede1..184aee9a440 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -911,13 +911,17 @@ class _MediaPlayerCapabilities(_AlexaEntity): return [_DisplayCategory.TV] def interfaces(self): - yield _AlexaPowerController(self.entity) yield _AlexaEndpointHealth(self.hass, self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.const.SUPPORT_VOLUME_SET: yield _AlexaSpeaker(self.entity) + power_features = (media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF) + if supported & power_features: + yield _AlexaPowerController(self.entity) + step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE | media_player.const.SUPPORT_VOLUME_STEP) if supported & step_volume_features: diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 924a568dea2..20b4495cd1a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -574,6 +574,32 @@ async def test_media_player(hass): payload={'volumeSteps': -20}) +async def test_media_player_power(hass): + """Test media player discovery with mapped on/off.""" + device = ( + 'media_player.test', + 'off', { + 'friendly_name': "Test media player", + 'supported_features': 0xfa3f, + 'volume_level': 0.75 + } + ) + appliance = await discovery_test(device, hass) + + assert appliance['endpointId'] == 'media_player#test' + assert appliance['displayCategories'][0] == "TV" + assert appliance['friendlyName'] == "Test media player" + + assert_endpoint_capabilities( + appliance, + 'Alexa.InputController', + 'Alexa.Speaker', + 'Alexa.StepSpeaker', + 'Alexa.PlaybackController', + 'Alexa.EndpointHealth', + ) + + async def test_alert(hass): """Test alert discovery.""" device = ('alert.test', 'off', {'friendly_name': "Test alert"}) From 8d775caaaca1502d04ad9f8378df8b3c5d9c6e43 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 29 Apr 2019 22:45:53 +0200 Subject: [PATCH 179/346] Always print invalid configuration data (#21972) * Always print invalide configuration data * Print offending data as yaml * Revert "Print offending data as yaml" This reverts commit 01721a21a9ff918ed2c8595151ebfe55eb2f7d36. * Do not print sensitive data * Print MQTT topic * Add line break * Review comments * review comments --- homeassistant/components/mqtt/discovery.py | 12 +++- homeassistant/helpers/config_validation.py | 68 +++++++++++++++------- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 4c1427d7e15..d9efd9123e7 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -210,6 +210,12 @@ def clear_discovery_hash(hass, discovery_hash): del hass.data[ALREADY_DISCOVERED][discovery_hash] +class MQTTConfig(dict): + """Dummy class to allow adding attributes.""" + + pass + + async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, config_entry=None) -> bool: """Initialize of MQTT Discovery.""" @@ -236,7 +242,7 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, object_id, payload) return - payload = dict(payload) + payload = MQTTConfig(payload) for key in list(payload.keys()): abbreviated_key = key @@ -264,6 +270,10 @@ async def async_start(hass: HomeAssistantType, discovery_topic, hass_config, discovery_hash = (component, discovery_id) if payload: + # Attach MQTT topic to the payload, used for debug prints + setattr(payload, '__configuration_source__', + "MQTT (topic: '{}')".format(topic)) + if CONF_PLATFORM in payload and 'schema' not in payload: platform = payload[CONF_PLATFORM] if (component in DEPRECATED_PLATFORM_TO_SCHEMA and diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index a954d01856e..1f139704e5f 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,5 +1,6 @@ """Helpers for config validation using voluptuous.""" import inspect +import json import logging import os import re @@ -15,11 +16,11 @@ from pkg_resources import parse_version import homeassistant.util.dt as dt_util from homeassistant.const import ( - CONF_PLATFORM, CONF_SCAN_INTERVAL, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_ALIAS, CONF_ENTITY_ID, CONF_VALUE_TEMPLATE, WEEKDAYS, - CONF_CONDITION, CONF_BELOW, CONF_ABOVE, CONF_TIMEOUT, SUN_EVENT_SUNSET, - SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - ENTITY_MATCH_ALL, CONF_ENTITY_NAMESPACE, __version__) + CONF_ABOVE, CONF_ALIAS, CONF_BELOW, CONF_CONDITION, CONF_ENTITY_ID, + CONF_ENTITY_NAMESPACE, CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, + CONF_TIMEOUT, ENTITY_MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, + TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, __version__) from homeassistant.core import valid_entity_id, split_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers import template as template_helper @@ -677,26 +678,53 @@ class HASchema(vol.Schema): self.extra = vol.PREVENT_EXTRA # This is a legacy config, print warning - extra_key_errs = [err for err in orig_err.errors + extra_key_errs = [err.path[-1] for err in orig_err.errors if err.error_message == 'extra keys not allowed'] - if extra_key_errs: - msg = "Your configuration contains extra keys " \ - "that the platform does not support.\n" \ - "Please remove " - submsg = ', '.join('[{}]'.format(err.path[-1]) for err in - extra_key_errs) - submsg += '. ' - if hasattr(data, '__config_file__'): - submsg += " (See {}, line {}). ".format( - data.__config_file__, data.__line__) - msg += submsg - logging.getLogger(__name__).warning(msg) - INVALID_EXTRA_KEYS_FOUND.append(submsg) - else: + + if not extra_key_errs: # This should not happen (all errors should be extra key # errors). Let's raise the original error anyway. raise orig_err + WHITELIST = [ + re.compile(CONF_NAME), + re.compile(CONF_PLATFORM), + re.compile('.*_topic'), + ] + + msg = "Your configuration contains extra keys " \ + "that the platform does not support.\n" \ + "Please remove " + submsg = ', '.join('[{}]'.format(err) for err in + extra_key_errs) + submsg += '. ' + + # Add file+line information, if available + if hasattr(data, '__config_file__'): + submsg += " (See {}, line {}). ".format( + data.__config_file__, data.__line__) + + # Add configuration source information, if available + if hasattr(data, '__configuration_source__'): + submsg += "\nConfiguration source: {}. ".format( + data.__configuration_source__) + redacted_data = {} + + # Print configuration causing the error, but filter any potentially + # sensitive data + for k, v in data.items(): + if (any(regex.match(k) for regex in WHITELIST) or + k in extra_key_errs): + redacted_data[k] = v + else: + redacted_data[k] = '' + submsg += "\nOffending data: {}".format( + json.dumps(redacted_data)) + + msg += submsg + logging.getLogger(__name__).warning(msg) + INVALID_EXTRA_KEYS_FOUND.append(submsg) + # Return legacy validated config return validated From 2aee31ec6af88a1932f60ac26360af77cc9ea62a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 29 Apr 2019 19:31:27 -0400 Subject: [PATCH 180/346] Don't use zigpy profiles for ZHA entities (#22844) * don't use zigpy profiles * use sets as they're the correct structure to use --- .../components/zha/core/discovery.py | 22 ++-- .../components/zha/core/registries.py | 110 ++++++++++-------- 2 files changed, 73 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index f5bd6ee99f2..d364c16536b 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -45,7 +45,7 @@ def async_process_endpoint( return component = None - profile_clusters = ([], []) + profile_clusters = [] device_key = "{}-{}".format(device.ieee, endpoint_id) node_config = {} if CONF_DEVICE_CONFIG in config: @@ -54,16 +54,17 @@ def async_process_endpoint( ) if endpoint.profile_id in zigpy.profiles.PROFILES: - profile = zigpy.profiles.PROFILES[endpoint.profile_id] if DEVICE_CLASS.get(endpoint.profile_id, {}).get( endpoint.device_type, None): - profile_clusters = profile.CLUSTERS[endpoint.device_type] profile_info = DEVICE_CLASS[endpoint.profile_id] component = profile_info[endpoint.device_type] + if component and component in COMPONENT_CLUSTERS: + profile_clusters = COMPONENT_CLUSTERS[component] if ha_const.CONF_TYPE in node_config: component = node_config[ha_const.CONF_TYPE] - profile_clusters = COMPONENT_CLUSTERS[component] + if component and component in COMPONENT_CLUSTERS: + profile_clusters = COMPONENT_CLUSTERS[component] if component and component in COMPONENTS: profile_match = _async_handle_profile_match( @@ -118,10 +119,10 @@ def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device, component, device_key, is_new_join): """Dispatch a profile match to the appropriate HA component.""" in_clusters = [endpoint.in_clusters[c] - for c in profile_clusters[0] + for c in profile_clusters if c in endpoint.in_clusters] out_clusters = [endpoint.out_clusters[c] - for c in profile_clusters[1] + for c in profile_clusters if c in endpoint.out_clusters] channels = [] @@ -143,10 +144,7 @@ def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device, if component == 'binary_sensor': discovery_info.update({SENSOR_TYPE: UNKNOWN}) - cluster_ids = [] - cluster_ids.extend(profile_clusters[0]) - cluster_ids.extend(profile_clusters[1]) - for cluster_id in cluster_ids: + for cluster_id in profile_clusters: if cluster_id in BINARY_SENSOR_TYPES: discovery_info.update({ SENSOR_TYPE: BINARY_SENSOR_TYPES.get( @@ -174,7 +172,7 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device, is_new_join, )) - if cluster.cluster_id not in profile_clusters[0]: + if cluster.cluster_id not in profile_clusters: cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, @@ -185,7 +183,7 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device, )) for cluster in endpoint.out_clusters.values(): - if cluster.cluster_id not in profile_clusters[1]: + if cluster.cluster_id not in profile_clusters: cluster_match_results.append(_async_handle_single_cluster_match( hass, zha_device, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 3cbd31aa304..2da0adb784a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -5,6 +5,12 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.components.switch import DOMAIN as SWITCH + from .const import ( HUMIDITY, TEMPERATURE, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, @@ -25,10 +31,17 @@ RADIO_TYPES = {} BINARY_SENSOR_TYPES = {} CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} -COMPONENT_CLUSTERS = {} EVENT_RELAY_CLUSTERS = [] NO_SENSOR_CLUSTERS = [] BINDABLE_CLUSTERS = [] +BINARY_SENSOR_CLUSTERS = set() +LIGHT_CLUSTERS = set() +SWITCH_CLUSTERS = set() +COMPONENT_CLUSTERS = { + BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, + LIGHT: LIGHT_CLUSTERS, + SWITCH: SWITCH_CLUSTERS +} def establish_device_mappings(): @@ -38,7 +51,7 @@ def establish_device_mappings(): in a function. """ from zigpy import zcl - from zigpy.profiles import PROFILES, zha, zll + from zigpy.profiles import zha, zll if zha.PROFILE_ID not in DEVICE_CLASS: DEVICE_CLASS[zha.PROFILE_ID] = {} @@ -97,53 +110,53 @@ def establish_device_mappings(): BINDABLE_CLUSTERS.append(zcl.clusters.lighting.Color.cluster_id) DEVICE_CLASS[zha.PROFILE_ID].update({ - 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.LEVEL_CONTROLLABLE_OUTPUT: 'light', - 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', + 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.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + 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].update({ - zll.DeviceType.ON_OFF_LIGHT: 'light', - zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch', - zll.DeviceType.DIMMABLE_LIGHT: 'light', - zll.DeviceType.DIMMABLE_PLUGIN_UNIT: 'light', - 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', + zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + 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_INPUT_CLUSTER_DEVICE_CLASS.update({ - zcl.clusters.general.OnOff: 'switch', - zcl.clusters.measurement.RelativeHumidity: 'sensor', + zcl.clusters.general.OnOff: SWITCH, + zcl.clusters.measurement.RelativeHumidity: SENSOR, # this works for now but if we hit conflicts we can break it out to # a different dict that is keyed by manufacturer - SMARTTHINGS_HUMIDITY_CLUSTER: 'sensor', - zcl.clusters.measurement.TemperatureMeasurement: 'sensor', - 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.measurement.OccupancySensing: 'binary_sensor', - zcl.clusters.hvac.Fan: 'fan', - SMARTTHINGS_ACCELERATION_CLUSTER: 'binary_sensor', + SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, + zcl.clusters.measurement.TemperatureMeasurement: SENSOR, + 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.measurement.OccupancySensing: BINARY_SENSOR, + zcl.clusters.hvac.Fan: FAN, + SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, }) SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({ - zcl.clusters.general.OnOff: 'binary_sensor', + zcl.clusters.general.OnOff: BINARY_SENSOR, }) SENSOR_TYPES.update({ @@ -269,12 +282,15 @@ def establish_device_mappings(): }], }) - # A map of hass components to all Zigbee clusters it could use - for profile_id, classes in DEVICE_CLASS.items(): - profile = PROFILES[profile_id] - for device_type, component in classes.items(): - if component not in COMPONENT_CLUSTERS: - COMPONENT_CLUSTERS[component] = (set(), set()) - clusters = profile.CLUSTERS[device_type] - COMPONENT_CLUSTERS[component][0].update(clusters[0]) - COMPONENT_CLUSTERS[component][1].update(clusters[1]) + BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) + BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) + BINARY_SENSOR_CLUSTERS.add(zcl.clusters.security.IasZone.cluster_id) + BINARY_SENSOR_CLUSTERS.add( + zcl.clusters.measurement.OccupancySensing.cluster_id) + BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) + + LIGHT_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) + LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) + LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id) + + SWITCH_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) From b4e2a0ef84e9bf011b90b3eb66645c2ccb0fd692 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 29 Apr 2019 20:02:54 -0400 Subject: [PATCH 181/346] Add HomeKit low battery threshold config (#23363) --- .../components/homekit/accessories.py | 9 ++-- homeassistant/components/homekit/const.py | 2 + homeassistant/components/homekit/util.py | 5 +- tests/components/homekit/test_accessories.py | 22 +++++--- tests/components/homekit/test_util.py | 50 +++++++++++++------ 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 8b0e70f616e..a6d7c3f642e 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -19,8 +19,8 @@ from homeassistant.util import dt as dt_util from .const import ( ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_SERIAL_NUMBER, CHAR_BATTERY_LEVEL, CHAR_CHARGING_STATE, CHAR_STATUS_LOW_BATTERY, - CONF_LINKED_BATTERY_SENSOR, DEBOUNCE_TIMEOUT, EVENT_HOMEKIT_CHANGED, - MANUFACTURER, SERV_BATTERY_SERVICE) + CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, DEBOUNCE_TIMEOUT, + EVENT_HOMEKIT_CHANGED, MANUFACTURER, SERV_BATTERY_SERVICE) from .util import convert_to_float, dismiss_setup_message, show_setup_message _LOGGER = logging.getLogger(__name__) @@ -73,6 +73,8 @@ class HomeAccessory(Accessory): self._support_battery_charging = True self.linked_battery_sensor = \ self.config.get(CONF_LINKED_BATTERY_SENSOR) + self.low_battery_threshold = \ + self.config.get(CONF_LOW_BATTERY_THRESHOLD) """Add battery service if available""" battery_found = self.hass.states.get(self.entity_id).attributes \ @@ -147,7 +149,8 @@ class HomeAccessory(Accessory): if battery_level is None: return self._char_battery.set_value(battery_level) - self._char_low_battery.set_value(battery_level < 20) + self._char_low_battery.set_value( + battery_level < self.low_battery_threshold) _LOGGER.debug('%s: Updated battery level to %d', self.entity_id, battery_level) if not self._support_battery_charging: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 0a2b7a0fd5d..a88cd7fc430 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -16,10 +16,12 @@ CONF_FEATURE = 'feature' CONF_FEATURE_LIST = 'feature_list' CONF_FILTER = 'filter' CONF_LINKED_BATTERY_SENSOR = 'linked_battery_sensor' +CONF_LOW_BATTERY_THRESHOLD = 'low_battery_threshold' CONF_SAFE_MODE = 'safe_mode' # #### Config Defaults #### DEFAULT_AUTO_START = True +DEFAULT_LOW_BATTERY_THRESHOLD = 20 DEFAULT_PORT = 51827 DEFAULT_SAFE_MODE = False diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 1bf57f1b1f9..b3c90ae6cbe 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -13,7 +13,8 @@ import homeassistant.util.temperature as temp_util from .const import ( CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, - FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + CONF_LOW_BATTERY_THRESHOLD, DEFAULT_LOW_BATTERY_THRESHOLD, FEATURE_ON_OFF, + FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) @@ -23,6 +24,8 @@ _LOGGER = logging.getLogger(__name__) BASIC_INFO_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LINKED_BATTERY_SENSOR): cv.entity_domain(sensor.DOMAIN), + vol.Optional(CONF_LOW_BATTERY_THRESHOLD, + default=DEFAULT_LOW_BATTERY_THRESHOLD): cv.positive_int, }) FEATURE_SCHEMA = BASIC_INFO_SCHEMA.extend({ diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index c9798f6302a..e87d38e27a4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -13,7 +13,8 @@ from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, ATTR_VALUE, BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, - CONF_LINKED_BATTERY_SENSOR, MANUFACTURER, SERV_ACCESSORY_INFO) + CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + DEFAULT_LOW_BATTERY_THRESHOLD, MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import ( __version__, ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_NOW, EVENT_TIME_CHANGED) @@ -106,7 +107,9 @@ async def test_battery_service(hass, hk_driver, caplog): hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 50}) await hass.async_block_till_done() - acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, + {CONF_LOW_BATTERY_THRESHOLD: + DEFAULT_LOW_BATTERY_THRESHOLD}) acc.update_state = lambda x: None assert acc._char_battery.value == 0 assert acc._char_low_battery.value == 0 @@ -136,7 +139,9 @@ async def test_battery_service(hass, hk_driver, caplog): ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True}) await hass.async_block_till_done() - acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, None) + acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, + {CONF_LOW_BATTERY_THRESHOLD: + DEFAULT_LOW_BATTERY_THRESHOLD}) acc.update_state = lambda x: None assert acc._char_battery.value == 0 assert acc._char_low_battery.value == 0 @@ -165,7 +170,9 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): await hass.async_block_till_done() acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, - {CONF_LINKED_BATTERY_SENSOR: linked_battery}) + {CONF_LINKED_BATTERY_SENSOR: linked_battery, + CONF_LOW_BATTERY_THRESHOLD: + DEFAULT_LOW_BATTERY_THRESHOLD}) acc.update_state = lambda x: None assert acc.linked_battery_sensor == linked_battery @@ -191,17 +198,18 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): assert acc._char_battery.value == 10 assert 'ERROR' not in caplog.text - # Test charging + # Test charging & low battery threshold hass.states.async_set(linked_battery, 20, {ATTR_BATTERY_CHARGING: True}) await hass.async_block_till_done() acc = HomeAccessory(hass, hk_driver, 'Battery Service', entity_id, 2, - {CONF_LINKED_BATTERY_SENSOR: linked_battery}) + {CONF_LINKED_BATTERY_SENSOR: linked_battery, + CONF_LOW_BATTERY_THRESHOLD: 50}) acc.update_state = lambda x: None await hass.async_add_job(acc.run) await hass.async_block_till_done() assert acc._char_battery.value == 20 - assert acc._char_low_battery.value == 0 + assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 1 hass.states.async_set(linked_battery, 100, {ATTR_BATTERY_CHARGING: False}) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 9ffcfe5c01e..f35194608c6 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,8 +4,9 @@ import voluptuous as vol from homeassistant.components.homekit.const import ( CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, - FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, HOMEKIT_NOTIFY_ID, TYPE_FAUCET, - TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, TYPE_SWITCH, TYPE_VALVE) + CONF_LOW_BATTERY_THRESHOLD, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, + HOMEKIT_NOTIFY_ID, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, TYPE_SPRINKLER, + TYPE_SWITCH, TYPE_VALVE) from homeassistant.components.homekit.util import ( HomeKitSpeedMapping, SpeedRange, convert_to_float, density_to_air_quality, dismiss_setup_message, show_setup_message, temperature_to_homekit, @@ -28,6 +29,9 @@ def test_validate_entity_config(): {'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: None}}, {'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: 'switch.demo'}}, + {'binary_sensor.demo': {CONF_LOW_BATTERY_THRESHOLD: + 'switch.demo'}}, + {'binary_sensor.demo': {CONF_LOW_BATTERY_THRESHOLD: -10}}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, {'demo.test': None}, {'demo.test': {CONF_NAME: None}}, {'media_player.test': {CONF_FEATURE_LIST: [ @@ -43,42 +47,56 @@ def test_validate_entity_config(): assert vec({}) == {} assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ - {'demo.test': {CONF_NAME: 'Name'}} + {'demo.test': {CONF_NAME: 'Name', CONF_LOW_BATTERY_THRESHOLD: 20}} assert vec({'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: 'sensor.demo_battery'}}) == \ {'binary_sensor.demo': {CONF_LINKED_BATTERY_SENSOR: - 'sensor.demo_battery'}} + 'sensor.demo_battery', + CONF_LOW_BATTERY_THRESHOLD: 20}} + assert vec({'binary_sensor.demo': {CONF_LOW_BATTERY_THRESHOLD: 50}}) == \ + {'binary_sensor.demo': {CONF_LOW_BATTERY_THRESHOLD: 50}} assert vec({'alarm_control_panel.demo': {}}) == \ - {'alarm_control_panel.demo': {ATTR_CODE: None}} + {'alarm_control_panel.demo': {ATTR_CODE: None, + CONF_LOW_BATTERY_THRESHOLD: 20}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ - {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} + {'alarm_control_panel.demo': {ATTR_CODE: '1234', + CONF_LOW_BATTERY_THRESHOLD: 20}} - assert vec({'lock.demo': {}}) == {'lock.demo': {ATTR_CODE: None}} + assert vec({'lock.demo': {}}) == \ + {'lock.demo': {ATTR_CODE: None, CONF_LOW_BATTERY_THRESHOLD: 20}} assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ - {'lock.demo': {ATTR_CODE: '1234'}} + {'lock.demo': {ATTR_CODE: '1234', CONF_LOW_BATTERY_THRESHOLD: 20}} assert vec({'media_player.demo': {}}) == \ - {'media_player.demo': {CONF_FEATURE_LIST: {}}} + {'media_player.demo': {CONF_FEATURE_LIST: {}, + CONF_LOW_BATTERY_THRESHOLD: 20}} 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: {}}}} + {FEATURE_ON_OFF: {}, FEATURE_PLAY_PAUSE: {}}, + CONF_LOW_BATTERY_THRESHOLD: 20}} assert vec({'switch.demo': {CONF_TYPE: TYPE_FAUCET}}) == \ - {'switch.demo': {CONF_TYPE: TYPE_FAUCET}} + {'switch.demo': {CONF_TYPE: TYPE_FAUCET, CONF_LOW_BATTERY_THRESHOLD: + 20}} assert vec({'switch.demo': {CONF_TYPE: TYPE_OUTLET}}) == \ - {'switch.demo': {CONF_TYPE: TYPE_OUTLET}} + {'switch.demo': {CONF_TYPE: TYPE_OUTLET, CONF_LOW_BATTERY_THRESHOLD: + 20}} assert vec({'switch.demo': {CONF_TYPE: TYPE_SHOWER}}) == \ - {'switch.demo': {CONF_TYPE: TYPE_SHOWER}} + {'switch.demo': {CONF_TYPE: TYPE_SHOWER, CONF_LOW_BATTERY_THRESHOLD: + 20}} assert vec({'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}}) == \ - {'switch.demo': {CONF_TYPE: TYPE_SPRINKLER}} + {'switch.demo': {CONF_TYPE: TYPE_SPRINKLER, CONF_LOW_BATTERY_THRESHOLD: + 20}} assert vec({'switch.demo': {CONF_TYPE: TYPE_SWITCH}}) == \ - {'switch.demo': {CONF_TYPE: TYPE_SWITCH}} + {'switch.demo': {CONF_TYPE: TYPE_SWITCH, CONF_LOW_BATTERY_THRESHOLD: + 20}} assert vec({'switch.demo': {CONF_TYPE: TYPE_VALVE}}) == \ - {'switch.demo': {CONF_TYPE: TYPE_VALVE}} + {'switch.demo': {CONF_TYPE: TYPE_VALVE, CONF_LOW_BATTERY_THRESHOLD: + 20}} def test_validate_media_player_features(): From 1d70005b01e27b4c11a09d186dc408ea1fc9fdd2 Mon Sep 17 00:00:00 2001 From: etheralm <8655564+etheralm@users.noreply.github.com> Date: Tue, 30 Apr 2019 02:24:05 +0200 Subject: [PATCH 182/346] Add sensor support for dyson 2018 models (#22578) fix check for already created entities remove hepa and carbon filter add AQI attribute initial commit fix check for already created entities remove hepa and carbon filter add AQI attribute add air quality component tests fix method call tests fix line lengths fix pylint issues fix docstrings revert fan related changes remove whitespace change fix fan update state test add for loop for platform initialization add requested changes to aiq platform change string concatenation to new style string formatting update air quality tests update air quality tests refactor sensor component changes fix pylint issues fix debug string in the air quality component replace failing tests for older devices fix line length fan tests remove dependencies const and move imports move back imports to methods remove whitespace from blank line --- homeassistant/components/dyson/__init__.py | 7 +- homeassistant/components/dyson/air_quality.py | 126 +++++++++++++++ homeassistant/components/dyson/fan.py | 3 +- homeassistant/components/dyson/sensor.py | 46 ++++-- tests/components/dyson/test_air_quality.py | 145 ++++++++++++++++++ tests/components/dyson/test_climate.py | 54 ++++--- tests/components/dyson/test_fan.py | 101 ++++-------- tests/components/dyson/test_init.py | 4 +- tests/components/dyson/test_sensor.py | 56 ++++++- 9 files changed, 430 insertions(+), 112 deletions(-) create mode 100644 homeassistant/components/dyson/air_quality.py create mode 100644 tests/components/dyson/test_air_quality.py diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index a857d6657fd..fdba263d4ca 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -16,6 +16,7 @@ CONF_RETRY = 'retry' DEFAULT_TIMEOUT = 5 DEFAULT_RETRY = 10 DYSON_DEVICES = 'dyson_devices' +DYSON_PLATFORMS = ['sensor', 'fan', 'vacuum', 'climate', 'air_quality'] DOMAIN = 'dyson' @@ -91,9 +92,7 @@ def setup(hass, config): # Start fan/sensors components if hass.data[DYSON_DEVICES]: _LOGGER.debug("Starting sensor/fan components") - discovery.load_platform(hass, "sensor", DOMAIN, {}, config) - discovery.load_platform(hass, "fan", DOMAIN, {}, config) - discovery.load_platform(hass, "vacuum", DOMAIN, {}, config) - discovery.load_platform(hass, "climate", DOMAIN, {}, config) + for platform in DYSON_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) return True diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py new file mode 100644 index 00000000000..238b8b6934d --- /dev/null +++ b/homeassistant/components/dyson/air_quality.py @@ -0,0 +1,126 @@ +"""Support for Dyson Pure Cool Air Quality Sensors.""" +import logging + +from homeassistant.components.air_quality import AirQualityEntity, DOMAIN +from . import DYSON_DEVICES + +ATTRIBUTION = 'Dyson purifier air quality sensor' + +_LOGGER = logging.getLogger(__name__) + +DYSON_AIQ_DEVICES = 'dyson_aiq_devices' + +ATTR_VOC = 'volatile_organic_compounds' + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Dyson Sensors.""" + from libpurecool.dyson_pure_cool import DysonPureCool + + if discovery_info is None: + return + + hass.data.setdefault(DYSON_AIQ_DEVICES, []) + + # Get Dyson Devices from parent component + device_ids = [device.unique_id for device in hass.data[DYSON_AIQ_DEVICES]] + for device in hass.data[DYSON_DEVICES]: + if isinstance(device, DysonPureCool) and \ + device.serial not in device_ids: + hass.data[DYSON_AIQ_DEVICES].append(DysonAirSensor(device)) + add_entities(hass.data[DYSON_AIQ_DEVICES]) + + +class DysonAirSensor(AirQualityEntity): + """Representation of a generic Dyson air quality sensor.""" + + def __init__(self, device): + """Create a new generic air quality Dyson sensor.""" + self._device = device + self._old_value = None + self._name = device.name + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self.hass.async_add_executor_job( + self._device.add_message_listener, self.on_message) + + def on_message(self, message): + """Handle new messages which are received from the fan.""" + from libpurecool.dyson_pure_state_v2 import \ + DysonEnvironmentalSensorV2State + + _LOGGER.debug('%s: Message received for %s device: %s', + DOMAIN, self.name, message) + if (self._old_value is None or + self._old_value != self._device.environmental_state) and \ + isinstance(message, DysonEnvironmentalSensorV2State): + self._old_value = self._device.environmental_state + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the Dyson sensor.""" + return self._name + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def air_quality_index(self): + """Return the Air Quality Index (AQI).""" + return max(self.particulate_matter_2_5, + self.particulate_matter_10, + self.nitrogen_dioxide, + self.volatile_organic_compounds) + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + if self._device.environmental_state: + return int(self._device.environmental_state.particulate_matter_25) + return None + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + if self._device.environmental_state: + return int(self._device.environmental_state.particulate_matter_10) + return None + + @property + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + if self._device.environmental_state: + return int(self._device.environmental_state.nitrogen_dioxide) + return None + + @property + def volatile_organic_compounds(self): + """Return the VOC (Volatile Organic Compounds) level.""" + if self._device.environmental_state: + return int(self._device. + environmental_state.volatile_organic_compounds) + return None + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return self._device.serial + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {} + + voc = self.volatile_organic_compounds + if voc is not None: + data[ATTR_VOC] = voc + return data diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 03a55f8abbe..65ff093d6d5 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -474,7 +474,8 @@ class DysonPureCoolDevice(FanEntity): FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_9.value: SPEED_HIGH} + FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_10.value: SPEED_HIGH} return speed_map[self._device.state.speed] diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 56c924d1a54..9cd1c915c57 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -3,7 +3,6 @@ import logging from homeassistant.const import STATE_OFF, TEMP_CELSIUS from homeassistant.helpers.entity import Entity - from . import DYSON_DEVICES SENSOR_UNITS = { @@ -21,26 +20,38 @@ SENSOR_ICONS = { 'temperature': 'mdi:thermometer', } +DYSON_SENSOR_DEVICES = 'dyson_sensor_devices' + _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dyson Sensors.""" - _LOGGER.debug("Creating new Dyson fans") - devices = [] - unit = hass.config.units.temperature_unit - # Get Dyson Devices from parent component - from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + from libpurecool.dyson_pure_cool import DysonPureCool - for device in [d for d in hass.data[DYSON_DEVICES] - if isinstance(d, DysonPureCoolLink) and - not isinstance(d, DysonPureCool)]: - devices.append(DysonFilterLifeSensor(device)) - devices.append(DysonDustSensor(device)) - devices.append(DysonHumiditySensor(device)) - devices.append(DysonTemperatureSensor(device, unit)) - devices.append(DysonAirQualitySensor(device)) + if discovery_info is None: + return + + hass.data.setdefault(DYSON_SENSOR_DEVICES, []) + unit = hass.config.units.temperature_unit + devices = hass.data[DYSON_SENSOR_DEVICES] + + # Get Dyson Devices from parent component + device_ids = [device.unique_id for device in + hass.data[DYSON_SENSOR_DEVICES]] + for device in hass.data[DYSON_DEVICES]: + if isinstance(device, DysonPureCool): + if '{}-{}'.format(device.serial, 'temperature') not in device_ids: + devices.append(DysonTemperatureSensor(device, unit)) + if '{}-{}'.format(device.serial, 'humidity') not in device_ids: + devices.append(DysonHumiditySensor(device)) + elif isinstance(device, DysonPureCoolLink): + devices.append(DysonFilterLifeSensor(device)) + devices.append(DysonDustSensor(device)) + devices.append(DysonHumiditySensor(device)) + devices.append(DysonTemperatureSensor(device, unit)) + devices.append(DysonAirQualitySensor(device)) add_entities(devices) @@ -56,7 +67,7 @@ class DysonSensor(Entity): async def async_added_to_hass(self): """Call when entity is added to hass.""" - self.hass.async_add_job( + self.hass.async_add_executor_job( self._device.add_message_listener, self.on_message) def on_message(self, message): @@ -88,6 +99,11 @@ class DysonSensor(Entity): """Return the icon for this sensor.""" return SENSOR_ICONS[self._sensor_type] + @property + def unique_id(self): + """Return the sensor's unique id.""" + return '{}-{}'.format(self._device.serial, self._sensor_type) + class DysonFilterLifeSensor(DysonSensor): """Representation of Dyson Filter Life sensor (in hours).""" diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py new file mode 100644 index 00000000000..ab068823d64 --- /dev/null +++ b/tests/components/dyson/test_air_quality.py @@ -0,0 +1,145 @@ +"""Test the Dyson air quality component.""" +import json +from unittest import mock + +import asynctest +from libpurecool.dyson_pure_cool import DysonPureCool +from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State + +import homeassistant.components.dyson.air_quality as dyson +from homeassistant.components import dyson as dyson_parent +from homeassistant.components.air_quality import DOMAIN as AIQ_DOMAIN, \ + ATTR_PM_2_5, ATTR_PM_10, ATTR_NO2 +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component + + +def _get_dyson_purecool_device(): + """Return a valid device as provided by the Dyson web services.""" + device = mock.Mock(spec=DysonPureCool) + device.serial = 'XX-XXXXX-XX' + device.name = 'Living room' + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.environmental_state.particulate_matter_25 = '0014' + device.environmental_state.particulate_matter_10 = '0025' + device.environmental_state.nitrogen_dioxide = '0042' + device.environmental_state.volatile_organic_compounds = '0035' + return device + + +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: 'email', + dyson_parent.CONF_PASSWORD: 'password', + dyson_parent.CONF_LANGUAGE: 'GB', + dyson_parent.CONF_DEVICES: [ + { + 'device_id': 'XX-XXXXX-XX', + 'device_ip': '192.168.0.1' + } + ] + }} + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_attributes(devices, login, hass): + """Test state attributes.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + fan_state = hass.states.get("air_quality.living_room") + attributes = fan_state.attributes + + assert fan_state.state == '14' + assert attributes[ATTR_PM_2_5] == 14 + assert attributes[ATTR_PM_10] == 25 + assert attributes[ATTR_NO2] == 42 + assert attributes[dyson.ATTR_VOC] == 35 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_update_state(devices, login, hass): + """Test state update.""" + device = devices.return_value[0] + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + event = { + "msg": "ENVIRONMENTAL-CURRENT-SENSOR-DATA", + "time": "2019-03-29T10:00:01.000Z", + "data": { + "pm10": "0080", + "p10r": "0151", + "hact": "0040", + "va10": "0055", + "p25r": "0161", + "noxl": "0069", + "pm25": "0035", + "sltm": "OFF", + "tact": "2960" + } + } + device.environmental_state = \ + DysonEnvironmentalSensorV2State(json.dumps(event)) + + callback = device.add_message_listener.call_args_list[2][0][0] + callback(device.environmental_state) + await hass.async_block_till_done() + fan_state = hass.states.get("air_quality.living_room") + attributes = fan_state.attributes + + assert fan_state.state == '35' + assert attributes[ATTR_PM_2_5] == 35 + assert attributes[ATTR_PM_10] == 80 + assert attributes[ATTR_NO2] == 69 + assert attributes[dyson.ATTR_VOC] == 55 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_component_setup_only_once(devices, login, hass): + """Test if entities are created only once.""" + config = _get_config() + await async_setup_component(hass, dyson_parent.DOMAIN, config) + await hass.async_block_till_done() + discovery.load_platform(hass, AIQ_DOMAIN, + dyson_parent.DOMAIN, {}, config) + await hass.async_block_till_done() + + assert len(hass.data[dyson.DYSON_AIQ_DEVICES]) == 1 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_without_discovery(devices, login, hass): + """Test if component correctly returns if discovery not set.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + add_entities_mock = mock.MagicMock() + + dyson.setup_platform(hass, None, add_entities_mock, None) + + assert add_entities_mock.call_count == 0 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_aiq_empty_environment_state(devices, login, hass): + """Test device with empty environmental state.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + device = hass.data[dyson.DYSON_AIQ_DEVICES][0] + device._device.environmental_state = None + + assert device.state is None + assert device.particulate_matter_2_5 is None + assert device.particulate_matter_10 is None + assert device.nitrogen_dioxide is None + assert device.volatile_organic_compounds is None diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 778b3bdad49..83ddbfed242 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -2,6 +2,7 @@ import unittest from unittest import mock +import asynctest from libpurecool.const import (FocusMode, HeatMode, HeatState, HeatTarget, TiltState) from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink @@ -10,7 +11,7 @@ from libpurecool.dyson_pure_state import DysonPureHotCoolState from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import climate as dyson from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant @@ -22,6 +23,25 @@ class MockDysonState(DysonPureHotCoolState): pass +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "GB", + dyson_parent.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + }, + { + "device_id": "YY-YYYYY-YY", + "device_ip": "192.168.0.2" + } + ] + }} + + def _get_device_with_no_state(): """Return a device with no state.""" device = mock.Mock(spec=DysonPureHotCoolLink) @@ -60,6 +80,7 @@ def _get_device_cool(): """Return a device with state of cooling.""" device = mock.Mock(spec=DysonPureHotCoolLink) device.name = "Device_name" + device.serial = "XX-XXXXX-XX" device.state.tilt = TiltState.TILT_FALSE.value device.state.focus_mode = FocusMode.FOCUS_OFF.value device.state.heat_target = HeatTarget.celsius(12) @@ -89,6 +110,7 @@ def _get_device_heat_on(): """Return a device with state of heating.""" device = mock.Mock(spec=DysonPureHotCoolLink) device.name = "Device_name" + device.serial = "YY-YYYYY-YY" device.state = mock.Mock() device.state.tilt = TiltState.TILT_FALSE.value device.state.focus_mode = FocusMode.FOCUS_ON.value @@ -111,24 +133,6 @@ class DysonTest(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_device_heat_on(), _get_device_cool()]) - @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) - def test_setup_component_with_parent_discovery(self, mocked_login, - mocked_devices): - """Test setup_component using discovery.""" - setup_component(self.hass, dyson_parent.DOMAIN, { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "US", - } - }) - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 2 - self.hass.block_till_done() - for m in mocked_devices.return_value: - assert m.add_message_listener.called - def test_setup_component_without_devices(self): """Test setup component with no devices.""" self.hass.data[dyson.DYSON_DEVICES] = [] @@ -357,3 +361,15 @@ class DysonTest(unittest.TestCase): device = _get_device_heat_on() entity = dyson.DysonPureHotCoolLinkDevice(device) assert entity.target_temperature == 23 + + +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_device_heat_on(), _get_device_cool()]) +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +async def test_setup_component_with_parent_discovery(mocked_login, + mocked_devices, hass): + """Test setup_component using discovery.""" + await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) + await hass.async_block_till_done() + + assert len(hass.data[dyson.DYSON_DEVICES]) == 2 diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 0a9469ae807..09622e4d36d 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -13,7 +13,7 @@ from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State import homeassistant.components.dyson.fan as dyson from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import DYSON_DEVICES -from homeassistant.components.fan import (DOMAIN, ATTR_SPEED, ATTR_SPEED_LIST, +from homeassistant.components.fan import (DOMAIN, ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SERVICE_OSCILLATE) @@ -21,7 +21,7 @@ from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID) from homeassistant.helpers import discovery -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant @@ -55,6 +55,21 @@ def _get_dyson_purecool_device(): return device +def _get_dyson_purecoollink_device(): + """Return a valid device as provided by the Dyson web services.""" + device = mock.Mock(spec=DysonPureCoolLink) + device.serial = "XX-XXXXX-XX" + device.name = "Living room" + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.state = mock.Mock() + device.state.oscillation = "ON" + device.state.fan_mode = "FAN" + device.state.speed = FanSpeed.FAN_SPEED_AUTO.value + device.state.night_mode = "OFF" + return device + + def _get_supported_speeds(): return [ int(FanSpeed.FAN_SPEED_1.value), @@ -173,45 +188,6 @@ class DysonTest(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() - @mock.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_device_on()]) - @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) - def test_get_state_attributes(self, mocked_login, mocked_devices): - """Test async added to hass.""" - setup_component(self.hass, dyson_parent.DOMAIN, { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "US", - } - }) - self.hass.block_till_done() - state = self.hass.states.get("{}.{}".format( - DOMAIN, - mocked_devices.return_value[0].name)) - - assert dyson.ATTR_NIGHT_MODE in state.attributes - assert dyson.ATTR_AUTO_MODE in state.attributes - assert ATTR_SPEED in state.attributes - assert ATTR_SPEED_LIST in state.attributes - assert ATTR_OSCILLATING in state.attributes - - @mock.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_device_on()]) - @mock.patch('libpurecool.dyson.DysonAccount.login', return_value=True) - def test_async_added_to_hass(self, mocked_login, mocked_devices): - """Test async added to hass.""" - setup_component(self.hass, dyson_parent.DOMAIN, { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "US", - } - }) - self.hass.block_till_done() - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_devices.return_value[0].add_message_listener.called - def test_dyson_set_speed(self): """Test set fan speed.""" device = _get_device_on() @@ -415,6 +391,22 @@ class DysonTest(unittest.TestCase): dyson_device.set_night_mode.assert_called_with(True) +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecoollink_device()]) +async def test_purecoollink_attributes(devices, login, hass): + """Test state attributes.""" + await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) + await hass.async_block_till_done() + fan_state = hass.states.get("fan.living_room") + attributes = fan_state.attributes + + assert fan_state.state == "on" + assert attributes[dyson.ATTR_NIGHT_MODE] is False + assert attributes[ATTR_SPEED] == FanSpeed.FAN_SPEED_AUTO.value + assert attributes[ATTR_OSCILLATING] is True + + @asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) @asynctest.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_purecool_device()]) @@ -670,31 +662,6 @@ async def test_purecool_set_timer(devices, login, hass): assert device.disable_sleep_timer.call_count == 1 -@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) -@asynctest.patch('libpurecool.dyson.DysonAccount.devices', - return_value=[_get_dyson_purecool_device()]) -async def test_purecool_attributes(devices, login, hass): - """Test state attributes.""" - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - fan_state = hass.states.get("fan.living_room") - attributes = fan_state.attributes - - assert fan_state.state == "on" - assert attributes[dyson.ATTR_NIGHT_MODE] is False - assert attributes[dyson.ATTR_AUTO_MODE] is True - assert attributes[dyson.ATTR_ANGLE_LOW] == 90 - assert attributes[dyson.ATTR_ANGLE_HIGH] == 180 - assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is True - assert attributes[dyson.ATTR_TIMER] == 60 - assert attributes[dyson.ATTR_HEPA_FILTER] == 90 - assert attributes[dyson.ATTR_CARBON_FILTER] == 80 - assert attributes[dyson.ATTR_DYSON_SPEED] == FanSpeed.FAN_SPEED_AUTO.value - assert attributes[ATTR_SPEED] == SPEED_MEDIUM - assert attributes[ATTR_OSCILLATING] is True - assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() - - @asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) @asynctest.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_purecool_device()]) @@ -713,7 +680,7 @@ async def test_purecool_update_state(devices, login, hass): "osau": "0095", "ancp": "CUST"}} device.state = DysonPureCoolV2State(json.dumps(event)) - callback = device.add_message_listener.call_args_list[0][0][0] + callback = device.add_message_listener.call_args_list[3][0][0] callback(device.state) await hass.async_block_till_done() fan_state = hass.states.get("fan.living_room") diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py index cc8c04a1559..9c1ea7ebabf 100644 --- a/tests/components/dyson/test_init.py +++ b/tests/components/dyson/test_init.py @@ -87,7 +87,7 @@ class DysonTest(unittest.TestCase): assert mocked_login.call_count == 1 assert mocked_devices.call_count == 1 assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_discovery.call_count == 4 + assert mocked_discovery.call_count == 5 @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) @@ -172,7 +172,7 @@ class DysonTest(unittest.TestCase): assert mocked_login.call_count == 1 assert mocked_devices.call_count == 1 assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_discovery.call_count == 4 + assert mocked_discovery.call_count == 5 @mock.patch('libpurecool.dyson.DysonAccount.devices', return_value=[_get_dyson_account_device_not_available()]) diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 67c34d4d180..d7b478776dc 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -2,17 +2,51 @@ import unittest from unittest import mock +import asynctest +from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from homeassistant.components import dyson as dyson_parent from homeassistant.components.dyson import sensor as dyson from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, \ STATE_OFF +from homeassistant.helpers import discovery +from homeassistant.setup import async_setup_component from tests.common import get_test_home_assistant +def _get_dyson_purecool_device(): + """Return a valid device provide by Dyson web services.""" + device = mock.Mock(spec=DysonPureCool) + device.serial = "XX-XXXXX-XX" + device.name = "Living room" + device.connect = mock.Mock(return_value=True) + device.auto_connect = mock.Mock(return_value=True) + device.environmental_state.humidity = 42 + device.environmental_state.temperature = 280 + device.state.hepa_filter_state = 90 + device.state.carbon_filter_state = 80 + return device + + +def _get_config(): + """Return a config dictionary.""" + return {dyson_parent.DOMAIN: { + dyson_parent.CONF_USERNAME: "email", + dyson_parent.CONF_PASSWORD: "password", + dyson_parent.CONF_LANGUAGE: "GB", + dyson_parent.CONF_DEVICES: [ + { + "device_id": "XX-XXXXX-XX", + "device_ip": "192.168.0.1" + } + ] + }} + + def _get_device_without_state(): """Return a valid device provide by Dyson web services.""" - device = mock.Mock() + device = mock.Mock(spec=DysonPureCoolLink) device.name = "Device_name" device.state = None device.environmental_state = None @@ -21,7 +55,7 @@ def _get_device_without_state(): def _get_with_state(): """Return a valid device with state values.""" - device = mock.Mock(spec=DysonPureCoolLink) + device = mock.Mock() device.name = "Device_name" device.state = mock.Mock() device.state.filter_life = 100 @@ -65,7 +99,7 @@ class DysonTest(unittest.TestCase): self.hass.data[dyson.DYSON_DEVICES] = [] add_entities = mock.MagicMock() dyson.setup_platform(self.hass, None, add_entities) - add_entities.assert_called_with([]) + add_entities.assert_not_called() def test_setup_component(self): """Test setup component with devices.""" @@ -80,7 +114,7 @@ class DysonTest(unittest.TestCase): device_fan = _get_device_without_state() device_non_fan = _get_with_state() self.hass.data[dyson.DYSON_DEVICES] = [device_fan, device_non_fan] - dyson.setup_platform(self.hass, None, _add_device) + dyson.setup_platform(self.hass, None, _add_device, mock.MagicMock()) def test_dyson_filter_life_sensor(self): """Test filter life sensor with no value.""" @@ -228,3 +262,17 @@ class DysonTest(unittest.TestCase): assert sensor.unit_of_measurement is None assert sensor.name == "Device_name AQI" assert sensor.entity_id == "sensor.dyson_1" + + +@asynctest.patch('libpurecool.dyson.DysonAccount.login', return_value=True) +@asynctest.patch('libpurecool.dyson.DysonAccount.devices', + return_value=[_get_dyson_purecool_device()]) +async def test_purecool_component_setup_only_once(devices, login, hass): + """Test if entities are created only once.""" + config = _get_config() + await async_setup_component(hass, dyson_parent.DOMAIN, config) + await hass.async_block_till_done() + discovery.load_platform(hass, "sensor", dyson_parent.DOMAIN, {}, config) + await hass.async_block_till_done() + + assert len(hass.data[dyson.DYSON_SENSOR_DEVICES]) == 2 From 5d5f073cffa1bcb93830300961eed66023a1898d Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 30 Apr 2019 08:02:45 +0200 Subject: [PATCH 183/346] Fix problem with using Traccar event (#23543) --- homeassistant/components/traccar/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index b3e2b2833c2..d2990e178ab 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -192,7 +192,7 @@ class TraccarScanner: if events is not None: for event in events: device_name = next(( - dev.get('name') for dev in self._api.devices() + dev.get('name') for dev in self._api.devices if dev.get('id') == event['deviceId']), None) self._hass.bus.async_fire( 'traccar_' + self._event_types.get(event["type"]), { From 7612703092710ae652fdeaf5bdf18157f8bcbe55 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 30 Apr 2019 13:44:48 +0100 Subject: [PATCH 184/346] Bump homekit_python to 0.14.0 (#23562) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3e447f08f4b..c1b923a5677 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "Homekit controller", "documentation": "https://www.home-assistant.io/components/homekit_controller", "requirements": [ - "homekit[IP]==0.13.0" + "homekit[IP]==0.14.0" ], "dependencies": ["configurator"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 0bd974f0de7..34155012f76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,7 +560,7 @@ home-assistant-frontend==20190427.0 homeassistant-pyozw==0.1.4 # homeassistant.components.homekit_controller -homekit[IP]==0.13.0 +homekit[IP]==0.14.0 # homeassistant.components.homematicip_cloud homematicip==0.10.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27550726f4a..e174bf65602 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -145,7 +145,7 @@ holidays==0.9.10 home-assistant-frontend==20190427.0 # homeassistant.components.homekit_controller -homekit[IP]==0.13.0 +homekit[IP]==0.14.0 # homeassistant.components.homematicip_cloud homematicip==0.10.7 From 6a6a99983307103b1d54f1742ad7fd6e478f10fb Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 30 Apr 2019 14:45:56 +0200 Subject: [PATCH 185/346] Upgrade pysonos to 0.0.12 (#23560) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 338d6454b28..5eac580313e 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "documentation": "https://www.home-assistant.io/components/sonos", "requirements": [ - "pysonos==0.0.11" + "pysonos==0.0.12" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 34155012f76..48cf6409dc0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1288,7 +1288,7 @@ pysmartthings==0.6.7 pysnmp==4.4.8 # homeassistant.components.sonos -pysonos==0.0.11 +pysonos==0.0.12 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e174bf65602..402647172e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -258,7 +258,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.7 # homeassistant.components.sonos -pysonos==0.0.11 +pysonos==0.0.12 # homeassistant.components.spc pyspcwebgw==0.4.0 From d71424f2852ca98fcd7a406c6a2fe9b93c031bbb Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 30 Apr 2019 10:40:52 -0400 Subject: [PATCH 186/346] Clean up ZHA discovery logic (#23563) * use domain constants from HA * cleanup endpoint processing in discovery * Whitespace. --- .../components/zha/core/discovery.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index d364c16536b..e81fa53020d 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -8,6 +8,8 @@ https://home-assistant.io/components/zha/ import logging from homeassistant import const as ha_const +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .channels import ( @@ -58,19 +60,18 @@ def async_process_endpoint( endpoint.device_type, None): profile_info = DEVICE_CLASS[endpoint.profile_id] component = profile_info[endpoint.device_type] - if component and component in COMPONENT_CLUSTERS: - profile_clusters = COMPONENT_CLUSTERS[component] if ha_const.CONF_TYPE in node_config: component = node_config[ha_const.CONF_TYPE] - if component and component in COMPONENT_CLUSTERS: - profile_clusters = COMPONENT_CLUSTERS[component] - if component and component in COMPONENTS: - profile_match = _async_handle_profile_match( - hass, endpoint, profile_clusters, zha_device, - component, device_key, is_new_join) - discovery_infos.append(profile_match) + if component and component in COMPONENTS and \ + component in COMPONENT_CLUSTERS: + profile_clusters = COMPONENT_CLUSTERS[component] + if profile_clusters: + profile_match = _async_handle_profile_match( + hass, endpoint, profile_clusters, zha_device, + component, device_key, is_new_join) + discovery_infos.append(profile_match) discovery_infos.extend(_async_handle_single_cluster_matches( hass, @@ -142,7 +143,7 @@ def _async_handle_profile_match(hass, endpoint, profile_clusters, zha_device, 'component': component } - if component == 'binary_sensor': + if component == BINARY_SENSOR: discovery_info.update({SENSOR_TYPE: UNKNOWN}) for cluster_id in profile_clusters: if cluster_id in BINARY_SENSOR_TYPES: @@ -242,11 +243,11 @@ def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key, 'component': component } - if component == 'sensor': + if component == SENSOR: discovery_info.update({ SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC) }) - if component == 'binary_sensor': + if component == BINARY_SENSOR: discovery_info.update({ SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN) }) From 407e0c58f90a4d972ed98bd608255f9f70c4da42 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 30 Apr 2019 18:20:38 +0200 Subject: [PATCH 187/346] Migrate tests to pytest (#23544) * Migrate tests to pytest * Fixup * Use loop fixture in test_check_config * Lint --- tests/helpers/test_entity_component.py | 367 +++--- tests/helpers/test_entity_platform.py | 390 +++--- tests/helpers/test_event.py | 1423 ++++++++++---------- tests/helpers/test_icon.py | 82 +- tests/helpers/test_init.py | 67 +- tests/helpers/test_intent.py | 39 +- tests/helpers/test_location.py | 89 +- tests/helpers/test_script.py | 1336 +++++++++---------- tests/helpers/test_state.py | 207 ++- tests/helpers/test_sun.py | 366 +++--- tests/helpers/test_temperature.py | 50 +- tests/helpers/test_template.py | 1680 ++++++++++++------------ tests/scripts/test_check_config.py | 265 ++-- tests/scripts/test_init.py | 22 +- tests/test_config.py | 902 +++++++------ tests/util/test_color.py | 531 ++++---- tests/util/test_distance.py | 113 +- tests/util/test_dt.py | 377 +++--- tests/util/test_init.py | 335 ++--- tests/util/test_json.py | 130 +- tests/util/test_pressure.py | 90 +- tests/util/test_ruamel_yaml.py | 77 +- tests/util/test_unit_system.py | 225 ++-- tests/util/test_volume.py | 64 +- tests/util/test_yaml.py | 427 +++--- 25 files changed, 4744 insertions(+), 4910 deletions(-) diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index cb433a16a7c..4fc834171c8 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,25 +1,24 @@ """The tests for the Entity component helper.""" # pylint: disable=protected-access -import asyncio from collections import OrderedDict import logging -import unittest from unittest.mock import patch, Mock from datetime import timedelta +import asynctest import pytest import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady from homeassistant.components import group from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.setup import setup_component, async_setup_component +from homeassistant.setup import async_setup_component from homeassistant.helpers import discovery import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockPlatform, MockModule, mock_coro, + MockPlatform, MockModule, mock_coro, async_fire_time_changed, MockEntity, MockConfigEntry, mock_entity_platform, mock_integration) @@ -27,178 +26,169 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" -class TestHelpersEntityComponent(unittest.TestCase): - """Test homeassistant.helpers.entity_component module.""" +async def test_setting_up_group(hass): + """Set up the setting of a group.""" + assert await async_setup_component(hass, 'group', {'group': {}}) + component = EntityComponent(_LOGGER, DOMAIN, hass, + group_name='everyone') - def setUp(self): # pylint: disable=invalid-name - """Initialize a test Home Assistant instance.""" - self.hass = get_test_home_assistant() + # No group after setup + assert len(hass.states.async_entity_ids()) == 0 - def tearDown(self): # pylint: disable=invalid-name - """Clean up the test Home Assistant instance.""" - self.hass.stop() + await component.async_add_entities([MockEntity()]) + await hass.async_block_till_done() - def test_setting_up_group(self): - """Set up the setting of a group.""" - setup_component(self.hass, 'group', {'group': {}}) - component = EntityComponent(_LOGGER, DOMAIN, self.hass, - group_name='everyone') + # group exists + assert len(hass.states.async_entity_ids()) == 2 + assert hass.states.async_entity_ids('group') == ['group.everyone'] - # No group after setup - assert len(self.hass.states.entity_ids()) == 0 + grp = hass.states.get('group.everyone') - component.add_entities([MockEntity()]) - self.hass.block_till_done() + assert grp.attributes.get('entity_id') == \ + ('test_domain.unnamed_device',) - # group exists - assert len(self.hass.states.entity_ids()) == 2 - assert self.hass.states.entity_ids('group') == ['group.everyone'] + # group extended + await component.async_add_entities([MockEntity(name='goodbye')]) + await hass.async_block_till_done() - group = self.hass.states.get('group.everyone') + assert len(hass.states.async_entity_ids()) == 3 + grp = hass.states.get('group.everyone') - assert group.attributes.get('entity_id') == \ - ('test_domain.unnamed_device',) - - # group extended - component.add_entities([MockEntity(name='goodbye')]) - self.hass.block_till_done() - - assert len(self.hass.states.entity_ids()) == 3 - group = self.hass.states.get('group.everyone') - - # Ordered in order of added to the group - assert group.attributes.get('entity_id') == \ - ('test_domain.goodbye', 'test_domain.unnamed_device') - - def test_setup_loads_platforms(self): - """Test the loading of the platforms.""" - component_setup = Mock(return_value=True) - platform_setup = Mock(return_value=None) - - mock_integration(self.hass, - MockModule('test_component', setup=component_setup)) - # mock the dependencies - mock_integration(self.hass, - MockModule('mod2', dependencies=['test_component'])) - mock_entity_platform(self.hass, 'test_domain.mod2', - MockPlatform(platform_setup)) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert not component_setup.called - assert not platform_setup.called - - component.setup({ - DOMAIN: { - 'platform': 'mod2', - } - }) - - self.hass.block_till_done() - assert component_setup.called - assert platform_setup.called - - def test_setup_recovers_when_setup_raises(self): - """Test the setup if exceptions are happening.""" - platform1_setup = Mock(side_effect=Exception('Broken')) - platform2_setup = Mock(return_value=None) - - mock_entity_platform(self.hass, 'test_domain.mod1', - MockPlatform(platform1_setup)) - mock_entity_platform(self.hass, 'test_domain.mod2', - MockPlatform(platform2_setup)) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert not platform1_setup.called - assert not platform2_setup.called - - component.setup(OrderedDict([ - (DOMAIN, {'platform': 'mod1'}), - ("{} 2".format(DOMAIN), {'platform': 'non_exist'}), - ("{} 3".format(DOMAIN), {'platform': 'mod2'}), - ])) - - self.hass.block_till_done() - assert platform1_setup.called - assert platform2_setup.called - - @patch('homeassistant.helpers.entity_component.EntityComponent' - '._async_setup_platform', return_value=mock_coro()) - @patch('homeassistant.setup.async_setup_component', - return_value=mock_coro(True)) - def test_setup_does_discovery(self, mock_setup_component, mock_setup): - """Test setup for discovery.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - component.setup({}) - - discovery.load_platform(self.hass, DOMAIN, 'platform_test', - {'msg': 'discovery_info'}, {DOMAIN: {}}) - - self.hass.block_till_done() - - assert mock_setup.called - assert ('platform_test', {}, {'msg': 'discovery_info'}) == \ - mock_setup.call_args[0] - - @patch('homeassistant.helpers.entity_platform.' - 'async_track_time_interval') - def test_set_scan_interval_via_config(self, mock_track): - """Test the setting of the scan interval via configuration.""" - def platform_setup(hass, config, add_entities, discovery_info=None): - """Test the platform setup.""" - add_entities([MockEntity(should_poll=True)]) - - mock_entity_platform(self.hass, 'test_domain.platform', - MockPlatform(platform_setup)) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - component.setup({ - DOMAIN: { - 'platform': 'platform', - 'scan_interval': timedelta(seconds=30), - } - }) - - self.hass.block_till_done() - assert mock_track.called - assert timedelta(seconds=30) == mock_track.call_args[0][2] - - def test_set_entity_namespace_via_config(self): - """Test setting an entity namespace.""" - def platform_setup(hass, config, add_entities, discovery_info=None): - """Test the platform setup.""" - add_entities([ - MockEntity(name='beer'), - MockEntity(name=None), - ]) - - platform = MockPlatform(platform_setup) - - mock_entity_platform(self.hass, 'test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - component.setup({ - DOMAIN: { - 'platform': 'platform', - 'entity_namespace': 'yummy' - } - }) - - self.hass.block_till_done() - - assert sorted(self.hass.states.entity_ids()) == \ - ['test_domain.yummy_beer', 'test_domain.yummy_unnamed_device'] + # Ordered in order of added to the group + assert grp.attributes.get('entity_id') == \ + ('test_domain.goodbye', 'test_domain.unnamed_device') -@asyncio.coroutine -def test_extract_from_service_available_device(hass): +async def test_setup_loads_platforms(hass): + """Test the loading of the platforms.""" + component_setup = Mock(return_value=True) + platform_setup = Mock(return_value=None) + + mock_integration(hass, MockModule('test_component', setup=component_setup)) + # mock the dependencies + mock_integration(hass, MockModule('mod2', dependencies=['test_component'])) + mock_entity_platform(hass, 'test_domain.mod2', + MockPlatform(platform_setup)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + assert not component_setup.called + assert not platform_setup.called + + component.setup({ + DOMAIN: { + 'platform': 'mod2', + } + }) + + await hass.async_block_till_done() + assert component_setup.called + assert platform_setup.called + + +async def test_setup_recovers_when_setup_raises(hass): + """Test the setup if exceptions are happening.""" + platform1_setup = Mock(side_effect=Exception('Broken')) + platform2_setup = Mock(return_value=None) + + mock_entity_platform(hass, 'test_domain.mod1', + MockPlatform(platform1_setup)) + mock_entity_platform(hass, 'test_domain.mod2', + MockPlatform(platform2_setup)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + assert not platform1_setup.called + assert not platform2_setup.called + + component.setup(OrderedDict([ + (DOMAIN, {'platform': 'mod1'}), + ("{} 2".format(DOMAIN), {'platform': 'non_exist'}), + ("{} 3".format(DOMAIN), {'platform': 'mod2'}), + ])) + + await hass.async_block_till_done() + assert platform1_setup.called + assert platform2_setup.called + + +@asynctest.patch('homeassistant.helpers.entity_component.EntityComponent' + '._async_setup_platform', return_value=mock_coro()) +@asynctest.patch('homeassistant.setup.async_setup_component', + return_value=mock_coro(True)) +async def test_setup_does_discovery(mock_setup_component, mock_setup, hass): + """Test setup for discovery.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + component.setup({}) + + discovery.load_platform(hass, DOMAIN, 'platform_test', + {'msg': 'discovery_info'}, {DOMAIN: {}}) + + await hass.async_block_till_done() + + assert mock_setup.called + assert ('platform_test', {}, {'msg': 'discovery_info'}) == \ + mock_setup.call_args[0] + + +@asynctest.patch('homeassistant.helpers.entity_platform.' + 'async_track_time_interval') +async def test_set_scan_interval_via_config(mock_track, hass): + """Test the setting of the scan interval via configuration.""" + def platform_setup(hass, config, add_entities, discovery_info=None): + """Test the platform setup.""" + add_entities([MockEntity(should_poll=True)]) + + mock_entity_platform(hass, 'test_domain.platform', + MockPlatform(platform_setup)) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + component.setup({ + DOMAIN: { + 'platform': 'platform', + 'scan_interval': timedelta(seconds=30), + } + }) + + await hass.async_block_till_done() + assert mock_track.called + assert timedelta(seconds=30) == mock_track.call_args[0][2] + + +async def test_set_entity_namespace_via_config(hass): + """Test setting an entity namespace.""" + def platform_setup(hass, config, add_entities, discovery_info=None): + """Test the platform setup.""" + add_entities([ + MockEntity(name='beer'), + MockEntity(name=None), + ]) + + platform = MockPlatform(platform_setup) + + mock_entity_platform(hass, 'test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + component.setup({ + DOMAIN: { + 'platform': 'platform', + 'entity_namespace': 'yummy' + } + }) + + await hass.async_block_till_done() + + assert sorted(hass.states.async_entity_ids()) == \ + ['test_domain.yummy_beer', 'test_domain.yummy_unnamed_device'] + + +async def test_extract_from_service_available_device(hass): """Test the extraction of entity from service and device is available.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='test_1'), MockEntity(name='test_2', available=False), MockEntity(name='test_3'), @@ -209,7 +199,7 @@ def test_extract_from_service_available_device(hass): assert ['test_domain.test_1', 'test_domain.test_3'] == \ sorted(ent.entity_id for ent in - (yield from component.async_extract_from_service(call_1))) + (await component.async_extract_from_service(call_1))) call_2 = ha.ServiceCall('test', 'service', data={ 'entity_id': ['test_domain.test_3', 'test_domain.test_4'], @@ -217,11 +207,10 @@ def test_extract_from_service_available_device(hass): assert ['test_domain.test_3'] == \ sorted(ent.entity_id for ent in - (yield from component.async_extract_from_service(call_2))) + (await component.async_extract_from_service(call_2))) -@asyncio.coroutine -def test_platform_not_ready(hass): +async def test_platform_not_ready(hass): """Test that we retry when platform not ready.""" platform1_setup = Mock(side_effect=[PlatformNotReady, PlatformNotReady, None]) @@ -231,7 +220,7 @@ def test_platform_not_ready(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_setup({ + await component.async_setup({ DOMAIN: { 'platform': 'mod1' } @@ -245,32 +234,31 @@ def test_platform_not_ready(hass): with patch('homeassistant.util.dt.utcnow', return_value=utcnow): # Should not trigger attempt 2 async_fire_time_changed(hass, utcnow + timedelta(seconds=29)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(platform1_setup.mock_calls) == 1 # Should trigger attempt 2 async_fire_time_changed(hass, utcnow + timedelta(seconds=30)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(platform1_setup.mock_calls) == 2 assert 'test_domain.mod1' not in hass.config.components # This should not trigger attempt 3 async_fire_time_changed(hass, utcnow + timedelta(seconds=59)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(platform1_setup.mock_calls) == 2 # Trigger attempt 3, which succeeds async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) - yield from hass.async_block_till_done() + await hass.async_block_till_done() assert len(platform1_setup.mock_calls) == 3 assert 'test_domain.mod1' in hass.config.components -@asyncio.coroutine -def test_extract_from_service_returns_all_if_no_entity_id(hass): +async def test_extract_from_service_returns_all_if_no_entity_id(hass): """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='test_1'), MockEntity(name='test_2'), ]) @@ -279,14 +267,13 @@ def test_extract_from_service_returns_all_if_no_entity_id(hass): assert ['test_domain.test_1', 'test_domain.test_2'] == \ sorted(ent.entity_id for ent in - (yield from component.async_extract_from_service(call))) + (await component.async_extract_from_service(call))) -@asyncio.coroutine -def test_extract_from_service_filter_out_non_existing_entities(hass): +async def test_extract_from_service_filter_out_non_existing_entities(hass): """Test the extraction of non existing entities from service.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='test_1'), MockEntity(name='test_2'), ]) @@ -297,28 +284,26 @@ def test_extract_from_service_filter_out_non_existing_entities(hass): assert ['test_domain.test_2'] == \ [ent.entity_id for ent - in (yield from component.async_extract_from_service(call))] + in await component.async_extract_from_service(call)] -@asyncio.coroutine -def test_extract_from_service_no_group_expand(hass): +async def test_extract_from_service_no_group_expand(hass): """Test not expanding a group.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - test_group = yield from group.Group.async_create_group( + test_group = await group.Group.async_create_group( hass, 'test_group', ['light.Ceiling', 'light.Kitchen']) - yield from component.async_add_entities([test_group]) + await component.async_add_entities([test_group]) call = ha.ServiceCall('test', 'service', { 'entity_id': ['group.test_group'] }) - extracted = yield from component.async_extract_from_service( + extracted = await component.async_extract_from_service( call, expand_group=False) assert extracted == [test_group] -@asyncio.coroutine -def test_setup_dependencies_platform(hass): +async def test_setup_dependencies_platform(hass): """Test we setup the dependencies of a platform. We're explictely testing that we process dependencies even if a component @@ -331,7 +316,7 @@ def test_setup_dependencies_platform(hass): component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_setup({ + await component.async_setup({ DOMAIN: { 'platform': 'test_component', } @@ -355,7 +340,7 @@ async def test_setup_entry(hass): 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] + p_hass, p_entry, _ = mock_setup_entry.mock_calls[0][1] assert p_hass is hass assert p_entry is entry @@ -448,7 +433,7 @@ async def test_set_service_race(hass): await async_setup_component(hass, 'group', {}) component = EntityComponent(_LOGGER, DOMAIN, hass, group_name='yo') - for i in range(2): + for _ in range(2): hass.async_create_task(component.async_add_entities([MockEntity()])) await hass.async_block_till_done() diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 0fed09b7cbc..65c22aa176f 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1,14 +1,14 @@ """Tests for the EntityPlatform helper.""" import asyncio import logging -import unittest from unittest.mock import patch, Mock, MagicMock from datetime import timedelta +import asynctest import pytest from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_component import ( EntityComponent, DEFAULT_SCAN_INTERVAL) from homeassistant.helpers import entity_platform, entity_registry @@ -16,7 +16,7 @@ from homeassistant.helpers import entity_platform, entity_registry import homeassistant.util.dt as dt_util from tests.common import ( - get_test_home_assistant, MockPlatform, fire_time_changed, mock_registry, + MockPlatform, async_fire_time_changed, mock_registry, MockEntity, MockEntityPlatform, MockConfigEntry, mock_entity_platform) _LOGGER = logging.getLogger(__name__) @@ -24,164 +24,158 @@ DOMAIN = "test_domain" PLATFORM = 'test_platform' -class TestHelpersEntityPlatform(unittest.TestCase): - """Test homeassistant.helpers.entity_component module.""" +async def test_polling_only_updates_entities_it_should_poll(hass): + """Test the polling of only updated entities.""" + component = EntityComponent( + _LOGGER, DOMAIN, hass, timedelta(seconds=20)) - def setUp(self): # pylint: disable=invalid-name - """Initialize a test Home Assistant instance.""" - self.hass = get_test_home_assistant() + no_poll_ent = MockEntity(should_poll=False) + no_poll_ent.async_update = Mock() + poll_ent = MockEntity(should_poll=True) + poll_ent.async_update = Mock() - def tearDown(self): # pylint: disable=invalid-name - """Clean up the test Home Assistant instance.""" - self.hass.stop() + await component.async_add_entities([no_poll_ent, poll_ent]) - def test_polling_only_updates_entities_it_should_poll(self): - """Test the polling of only updated entities.""" - component = EntityComponent( - _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) + no_poll_ent.async_update.reset_mock() + poll_ent.async_update.reset_mock() - no_poll_ent = MockEntity(should_poll=False) - no_poll_ent.async_update = Mock() - poll_ent = MockEntity(should_poll=True) - poll_ent.async_update = Mock() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() - component.add_entities([no_poll_ent, poll_ent]) - - no_poll_ent.async_update.reset_mock() - poll_ent.async_update.reset_mock() - - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) - self.hass.block_till_done() - - assert not no_poll_ent.async_update.called - assert poll_ent.async_update.called - - def test_polling_updates_entities_with_exception(self): - """Test the updated entities that not break with an exception.""" - component = EntityComponent( - _LOGGER, DOMAIN, self.hass, timedelta(seconds=20)) - - update_ok = [] - update_err = [] - - def update_mock(): - """Mock normal update.""" - update_ok.append(None) - - def update_mock_err(): - """Mock error update.""" - update_err.append(None) - raise AssertionError("Fake error update") - - ent1 = MockEntity(should_poll=True) - ent1.update = update_mock_err - ent2 = MockEntity(should_poll=True) - ent2.update = update_mock - ent3 = MockEntity(should_poll=True) - ent3.update = update_mock - ent4 = MockEntity(should_poll=True) - ent4.update = update_mock - - component.add_entities([ent1, ent2, ent3, ent4]) - - update_ok.clear() - update_err.clear() - - fire_time_changed(self.hass, dt_util.utcnow() + timedelta(seconds=20)) - self.hass.block_till_done() - - assert len(update_ok) == 3 - assert len(update_err) == 1 - - def test_update_state_adds_entities(self): - """Test if updating poll entities cause an entity to be added works.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent1 = MockEntity() - ent2 = MockEntity(should_poll=True) - - component.add_entities([ent2]) - assert 1 == len(self.hass.states.entity_ids()) - ent2.update = lambda *_: component.add_entities([ent1]) - - fire_time_changed( - self.hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL - ) - self.hass.block_till_done() - - assert 2 == len(self.hass.states.entity_ids()) - - def test_update_state_adds_entities_with_update_before_add_true(self): - """Test if call update before add to state machine.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent = MockEntity() - ent.update = Mock(spec_set=True) - - component.add_entities([ent], True) - self.hass.block_till_done() - - assert 1 == len(self.hass.states.entity_ids()) - assert ent.update.called - - def test_update_state_adds_entities_with_update_before_add_false(self): - """Test if not call update before add to state machine.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - ent = MockEntity() - ent.update = Mock(spec_set=True) - - component.add_entities([ent], False) - self.hass.block_till_done() - - assert 1 == len(self.hass.states.entity_ids()) - assert not ent.update.called - - @patch('homeassistant.helpers.entity_platform.' - 'async_track_time_interval') - def test_set_scan_interval_via_platform(self, mock_track): - """Test the setting of the scan interval via platform.""" - def platform_setup(hass, config, add_entities, discovery_info=None): - """Test the platform setup.""" - add_entities([MockEntity(should_poll=True)]) - - platform = MockPlatform(platform_setup) - platform.SCAN_INTERVAL = timedelta(seconds=30) - - mock_entity_platform(self.hass, 'test_domain.platform', platform) - - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - component.setup({ - DOMAIN: { - 'platform': 'platform', - } - }) - - self.hass.block_till_done() - assert mock_track.called - assert timedelta(seconds=30) == mock_track.call_args[0][2] - - def test_adding_entities_with_generator_and_thread_callback(self): - """Test generator in add_entities that calls thread method. - - We should make sure we resolve the generator to a list before passing - it into an async context. - """ - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - def create_entity(number): - """Create entity helper.""" - entity = MockEntity() - entity.entity_id = generate_entity_id(DOMAIN + '.{}', - 'Number', hass=self.hass) - return entity - - component.add_entities(create_entity(i) for i in range(2)) + assert not no_poll_ent.async_update.called + assert poll_ent.async_update.called -@asyncio.coroutine -def test_platform_warn_slow_setup(hass): +async def test_polling_updates_entities_with_exception(hass): + """Test the updated entities that not break with an exception.""" + component = EntityComponent( + _LOGGER, DOMAIN, hass, timedelta(seconds=20)) + + update_ok = [] + update_err = [] + + def update_mock(): + """Mock normal update.""" + update_ok.append(None) + + def update_mock_err(): + """Mock error update.""" + update_err.append(None) + raise AssertionError("Fake error update") + + ent1 = MockEntity(should_poll=True) + ent1.update = update_mock_err + ent2 = MockEntity(should_poll=True) + ent2.update = update_mock + ent3 = MockEntity(should_poll=True) + ent3.update = update_mock + ent4 = MockEntity(should_poll=True) + ent4.update = update_mock + + await component.async_add_entities([ent1, ent2, ent3, ent4]) + + update_ok.clear() + update_err.clear() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + + assert len(update_ok) == 3 + assert len(update_err) == 1 + + +async def test_update_state_adds_entities(hass): + """Test if updating poll entities cause an entity to be added works.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + ent1 = MockEntity() + ent2 = MockEntity(should_poll=True) + + await component.async_add_entities([ent2]) + assert len(hass.states.async_entity_ids()) == 1 + ent2.update = lambda *_: component.add_entities([ent1]) + + async_fire_time_changed( + hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 2 + + +async def test_update_state_adds_entities_with_update_before_add_true(hass): + """Test if call update before add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + ent = MockEntity() + ent.update = Mock(spec_set=True) + + await component.async_add_entities([ent], True) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + assert ent.update.called + + +async def test_update_state_adds_entities_with_update_before_add_false(hass): + """Test if not call update before add to state machine.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + ent = MockEntity() + ent.update = Mock(spec_set=True) + + await component.async_add_entities([ent], False) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids()) == 1 + assert not ent.update.called + + +@asynctest.patch('homeassistant.helpers.entity_platform.' + 'async_track_time_interval') +async def test_set_scan_interval_via_platform(mock_track, hass): + """Test the setting of the scan interval via platform.""" + def platform_setup(hass, config, add_entities, discovery_info=None): + """Test the platform setup.""" + add_entities([MockEntity(should_poll=True)]) + + platform = MockPlatform(platform_setup) + platform.SCAN_INTERVAL = timedelta(seconds=30) + + mock_entity_platform(hass, 'test_domain.platform', platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + + component.setup({ + DOMAIN: { + 'platform': 'platform', + } + }) + + await hass.async_block_till_done() + assert mock_track.called + assert timedelta(seconds=30) == mock_track.call_args[0][2] + + +async def test_adding_entities_with_generator_and_thread_callback(hass): + """Test generator in add_entities that calls thread method. + + We should make sure we resolve the generator to a list before passing + it into an async context. + """ + component = EntityComponent(_LOGGER, DOMAIN, hass) + + def create_entity(number): + """Create entity helper.""" + entity = MockEntity() + entity.entity_id = async_generate_entity_id(DOMAIN + '.{}', + 'Number', hass=hass) + return entity + + await component.async_add_entities(create_entity(i) for i in range(2)) + + +async def test_platform_warn_slow_setup(hass): """Warn we log when platform setup takes a long time.""" platform = MockPlatform() @@ -191,7 +185,7 @@ def test_platform_warn_slow_setup(hass): with patch.object(hass.loop, 'call_later', MagicMock()) \ as mock_call: - yield from component.async_setup({ + await component.async_setup({ DOMAIN: { 'platform': 'platform', } @@ -208,21 +202,19 @@ def test_platform_warn_slow_setup(hass): assert mock_call().cancel.called -@asyncio.coroutine -def test_platform_error_slow_setup(hass, caplog): +async def test_platform_error_slow_setup(hass, caplog): """Don't block startup more than SLOW_SETUP_MAX_WAIT.""" with patch.object(entity_platform, 'SLOW_SETUP_MAX_WAIT', 0): called = [] - @asyncio.coroutine - def setup_platform(*args): + async def setup_platform(*args): called.append(1) - yield from asyncio.sleep(1, loop=hass.loop) + await asyncio.sleep(1, loop=hass.loop) platform = MockPlatform(async_setup_platform=setup_platform) component = EntityComponent(_LOGGER, DOMAIN, hass) mock_entity_platform(hass, 'test_domain.test_platform', platform) - yield from component.async_setup({ + await component.async_setup({ DOMAIN: { 'platform': 'test_platform', } @@ -232,23 +224,21 @@ def test_platform_error_slow_setup(hass, caplog): assert 'test_platform is taking longer than 0 seconds' in caplog.text -@asyncio.coroutine -def test_updated_state_used_for_entity_id(hass): +async def test_updated_state_used_for_entity_id(hass): """Test that first update results used for entity ID generation.""" component = EntityComponent(_LOGGER, DOMAIN, hass) class MockEntityNameFetcher(MockEntity): """Mock entity that fetches a friendly name.""" - @asyncio.coroutine - def async_update(self): + async def async_update(self): """Mock update that assigns a name.""" self._values['name'] = "Living Room" - yield from component.async_add_entities([MockEntityNameFetcher()], True) + await component.async_add_entities([MockEntityNameFetcher()], True) entity_ids = hass.states.async_entity_ids() - assert 1 == len(entity_ids) + assert len(entity_ids) == 1 assert entity_ids[0] == "test_domain.living_room" @@ -374,8 +364,7 @@ async def test_parallel_updates_sync_platform_with_constant(hass): assert entity.parallel_updates._value == 2 -@asyncio.coroutine -def test_raise_error_on_update(hass): +async def test_raise_error_on_update(hass): """Test the add entity if they raise an error on update.""" updates = [] component = EntityComponent(_LOGGER, DOMAIN, hass) @@ -389,63 +378,58 @@ def test_raise_error_on_update(hass): entity1.update = _raise entity2.update = lambda: updates.append(1) - yield from component.async_add_entities([entity1, entity2], True) + await component.async_add_entities([entity1, entity2], True) assert len(updates) == 1 assert 1 in updates -@asyncio.coroutine -def test_async_remove_with_platform(hass): +async def test_async_remove_with_platform(hass): """Remove an entity from a platform.""" component = EntityComponent(_LOGGER, DOMAIN, hass) entity1 = MockEntity(name='test_1') - yield from component.async_add_entities([entity1]) + await component.async_add_entities([entity1]) assert len(hass.states.async_entity_ids()) == 1 - yield from entity1.async_remove() + await entity1.async_remove() assert len(hass.states.async_entity_ids()) == 0 -@asyncio.coroutine -def test_not_adding_duplicate_entities_with_unique_id(hass): +async def test_not_adding_duplicate_entities_with_unique_id(hass): """Test for not adding duplicate entities.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='test1', unique_id='not_very_unique')]) assert len(hass.states.async_entity_ids()) == 1 - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='test2', unique_id='not_very_unique')]) assert len(hass.states.async_entity_ids()) == 1 -@asyncio.coroutine -def test_using_prescribed_entity_id(hass): +async def test_using_prescribed_entity_id(hass): """Test for using predefined entity ID.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='bla', entity_id='hello.world')]) assert 'hello.world' in hass.states.async_entity_ids() -@asyncio.coroutine -def test_using_prescribed_entity_id_with_unique_id(hass): +async def test_using_prescribed_entity_id_with_unique_id(hass): """Test for ammending predefined entity ID because currently exists.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(entity_id='test_domain.world')]) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(entity_id='test_domain.world', unique_id='bla')]) assert 'test_domain.world_2' in hass.states.async_entity_ids() -@asyncio.coroutine -def test_using_prescribed_entity_id_which_is_registered(hass): +async def test_using_prescribed_entity_id_which_is_registered(hass): """Test not allowing predefined entity ID that already registered.""" component = EntityComponent(_LOGGER, DOMAIN, hass) registry = mock_registry(hass) @@ -454,14 +438,13 @@ def test_using_prescribed_entity_id_which_is_registered(hass): DOMAIN, 'test', '1234', suggested_object_id='world') # This entity_id will be rewritten - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(entity_id='test_domain.world')]) assert 'test_domain.world_2' in hass.states.async_entity_ids() -@asyncio.coroutine -def test_name_which_conflict_with_registered(hass): +async def test_name_which_conflict_with_registered(hass): """Test not generating conflicting entity ID based on name.""" component = EntityComponent(_LOGGER, DOMAIN, hass) registry = mock_registry(hass) @@ -470,24 +453,22 @@ def test_name_which_conflict_with_registered(hass): registry.async_get_or_create( DOMAIN, 'test', '1234', suggested_object_id='world') - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(name='world')]) assert 'test_domain.world_2' in hass.states.async_entity_ids() -@asyncio.coroutine -def test_entity_with_name_and_entity_id_getting_registered(hass): +async def test_entity_with_name_and_entity_id_getting_registered(hass): """Ensure that entity ID is used for registration.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(unique_id='1234', name='bla', entity_id='test_domain.world')]) assert 'test_domain.world' in hass.states.async_entity_ids() -@asyncio.coroutine -def test_overriding_name_from_registry(hass): +async def test_overriding_name_from_registry(hass): """Test that we can override a name via the Entity Registry.""" component = EntityComponent(_LOGGER, DOMAIN, hass) mock_registry(hass, { @@ -499,7 +480,7 @@ def test_overriding_name_from_registry(hass): name='Overridden' ) }) - yield from component.async_add_entities([ + await component.async_add_entities([ MockEntity(unique_id='1234', name='Device Name')]) state = hass.states.get('test_domain.world') @@ -507,18 +488,16 @@ def test_overriding_name_from_registry(hass): assert state.name == 'Overridden' -@asyncio.coroutine -def test_registry_respect_entity_namespace(hass): +async def test_registry_respect_entity_namespace(hass): """Test that the registry respects entity namespace.""" mock_registry(hass) platform = MockEntityPlatform(hass, entity_namespace='ns') entity = MockEntity(unique_id='1234', name='Device Name') - yield from platform.async_add_entities([entity]) + await platform.async_add_entities([entity]) assert entity.entity_id == 'test_domain.ns_device_name' -@asyncio.coroutine -def test_registry_respect_entity_disabled(hass): +async def test_registry_respect_entity_disabled(hass): """Test that the registry respects entity disabled.""" mock_registry(hass, { 'test_domain.world': entity_registry.RegistryEntry( @@ -531,7 +510,7 @@ def test_registry_respect_entity_disabled(hass): }) platform = MockEntityPlatform(hass) entity = MockEntity(unique_id='1234') - yield from platform.async_add_entities([entity]) + await platform.async_add_entities([entity]) assert entity.entity_id is None assert hass.states.async_entity_ids() == [] @@ -643,12 +622,11 @@ async def test_reset_cancels_retry_setup(hass): assert ent_platform._async_cancel_retry_setup is None -@asyncio.coroutine -def test_not_fails_with_adding_empty_entities_(hass): +async def test_not_fails_with_adding_empty_entities_(hass): """Test for not fails on empty entities list.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - yield from component.async_add_entities([]) + await component.async_add_entities([]) assert len(hass.states.async_entity_ids()) == 0 diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index dd5744bbb52..0756bab2eec 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,763 +1,752 @@ """Test event helpers.""" # pylint: disable=protected-access import asyncio -import unittest from datetime import datetime, timedelta +from unittest.mock import patch from astral import Astral import pytest from homeassistant.core import callback -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component import homeassistant.core as ha from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import ( async_call_later, - call_later, - track_point_in_utc_time, - track_point_in_time, - track_utc_time_change, - track_time_change, - track_state_change, - track_time_interval, - track_template, - track_same_state, - track_sunrise, - track_sunset, + async_track_point_in_time, + async_track_point_in_utc_time, + async_track_same_state, + async_track_state_change, + async_track_sunrise, + async_track_sunset, + async_track_template, + async_track_time_change, + async_track_time_interval, + async_track_utc_time_change, ) from homeassistant.helpers.template import Template from homeassistant.components import sun import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant, fire_time_changed -from unittest.mock import patch - - -class TestEventHelpers(unittest.TestCase): - """Test the Home Assistant event helpers.""" - - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_track_point_in_time(self): - """Test track point in time.""" - before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) - birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) - after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) - - runs = [] - - track_point_in_utc_time( - self.hass, callback(lambda x: runs.append(1)), birthday_paulus) - - self._send_time_changed(before_birthday) - self.hass.block_till_done() - assert 0 == len(runs) - - self._send_time_changed(birthday_paulus) - self.hass.block_till_done() - assert 1 == len(runs) - - # A point in time tracker will only fire once, this should do nothing - self._send_time_changed(birthday_paulus) - self.hass.block_till_done() - assert 1 == len(runs) - - track_point_in_time( - self.hass, callback(lambda x: runs.append(1)), birthday_paulus) - - self._send_time_changed(after_birthday) - self.hass.block_till_done() - assert 2 == len(runs) - - unsub = track_point_in_time( - self.hass, callback(lambda x: runs.append(1)), birthday_paulus) - unsub() - - self._send_time_changed(after_birthday) - self.hass.block_till_done() - assert 2 == len(runs) - - def test_track_state_change(self): - """Test track_state_change.""" - # 2 lists to track how often our callbacks get called - specific_runs = [] - wildcard_runs = [] - wildercard_runs = [] - - def specific_run_callback(entity_id, old_state, new_state): - specific_runs.append(1) - - track_state_change( - self.hass, 'light.Bowl', specific_run_callback, 'on', 'off') - - @ha.callback - def wildcard_run_callback(entity_id, old_state, new_state): - wildcard_runs.append((old_state, new_state)) - - track_state_change(self.hass, 'light.Bowl', wildcard_run_callback) - - @asyncio.coroutine - def wildercard_run_callback(entity_id, old_state, new_state): - wildercard_runs.append((old_state, new_state)) - - track_state_change(self.hass, MATCH_ALL, wildercard_run_callback) - - # Adding state to state machine - self.hass.states.set("light.Bowl", "on") - self.hass.block_till_done() - assert 0 == len(specific_runs) - assert 1 == len(wildcard_runs) - assert 1 == len(wildercard_runs) - assert wildcard_runs[-1][0] is None - assert wildcard_runs[-1][1] is not None - - # Set same state should not trigger a state change/listener - self.hass.states.set('light.Bowl', 'on') - self.hass.block_till_done() - assert 0 == len(specific_runs) - assert 1 == len(wildcard_runs) - assert 1 == len(wildercard_runs) - - # State change off -> on - self.hass.states.set('light.Bowl', 'off') - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 2 == len(wildcard_runs) - assert 2 == len(wildercard_runs) - - # State change off -> off - self.hass.states.set('light.Bowl', 'off', {"some_attr": 1}) - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 3 == len(wildcard_runs) - assert 3 == len(wildercard_runs) - - # State change off -> on - self.hass.states.set('light.Bowl', 'on') - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 4 == len(wildcard_runs) - assert 4 == len(wildercard_runs) - - self.hass.states.remove('light.bowl') - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 5 == len(wildcard_runs) - assert 5 == len(wildercard_runs) - assert wildcard_runs[-1][0] is not None - assert wildcard_runs[-1][1] is None - assert wildercard_runs[-1][0] is not None - assert wildercard_runs[-1][1] is None - - # Set state for different entity id - self.hass.states.set('switch.kitchen', 'on') - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 5 == len(wildcard_runs) - assert 6 == len(wildercard_runs) - - def test_track_template(self): - """Test tracking template.""" - specific_runs = [] - wildcard_runs = [] - wildercard_runs = [] - - template_condition = Template( - "{{states.switch.test.state == 'on'}}", - self.hass - ) - template_condition_var = Template( - "{{states.switch.test.state == 'on' and test == 5}}", - self.hass - ) - - self.hass.states.set('switch.test', 'off') - - def specific_run_callback(entity_id, old_state, new_state): - specific_runs.append(1) - - track_template(self.hass, template_condition, specific_run_callback) - - @ha.callback - def wildcard_run_callback(entity_id, old_state, new_state): - wildcard_runs.append((old_state, new_state)) - - track_template(self.hass, template_condition, wildcard_run_callback) - - @asyncio.coroutine - def wildercard_run_callback(entity_id, old_state, new_state): - wildercard_runs.append((old_state, new_state)) - - track_template( - self.hass, template_condition_var, wildercard_run_callback, - {'test': 5}) - - self.hass.states.set('switch.test', 'on') - self.hass.block_till_done() - - assert 1 == len(specific_runs) - assert 1 == len(wildcard_runs) - assert 1 == len(wildercard_runs) - - self.hass.states.set('switch.test', 'on') - self.hass.block_till_done() - - assert 1 == len(specific_runs) - assert 1 == len(wildcard_runs) - assert 1 == len(wildercard_runs) - - self.hass.states.set('switch.test', 'off') - self.hass.block_till_done() - - assert 1 == len(specific_runs) - assert 1 == len(wildcard_runs) - assert 1 == len(wildercard_runs) - - self.hass.states.set('switch.test', 'off') - self.hass.block_till_done() - - assert 1 == len(specific_runs) - assert 1 == len(wildcard_runs) - assert 1 == len(wildercard_runs) - - self.hass.states.set('switch.test', 'on') - self.hass.block_till_done() - - assert 2 == len(specific_runs) - assert 2 == len(wildcard_runs) - assert 2 == len(wildercard_runs) - - def test_track_same_state_simple_trigger(self): - """Test track_same_change with trigger simple.""" - thread_runs = [] - callback_runs = [] - coroutine_runs = [] - period = timedelta(minutes=1) - - def thread_run_callback(): - thread_runs.append(1) - - track_same_state( - self.hass, period, thread_run_callback, - lambda _, _2, to_s: to_s.state == 'on', - entity_ids='light.Bowl') - - @ha.callback - def callback_run_callback(): - callback_runs.append(1) - - track_same_state( - self.hass, period, callback_run_callback, - lambda _, _2, to_s: to_s.state == 'on', - entity_ids='light.Bowl') - - @asyncio.coroutine - def coroutine_run_callback(): - coroutine_runs.append(1) - - track_same_state( - self.hass, period, coroutine_run_callback, - lambda _, _2, to_s: to_s.state == 'on') - - # Adding state to state machine - self.hass.states.set("light.Bowl", "on") - self.hass.block_till_done() - assert 0 == len(thread_runs) - assert 0 == len(callback_runs) - assert 0 == len(coroutine_runs) - - # change time to track and see if they trigger - future = dt_util.utcnow() + period - fire_time_changed(self.hass, future) - self.hass.block_till_done() - assert 1 == len(thread_runs) - assert 1 == len(callback_runs) - assert 1 == len(coroutine_runs) - - def test_track_same_state_simple_no_trigger(self): - """Test track_same_change with no trigger.""" - callback_runs = [] - period = timedelta(minutes=1) - - @ha.callback - def callback_run_callback(): - callback_runs.append(1) - - track_same_state( - self.hass, period, callback_run_callback, - lambda _, _2, to_s: to_s.state == 'on', - entity_ids='light.Bowl') - - # Adding state to state machine - self.hass.states.set("light.Bowl", "on") - self.hass.block_till_done() - assert 0 == len(callback_runs) - - # Change state on state machine - self.hass.states.set("light.Bowl", "off") - self.hass.block_till_done() - assert 0 == len(callback_runs) - - # change time to track and see if they trigger - future = dt_util.utcnow() + period - fire_time_changed(self.hass, future) - self.hass.block_till_done() - assert 0 == len(callback_runs) - - def test_track_same_state_simple_trigger_check_funct(self): - """Test track_same_change with trigger and check funct.""" - callback_runs = [] - check_func = [] - period = timedelta(minutes=1) - - @ha.callback - def callback_run_callback(): - callback_runs.append(1) - - @ha.callback - def async_check_func(entity, from_s, to_s): - check_func.append((entity, from_s, to_s)) - return True - - track_same_state( - self.hass, period, callback_run_callback, - entity_ids='light.Bowl', async_check_same_func=async_check_func) - - # Adding state to state machine - self.hass.states.set("light.Bowl", "on") - self.hass.block_till_done() - assert 0 == len(callback_runs) - assert 'on' == check_func[-1][2].state - assert 'light.bowl' == check_func[-1][0] - - # change time to track and see if they trigger - future = dt_util.utcnow() + period - fire_time_changed(self.hass, future) - self.hass.block_till_done() - assert 1 == len(callback_runs) - - def test_track_time_interval(self): - """Test tracking time interval.""" - specific_runs = [] - - utc_now = dt_util.utcnow() - unsub = track_time_interval( - self.hass, lambda x: specific_runs.append(1), - timedelta(seconds=10) - ) - - self._send_time_changed(utc_now + timedelta(seconds=5)) - self.hass.block_till_done() - assert 0 == len(specific_runs) - - self._send_time_changed(utc_now + timedelta(seconds=13)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed(utc_now + timedelta(minutes=20)) - self.hass.block_till_done() - assert 2 == len(specific_runs) - - unsub() - - self._send_time_changed(utc_now + timedelta(seconds=30)) - self.hass.block_till_done() - assert 2 == len(specific_runs) - - def test_track_sunrise(self): - """Test track the sunrise.""" - latitude = 32.87336 - longitude = 117.22743 - - # Setup sun component - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) - - # Get next sunrise/sunset - astral = Astral() - utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) - utc_today = utc_now.date() - - mod = -1 - while True: - next_rising = (astral.sunrise_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_rising > utc_now: - break - mod += 1 - - # Track sunrise - runs = [] - with patch('homeassistant.util.dt.utcnow', return_value=utc_now): - unsub = track_sunrise(self.hass, lambda: runs.append(1)) - - offset_runs = [] - offset = timedelta(minutes=30) - with patch('homeassistant.util.dt.utcnow', return_value=utc_now): - unsub2 = track_sunrise(self.hass, lambda: offset_runs.append(1), - offset) - - # run tests - self._send_time_changed(next_rising - offset) - self.hass.block_till_done() - assert 0 == len(runs) - assert 0 == len(offset_runs) - - self._send_time_changed(next_rising) - self.hass.block_till_done() - assert 1 == len(runs) - assert 0 == len(offset_runs) - - self._send_time_changed(next_rising + offset) - self.hass.block_till_done() - assert 1 == len(runs) - assert 1 == len(offset_runs) - - unsub() - unsub2() - - self._send_time_changed(next_rising + offset) - self.hass.block_till_done() - assert 1 == len(runs) - assert 1 == len(offset_runs) - - def test_track_sunset(self): - """Test track the sunset.""" - latitude = 32.87336 - longitude = 117.22743 - - # Setup sun component - self.hass.config.latitude = latitude - self.hass.config.longitude = longitude - setup_component(self.hass, sun.DOMAIN, { - sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) - - # Get next sunrise/sunset - astral = Astral() - utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) - utc_today = utc_now.date() - - mod = -1 - while True: - next_setting = (astral.sunset_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_setting > utc_now: - break - mod += 1 - - # Track sunset - runs = [] - with patch('homeassistant.util.dt.utcnow', return_value=utc_now): - unsub = track_sunset(self.hass, lambda: runs.append(1)) - - offset_runs = [] - offset = timedelta(minutes=30) - with patch('homeassistant.util.dt.utcnow', return_value=utc_now): - unsub2 = track_sunset( - self.hass, lambda: offset_runs.append(1), offset) - - # Run tests - self._send_time_changed(next_setting - offset) - self.hass.block_till_done() - assert 0 == len(runs) - assert 0 == len(offset_runs) - - self._send_time_changed(next_setting) - self.hass.block_till_done() - assert 1 == len(runs) - assert 0 == len(offset_runs) - - self._send_time_changed(next_setting + offset) - self.hass.block_till_done() - assert 1 == len(runs) - assert 1 == len(offset_runs) - - unsub() - unsub2() - - self._send_time_changed(next_setting + offset) - self.hass.block_till_done() - assert 1 == len(runs) - assert 1 == len(offset_runs) - - def _send_time_changed(self, now): - """Send a time changed event.""" - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) - - -class TestTrackTimeChange(unittest.TestCase): - """Test track time change methods.""" - - def setUp(self): - """Set up the tests.""" - self.orig_default_time_zone = dt_util.DEFAULT_TIME_ZONE - self.hass = get_test_home_assistant() - - def tearDown(self): - """Stop everything that was started.""" - dt_util.set_default_time_zone(self.orig_default_time_zone) - self.hass.stop() - - def _send_time_changed(self, now): - """Send a time changed event.""" - self.hass.bus.fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) - - def test_track_time_change(self): - """Test tracking time change.""" - wildcard_runs = [] - specific_runs = [] - - unsub = track_time_change(self.hass, - lambda x: wildcard_runs.append(1)) - unsub_utc = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), second=[0, 30]) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 1 == len(wildcard_runs) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 15)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - assert 2 == len(wildcard_runs) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) - self.hass.block_till_done() - assert 2 == len(specific_runs) - assert 3 == len(wildcard_runs) - - unsub() - unsub_utc() - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 30)) - self.hass.block_till_done() - assert 2 == len(specific_runs) - assert 3 == len(wildcard_runs) - - def test_periodic_task_minute(self): - """Test periodic tasks per minute.""" - specific_runs = [] - - unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), minute='/5', - second=0) - - self._send_time_changed(datetime(2014, 5, 24, 12, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed(datetime(2014, 5, 24, 12, 3, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed(datetime(2014, 5, 24, 12, 5, 0)) - self.hass.block_till_done() - assert 2 == len(specific_runs) +from tests.common import async_fire_time_changed - unsub() +DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE - self._send_time_changed(datetime(2014, 5, 24, 12, 5, 0)) - self.hass.block_till_done() - assert 2 == len(specific_runs) - def test_periodic_task_hour(self): - """Test periodic tasks per hour.""" - specific_runs = [] +def teardown(): + """Stop everything that was started.""" + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) - unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), hour='/2', - minute=0, second=0) - self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) +def _send_time_changed(hass, now): + """Send a time changed event.""" + hass.bus.async_fire(ha.EVENT_TIME_CHANGED, {ha.ATTR_NOW: now}) - self._send_time_changed(datetime(2014, 5, 24, 23, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - self._send_time_changed(datetime(2014, 5, 25, 0, 0, 0)) - self.hass.block_till_done() - assert 2 == len(specific_runs) +async def test_track_point_in_time(hass): + """Test track point in time.""" + before_birthday = datetime(1985, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + birthday_paulus = datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) + after_birthday = datetime(1987, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) - self._send_time_changed(datetime(2014, 5, 25, 1, 0, 0)) - self.hass.block_till_done() - assert 2 == len(specific_runs) + runs = [] - self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) - self.hass.block_till_done() - assert 3 == len(specific_runs) - - unsub() - - self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) - self.hass.block_till_done() - assert 3 == len(specific_runs) + async_track_point_in_utc_time( + hass, callback(lambda x: runs.append(1)), birthday_paulus) + + _send_time_changed(hass, before_birthday) + await hass.async_block_till_done() + assert len(runs) == 0 + + _send_time_changed(hass, birthday_paulus) + await hass.async_block_till_done() + assert len(runs) == 1 + + # A point in time tracker will only fire once, this should do nothing + _send_time_changed(hass, birthday_paulus) + await hass.async_block_till_done() + assert len(runs) == 1 + + async_track_point_in_utc_time( + hass, callback(lambda x: runs.append(1)), birthday_paulus) + + _send_time_changed(hass, after_birthday) + await hass.async_block_till_done() + assert len(runs) == 2 + + unsub = async_track_point_in_time( + hass, callback(lambda x: runs.append(1)), birthday_paulus) + unsub() + + _send_time_changed(hass, after_birthday) + await hass.async_block_till_done() + assert len(runs) == 2 + + +async def test_track_state_change(hass): + """Test track_state_change.""" + # 2 lists to track how often our callbacks get called + specific_runs = [] + wildcard_runs = [] + wildercard_runs = [] + + def specific_run_callback(entity_id, old_state, new_state): + specific_runs.append(1) + + async_track_state_change( + hass, 'light.Bowl', specific_run_callback, 'on', 'off') + + @ha.callback + def wildcard_run_callback(entity_id, old_state, new_state): + wildcard_runs.append((old_state, new_state)) + + async_track_state_change(hass, 'light.Bowl', wildcard_run_callback) - def test_periodic_task_wrong_input(self): - """Test periodic tasks with wrong input.""" - specific_runs = [] - - with pytest.raises(ValueError): - track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), hour='/two') - - self._send_time_changed(datetime(2014, 5, 2, 0, 0, 0)) - self.hass.block_till_done() - assert 0 == len(specific_runs) + @asyncio.coroutine + def wildercard_run_callback(entity_id, old_state, new_state): + wildercard_runs.append((old_state, new_state)) - def test_periodic_task_clock_rollback(self): - """Test periodic tasks with the time rolling backwards.""" - specific_runs = [] + async_track_state_change(hass, MATCH_ALL, wildercard_run_callback) + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(specific_runs) == 0 + assert len(wildcard_runs) == 1 + assert len(wildercard_runs) == 1 + assert wildcard_runs[-1][0] is None + assert wildcard_runs[-1][1] is not None - unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), hour='/2', minute=0, - second=0) + # Set same state should not trigger a state change/listener + hass.states.async_set('light.Bowl', 'on') + await hass.async_block_till_done() + assert len(specific_runs) == 0 + assert len(wildcard_runs) == 1 + assert len(wildercard_runs) == 1 - self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed(datetime(2014, 5, 24, 23, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) - self.hass.block_till_done() - assert 2 == len(specific_runs) + # State change off -> on + hass.states.async_set('light.Bowl', 'off') + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 2 + assert len(wildercard_runs) == 2 - self._send_time_changed(datetime(2014, 5, 24, 0, 0, 0)) - self.hass.block_till_done() - assert 3 == len(specific_runs) - - self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) - self.hass.block_till_done() - assert 4 == len(specific_runs) + # State change off -> off + hass.states.async_set('light.Bowl', 'off', {"some_attr": 1}) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 3 + assert len(wildercard_runs) == 3 + + # State change off -> on + hass.states.async_set('light.Bowl', 'on') + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 4 + assert len(wildercard_runs) == 4 + + hass.states.async_remove('light.bowl') + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 5 + assert len(wildercard_runs) == 5 + assert wildcard_runs[-1][0] is not None + assert wildcard_runs[-1][1] is None + assert wildercard_runs[-1][0] is not None + assert wildercard_runs[-1][1] is None + + # Set state for different entity id + hass.states.async_set('switch.kitchen', 'on') + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 5 + assert len(wildercard_runs) == 6 + + +async def test_track_template(hass): + """Test tracking template.""" + specific_runs = [] + wildcard_runs = [] + wildercard_runs = [] + + template_condition = Template( + "{{states.switch.test.state == 'on'}}", + hass + ) + template_condition_var = Template( + "{{states.switch.test.state == 'on' and test == 5}}", + hass + ) + + hass.states.async_set('switch.test', 'off') + + def specific_run_callback(entity_id, old_state, new_state): + specific_runs.append(1) + + async_track_template(hass, template_condition, specific_run_callback) + + @ha.callback + def wildcard_run_callback(entity_id, old_state, new_state): + wildcard_runs.append((old_state, new_state)) + + async_track_template(hass, template_condition, wildcard_run_callback) + + @asyncio.coroutine + def wildercard_run_callback(entity_id, old_state, new_state): + wildercard_runs.append((old_state, new_state)) + + async_track_template( + hass, template_condition_var, wildercard_run_callback, + {'test': 5}) + + hass.states.async_set('switch.test', 'on') + await hass.async_block_till_done() + + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 1 + assert len(wildercard_runs) == 1 + + hass.states.async_set('switch.test', 'on') + await hass.async_block_till_done() + + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 1 + assert len(wildercard_runs) == 1 + + hass.states.async_set('switch.test', 'off') + await hass.async_block_till_done() + + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 1 + assert len(wildercard_runs) == 1 + + hass.states.async_set('switch.test', 'off') + await hass.async_block_till_done() + + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 1 + assert len(wildercard_runs) == 1 + + hass.states.async_set('switch.test', 'on') + await hass.async_block_till_done() + + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 2 + assert len(wildercard_runs) == 2 + + +async def test_track_same_state_simple_trigger(hass): + """Test track_same_change with trigger simple.""" + thread_runs = [] + callback_runs = [] + coroutine_runs = [] + period = timedelta(minutes=1) + + def thread_run_callback(): + thread_runs.append(1) + + async_track_same_state( + hass, period, thread_run_callback, + lambda _, _2, to_s: to_s.state == 'on', + entity_ids='light.Bowl') + + @ha.callback + def callback_run_callback(): + callback_runs.append(1) + + async_track_same_state( + hass, period, callback_run_callback, + lambda _, _2, to_s: to_s.state == 'on', + entity_ids='light.Bowl') + + @asyncio.coroutine + def coroutine_run_callback(): + coroutine_runs.append(1) + + async_track_same_state( + hass, period, coroutine_run_callback, + lambda _, _2, to_s: to_s.state == 'on') + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(thread_runs) == 0 + assert len(callback_runs) == 0 + assert len(coroutine_runs) == 0 + + # change time to track and see if they trigger + future = dt_util.utcnow() + period + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert len(thread_runs) == 1 + assert len(callback_runs) == 1 + assert len(coroutine_runs) == 1 - unsub() - self._send_time_changed(datetime(2014, 5, 25, 2, 0, 0)) - self.hass.block_till_done() - assert 4 == len(specific_runs) - - def test_periodic_task_duplicate_time(self): - """Test periodic tasks not triggering on duplicate time.""" - specific_runs = [] +async def test_track_same_state_simple_no_trigger(hass): + """Test track_same_change with no trigger.""" + callback_runs = [] + period = timedelta(minutes=1) + + @ha.callback + def callback_run_callback(): + callback_runs.append(1) + + async_track_same_state( + hass, period, callback_run_callback, + lambda _, _2, to_s: to_s.state == 'on', + entity_ids='light.Bowl') + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(callback_runs) == 0 + + # Change state on state machine + hass.states.async_set("light.Bowl", "off") + await hass.async_block_till_done() + assert len(callback_runs) == 0 + + # change time to track and see if they trigger + future = dt_util.utcnow() + period + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert len(callback_runs) == 0 + + +async def test_track_same_state_simple_trigger_check_funct(hass): + """Test track_same_change with trigger and check funct.""" + callback_runs = [] + check_func = [] + period = timedelta(minutes=1) + + @ha.callback + def callback_run_callback(): + callback_runs.append(1) + + @ha.callback + def async_check_func(entity, from_s, to_s): + check_func.append((entity, from_s, to_s)) + return True + + async_track_same_state( + hass, period, callback_run_callback, + entity_ids='light.Bowl', async_check_same_func=async_check_func) + + # Adding state to state machine + hass.states.async_set("light.Bowl", "on") + await hass.async_block_till_done() + assert len(callback_runs) == 0 + assert check_func[-1][2].state == 'on' + assert check_func[-1][0] == 'light.bowl' + + # change time to track and see if they trigger + future = dt_util.utcnow() + period + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert len(callback_runs) == 1 + + +async def test_track_time_interval(hass): + """Test tracking time interval.""" + specific_runs = [] + + utc_now = dt_util.utcnow() + unsub = async_track_time_interval( + hass, lambda x: specific_runs.append(1), + timedelta(seconds=10) + ) + + _send_time_changed(hass, utc_now + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + _send_time_changed(hass, utc_now + timedelta(seconds=13)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, utc_now + timedelta(minutes=20)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + unsub() + + _send_time_changed(hass, utc_now + timedelta(seconds=30)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + +async def test_track_sunrise(hass): + """Test track the sunrise.""" + latitude = 32.87336 + longitude = 117.22743 + + # Setup sun component + hass.config.latitude = latitude + hass.config.longitude = longitude + assert await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # Get next sunrise/sunset + astral = Astral() + utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) + utc_today = utc_now.date() + + mod = -1 + while True: + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + # Track sunrise + runs = [] + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub = async_track_sunrise(hass, lambda: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub2 = async_track_sunrise(hass, lambda: offset_runs.append(1), + offset) + + # run tests + _send_time_changed(hass, next_rising - offset) + await hass.async_block_till_done() + assert len(runs) == 0 + assert len(offset_runs) == 0 + + _send_time_changed(hass, next_rising) + await hass.async_block_till_done() + assert len(runs) == 1 + assert len(offset_runs) == 0 + + _send_time_changed(hass, next_rising + offset) + await hass.async_block_till_done() + assert len(runs) == 1 + assert len(offset_runs) == 1 + + unsub() + unsub2() + + _send_time_changed(hass, next_rising + offset) + await hass.async_block_till_done() + assert len(runs) == 1 + assert len(offset_runs) == 1 + + +async def test_track_sunset(hass): + """Test track the sunset.""" + latitude = 32.87336 + longitude = 117.22743 + + # Setup sun component + hass.config.latitude = latitude + hass.config.longitude = longitude + assert await async_setup_component(hass, sun.DOMAIN, { + sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) + + # Get next sunrise/sunset + astral = Astral() + utc_now = datetime(2014, 5, 24, 12, 0, 0, tzinfo=dt_util.UTC) + utc_today = utc_now.date() + + mod = -1 + while True: + next_setting = (astral.sunset_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + # Track sunset + runs = [] + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub = async_track_sunset(hass, lambda: runs.append(1)) + + offset_runs = [] + offset = timedelta(minutes=30) + with patch('homeassistant.util.dt.utcnow', return_value=utc_now): + unsub2 = async_track_sunset( + hass, lambda: offset_runs.append(1), offset) + + # Run tests + _send_time_changed(hass, next_setting - offset) + await hass.async_block_till_done() + assert len(runs) == 0 + assert len(offset_runs) == 0 + + _send_time_changed(hass, next_setting) + await hass.async_block_till_done() + assert len(runs) == 1 + assert len(offset_runs) == 0 + + _send_time_changed(hass, next_setting + offset) + await hass.async_block_till_done() + assert len(runs) == 1 + assert len(offset_runs) == 1 + + unsub() + unsub2() + + _send_time_changed(hass, next_setting + offset) + await hass.async_block_till_done() + assert len(runs) == 1 + assert len(offset_runs) == 1 + + +async def test_async_track_time_change(hass): + """Test tracking time change.""" + wildcard_runs = [] + specific_runs = [] + + unsub = async_track_time_change(hass, + lambda x: wildcard_runs.append(1)) + unsub_utc = async_track_utc_time_change( + hass, lambda x: specific_runs.append(1), second=[0, 30]) + + _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 1 - unsub = track_utc_time_change( - self.hass, lambda x: specific_runs.append(1), hour='/2', minute=0, - second=0) + _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 15)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert len(wildcard_runs) == 2 - self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) + _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 3 - self._send_time_changed(datetime(2014, 5, 24, 22, 0, 0)) - self.hass.block_till_done() - assert 1 == len(specific_runs) + unsub() + unsub_utc() - self._send_time_changed(datetime(2014, 5, 25, 0, 0, 0)) - self.hass.block_till_done() - assert 2 == len(specific_runs) + _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 30)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + assert len(wildcard_runs) == 3 - unsub() - - def test_periodic_task_entering_dst(self): - """Test periodic task behavior when entering dst.""" - tz = dt_util.get_time_zone('Europe/Vienna') - dt_util.set_default_time_zone(tz) - specific_runs = [] - unsub = track_time_change( - self.hass, lambda x: specific_runs.append(1), hour=2, minute=30, - second=0) +async def test_periodic_task_minute(hass): + """Test periodic tasks per minute.""" + specific_runs = [] - self._send_time_changed( - tz.localize(datetime(2018, 3, 25, 1, 50, 0))) - self.hass.block_till_done() - assert 0 == len(specific_runs) - - self._send_time_changed( - tz.localize(datetime(2018, 3, 25, 3, 50, 0))) - self.hass.block_till_done() - assert 0 == len(specific_runs) - - self._send_time_changed( - tz.localize(datetime(2018, 3, 26, 1, 50, 0))) - self.hass.block_till_done() - assert 0 == len(specific_runs) + unsub = async_track_utc_time_change( + hass, lambda x: specific_runs.append(1), minute='/5', + second=0) - self._send_time_changed( - tz.localize(datetime(2018, 3, 26, 2, 50, 0))) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - unsub() - - def test_periodic_task_leaving_dst(self): - """Test periodic task behavior when leaving dst.""" - tz = dt_util.get_time_zone('Europe/Vienna') - dt_util.set_default_time_zone(tz) - specific_runs = [] - - unsub = track_time_change( - self.hass, lambda x: specific_runs.append(1), hour=2, minute=30, - second=0) - - self._send_time_changed( - tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False)) - self.hass.block_till_done() - assert 0 == len(specific_runs) - - self._send_time_changed( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed( - tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True)) - self.hass.block_till_done() - assert 1 == len(specific_runs) - - self._send_time_changed( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True)) - self.hass.block_till_done() - assert 2 == len(specific_runs) - - unsub() - - def test_call_later(self): - """Test calling an action later.""" - def action(): pass - now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) - - with patch('homeassistant.helpers.event' - '.async_track_point_in_utc_time') as mock, \ - patch('homeassistant.util.dt.utcnow', return_value=now): - call_later(self.hass, 3, action) - - assert len(mock.mock_calls) == 1 - p_hass, p_action, p_point = mock.mock_calls[0][1] - assert p_hass is self.hass - assert p_action is action - assert p_point == now + timedelta(seconds=3) - - -@asyncio.coroutine -def test_async_call_later(hass): + _send_time_changed(hass, datetime(2014, 5, 24, 12, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 24, 12, 3, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + unsub() + + _send_time_changed(hass, datetime(2014, 5, 24, 12, 5, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + +async def test_periodic_task_hour(hass): + """Test periodic tasks per hour.""" + specific_runs = [] + + unsub = async_track_utc_time_change( + hass, lambda x: specific_runs.append(1), hour='/2', + minute=0, second=0) + + _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + _send_time_changed(hass, datetime(2014, 5, 25, 1, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + unsub() + + _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + +async def test_periodic_task_wrong_input(hass): + """Test periodic tasks with wrong input.""" + specific_runs = [] + + with pytest.raises(ValueError): + async_track_utc_time_change( + hass, lambda x: specific_runs.append(1), hour='/two') + + _send_time_changed(hass, datetime(2014, 5, 2, 0, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + +async def test_periodic_task_clock_rollback(hass): + """Test periodic tasks with the time rolling backwards.""" + specific_runs = [] + + unsub = async_track_utc_time_change( + hass, lambda x: specific_runs.append(1), hour='/2', minute=0, + second=0) + + _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 24, 23, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + _send_time_changed(hass, datetime(2014, 5, 24, 0, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 3 + + _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 4 + + unsub() + + _send_time_changed(hass, datetime(2014, 5, 25, 2, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 4 + + +async def test_periodic_task_duplicate_time(hass): + """Test periodic tasks not triggering on duplicate time.""" + specific_runs = [] + + unsub = async_track_utc_time_change( + hass, lambda x: specific_runs.append(1), hour='/2', minute=0, + second=0) + + _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 24, 22, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, datetime(2014, 5, 25, 0, 0, 0)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + unsub() + + +async def test_periodic_task_entering_dst(hass): + """Test periodic task behavior when entering dst.""" + tz = dt_util.get_time_zone('Europe/Vienna') + dt_util.set_default_time_zone(tz) + specific_runs = [] + + unsub = async_track_time_change( + hass, lambda x: specific_runs.append(1), hour=2, minute=30, + second=0) + + _send_time_changed(hass, tz.localize(datetime(2018, 3, 25, 1, 50, 0))) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + _send_time_changed(hass, tz.localize(datetime(2018, 3, 25, 3, 50, 0))) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + _send_time_changed(hass, tz.localize(datetime(2018, 3, 26, 1, 50, 0))) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + _send_time_changed(hass, tz.localize(datetime(2018, 3, 26, 2, 50, 0))) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + unsub() + + +async def test_periodic_task_leaving_dst(hass): + """Test periodic task behavior when leaving dst.""" + tz = dt_util.get_time_zone('Europe/Vienna') + dt_util.set_default_time_zone(tz) + specific_runs = [] + + unsub = async_track_time_change( + hass, lambda x: specific_runs.append(1), hour=2, minute=30, + second=0) + + _send_time_changed(hass, tz.localize(datetime(2018, 10, 28, 2, 5, 0), + is_dst=False)) + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + _send_time_changed(hass, tz.localize(datetime(2018, 10, 28, 2, 55, 0), + is_dst=False)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, tz.localize(datetime(2018, 10, 28, 2, 5, 0), + is_dst=True)) + await hass.async_block_till_done() + assert len(specific_runs) == 1 + + _send_time_changed(hass, tz.localize(datetime(2018, 10, 28, 2, 55, 0), + is_dst=True)) + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + unsub() + + +async def test_call_later(hass): """Test calling an action later.""" - def action(): pass + def action(): + pass + now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) + + with patch('homeassistant.helpers.event' + '.async_track_point_in_utc_time') as mock, \ + patch('homeassistant.util.dt.utcnow', return_value=now): + async_call_later(hass, 3, action) + + assert len(mock.mock_calls) == 1 + p_hass, p_action, p_point = mock.mock_calls[0][1] + assert p_hass is hass + assert p_action is action + assert p_point == now + timedelta(seconds=3) + + +async def test_async_call_later(hass): + """Test calling an action later.""" + def action(): + pass now = datetime(2017, 12, 19, 15, 40, 0, tzinfo=dt_util.UTC) with patch('homeassistant.helpers.event' diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index 417dded790a..2168974b783 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -1,53 +1,43 @@ """Test Home Assistant icon util methods.""" -import unittest -class TestIconUtil(unittest.TestCase): - """Test icon util methods.""" +def test_battery_icon(): + """Test icon generator for battery sensor.""" + from homeassistant.helpers.icon import icon_for_battery_level - def test_battery_icon(self): - """Test icon generator for battery sensor.""" - from homeassistant.helpers.icon import icon_for_battery_level + assert icon_for_battery_level(None, True) == 'mdi:battery-unknown' + assert icon_for_battery_level(None, False) == 'mdi:battery-unknown' - assert 'mdi:battery-unknown' == \ - icon_for_battery_level(None, True) - assert 'mdi:battery-unknown' == \ - icon_for_battery_level(None, False) + assert icon_for_battery_level(5, True) == 'mdi:battery-outline' + assert icon_for_battery_level(5, False) == 'mdi:battery-alert' - assert 'mdi:battery-outline' == \ - icon_for_battery_level(5, True) - assert 'mdi:battery-alert' == \ - icon_for_battery_level(5, False) + assert icon_for_battery_level(100, True) == 'mdi:battery-charging-100' + assert icon_for_battery_level(100, False) == 'mdi:battery' - assert 'mdi:battery-charging-100' == \ - icon_for_battery_level(100, True) - assert 'mdi:battery' == \ - icon_for_battery_level(100, False) - - iconbase = 'mdi:battery' - for level in range(0, 100, 5): - print('Level: %d. icon: %s, charging: %s' - % (level, icon_for_battery_level(level, False), - icon_for_battery_level(level, True))) - if level <= 10: - postfix_charging = '-outline' - elif level <= 30: - postfix_charging = '-charging-20' - elif level <= 50: - postfix_charging = '-charging-40' - elif level <= 70: - postfix_charging = '-charging-60' - elif level <= 90: - postfix_charging = '-charging-80' - else: - postfix_charging = '-charging-100' - if 5 < level < 95: - postfix = '-{}'.format(int(round(level / 10 - .01)) * 10) - elif level <= 5: - postfix = '-alert' - else: - postfix = '' - assert iconbase + postfix == \ - icon_for_battery_level(level, False) - assert iconbase + postfix_charging == \ - icon_for_battery_level(level, True) + iconbase = 'mdi:battery' + for level in range(0, 100, 5): + print('Level: %d. icon: %s, charging: %s' + % (level, icon_for_battery_level(level, False), + icon_for_battery_level(level, True))) + if level <= 10: + postfix_charging = '-outline' + elif level <= 30: + postfix_charging = '-charging-20' + elif level <= 50: + postfix_charging = '-charging-40' + elif level <= 70: + postfix_charging = '-charging-60' + elif level <= 90: + postfix_charging = '-charging-80' + else: + postfix_charging = '-charging-100' + if 5 < level < 95: + postfix = '-{}'.format(int(round(level / 10 - .01)) * 10) + elif level <= 5: + postfix = '-alert' + else: + postfix = '' + assert iconbase + postfix == \ + icon_for_battery_level(level, False) + assert iconbase + postfix_charging == \ + icon_for_battery_level(level, True) diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index 6af28e686f0..104801c84bb 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -1,50 +1,35 @@ """Test component helpers.""" # pylint: disable=protected-access from collections import OrderedDict -import unittest from homeassistant import helpers -from tests.common import get_test_home_assistant + +def test_extract_domain_configs(): + """Test the extraction of domain configuration.""" + config = { + 'zone': None, + 'zoner': None, + 'zone ': None, + 'zone Hallo': None, + 'zone 100': None, + } + + assert set(['zone', 'zone Hallo', 'zone 100']) == \ + set(helpers.extract_domain_configs(config, 'zone')) -class TestHelpers(unittest.TestCase): - """Tests homeassistant.helpers module.""" +def test_config_per_platform(): + """Test config per platform method.""" + config = OrderedDict([ + ('zone', {'platform': 'hello'}), + ('zoner', None), + ('zone Hallo', [1, {'platform': 'hello 2'}]), + ('zone 100', None), + ]) - # pylint: disable=invalid-name - def setUp(self): - """Init needed objects.""" - self.hass = get_test_home_assistant() - - # pylint: disable=invalid-name - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_extract_domain_configs(self): - """Test the extraction of domain configuration.""" - config = { - 'zone': None, - 'zoner': None, - 'zone ': None, - 'zone Hallo': None, - 'zone 100': None, - } - - assert set(['zone', 'zone Hallo', 'zone 100']) == \ - set(helpers.extract_domain_configs(config, 'zone')) - - def test_config_per_platform(self): - """Test config per platform method.""" - config = OrderedDict([ - ('zone', {'platform': 'hello'}), - ('zoner', None), - ('zone Hallo', [1, {'platform': 'hello 2'}]), - ('zone 100', None), - ]) - - assert [ - ('hello', config['zone']), - (None, 1), - ('hello 2', config['zone Hallo'][1]), - ] == list(helpers.config_per_platform(config, 'zone')) + assert [ + ('hello', config['zone']), + (None, 1), + ('hello 2', config['zone Hallo'][1]), + ] == list(helpers.config_per_platform(config, 'zone')) diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 1a5b63fbab9..671c6f0d5ac 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -1,11 +1,11 @@ """Tests for the intent helpers.""" -import unittest import voluptuous as vol +import pytest + from homeassistant.core import State from homeassistant.helpers import (intent, config_validation as cv) -import pytest class MockIntentHandler(intent.IntentHandler): @@ -25,23 +25,20 @@ def test_async_match_state(): assert state is state1 -class TestIntentHandler(unittest.TestCase): - """Test the Home Assistant event helpers.""" +def test_async_validate_slots(): + """Test async_validate_slots of IntentHandler.""" + handler1 = MockIntentHandler({ + vol.Required('name'): cv.string, + }) - def test_async_validate_slots(self): - """Test async_validate_slots of IntentHandler.""" - handler1 = MockIntentHandler({ - vol.Required('name'): cv.string, - }) - - with pytest.raises(vol.error.MultipleInvalid): - handler1.async_validate_slots({}) - with pytest.raises(vol.error.MultipleInvalid): - handler1.async_validate_slots({'name': 1}) - with pytest.raises(vol.error.MultipleInvalid): - handler1.async_validate_slots({'name': 'kitchen'}) - handler1.async_validate_slots({'name': {'value': 'kitchen'}}) - handler1.async_validate_slots({ - 'name': {'value': 'kitchen'}, - 'probability': {'value': '0.5'} - }) + with pytest.raises(vol.error.MultipleInvalid): + handler1.async_validate_slots({}) + with pytest.raises(vol.error.MultipleInvalid): + handler1.async_validate_slots({'name': 1}) + with pytest.raises(vol.error.MultipleInvalid): + handler1.async_validate_slots({'name': 'kitchen'}) + handler1.async_validate_slots({'name': {'value': 'kitchen'}}) + handler1.async_validate_slots({ + 'name': {'value': 'kitchen'}, + 'probability': {'value': '0.5'} + }) diff --git a/tests/helpers/test_location.py b/tests/helpers/test_location.py index 5ff7abdbcdd..c48afde5f12 100644 --- a/tests/helpers/test_location.py +++ b/tests/helpers/test_location.py @@ -1,58 +1,57 @@ """Tests Home Assistant location helpers.""" -import unittest - from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.core import State from homeassistant.helpers import location -class TestHelpersLocation(unittest.TestCase): +def test_has_location_with_invalid_states(): """Set up the tests.""" - - def test_has_location_with_invalid_states(self): - """Set up the tests.""" - for state in (None, 1, "hello", object): - assert not location.has_location(state) - - def test_has_location_with_states_with_invalid_locations(self): - """Set up the tests.""" - state = State('hello.world', 'invalid', { - ATTR_LATITUDE: 'no number', - ATTR_LONGITUDE: 123.12 - }) + for state in (None, 1, "hello", object): assert not location.has_location(state) - def test_has_location_with_states_with_valid_location(self): - """Set up the tests.""" - state = State('hello.world', 'invalid', { - ATTR_LATITUDE: 123.12, - ATTR_LONGITUDE: 123.12 - }) - assert location.has_location(state) - def test_closest_with_no_states_with_location(self): - """Set up the tests.""" - state = State('light.test', 'on') - state2 = State('light.test', 'on', { - ATTR_LATITUDE: 'invalid', - ATTR_LONGITUDE: 123.45, - }) - state3 = State('light.test', 'on', { - ATTR_LONGITUDE: 123.45, - }) +def test_has_location_with_states_with_invalid_locations(): + """Set up the tests.""" + state = State('hello.world', 'invalid', { + ATTR_LATITUDE: 'no number', + ATTR_LONGITUDE: 123.12 + }) + assert not location.has_location(state) - assert \ - location.closest(123.45, 123.45, [state, state2, state3]) is None - def test_closest_returns_closest(self): - """Test .""" - state = State('light.test', 'on', { - ATTR_LATITUDE: 124.45, - ATTR_LONGITUDE: 124.45, - }) - state2 = State('light.test', 'on', { - ATTR_LATITUDE: 125.45, - ATTR_LONGITUDE: 125.45, - }) +def test_has_location_with_states_with_valid_location(): + """Set up the tests.""" + state = State('hello.world', 'invalid', { + ATTR_LATITUDE: 123.12, + ATTR_LONGITUDE: 123.12 + }) + assert location.has_location(state) - assert state == location.closest(123.45, 123.45, [state, state2]) + +def test_closest_with_no_states_with_location(): + """Set up the tests.""" + state = State('light.test', 'on') + state2 = State('light.test', 'on', { + ATTR_LATITUDE: 'invalid', + ATTR_LONGITUDE: 123.45, + }) + state3 = State('light.test', 'on', { + ATTR_LONGITUDE: 123.45, + }) + + assert \ + location.closest(123.45, 123.45, [state, state2, state3]) is None + + +def test_closest_returns_closest(): + """Test .""" + state = State('light.test', 'on', { + ATTR_LATITUDE: 124.45, + ATTR_LONGITUDE: 124.45, + }) + state2 = State('light.test', 'on', { + ATTR_LATITUDE: 125.45, + ATTR_LONGITUDE: 125.45, + }) + + assert state == location.closest(123.45, 123.45, [state, state2]) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index d04044d9b60..f9cd49ade1d 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1,9 +1,10 @@ """The tests for the Script component.""" # pylint: disable=protected-access from datetime import timedelta +import functools as ft from unittest import mock -import unittest +import asynctest import jinja2 import voluptuous as vol import pytest @@ -11,139 +12,130 @@ import pytest from homeassistant import exceptions from homeassistant.core import Context, callback # Otherwise can't test just this file (import order issue) -import homeassistant.components # noqa import homeassistant.util.dt as dt_util from homeassistant.helpers import script, config_validation as cv -from tests.common import fire_time_changed, get_test_home_assistant +from tests.common import async_fire_time_changed ENTITY_ID = 'script.test' -class TestScriptHelper(unittest.TestCase): - """Test the Script component.""" +async def test_firing_event(hass): + """Test the firing of events.""" + event = 'test_event' + context = Context() + calls = [] - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + @callback + def record_event(event): + """Add recorded event to set.""" + calls.append(event) - # pylint: disable=invalid-name - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.bus.async_listen(event, record_event) - def test_firing_event(self): - """Test the firing of events.""" - event = 'test_event' - context = Context() - calls = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - calls.append(event) - - self.hass.bus.listen(event, record_event) - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA({ - 'event': event, - 'event_data': { - 'hello': 'world' - } - })) - - script_obj.run(context=context) - - self.hass.block_till_done() - - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data.get('hello') == 'world' - assert not script_obj.can_cancel - - def test_firing_event_template(self): - """Test the firing of events.""" - event = 'test_event' - context = Context() - calls = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - calls.append(event) - - self.hass.bus.listen(event, record_event) - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA({ - 'event': event, - 'event_data_template': { - 'dict': { - 1: '{{ is_world }}', - 2: '{{ is_world }}{{ is_world }}', - 3: '{{ is_world }}{{ is_world }}{{ is_world }}', - }, - 'list': [ - '{{ is_world }}', '{{ is_world }}{{ is_world }}' - ] - } - })) - - script_obj.run({'is_world': 'yes'}, context=context) - - self.hass.block_till_done() - - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data == { - 'dict': { - 1: 'yes', - 2: 'yesyes', - 3: 'yesyesyes', - }, - 'list': ['yes', 'yesyes'] + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA({ + 'event': event, + 'event_data': { + 'hello': 'world' } - assert not script_obj.can_cancel + })) - def test_calling_service(self): - """Test the calling of a service.""" - calls = [] - context = Context() + await script_obj.async_run(context=context) - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) + await hass.async_block_till_done() - self.hass.services.register('test', 'script', record_call) + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get('hello') == 'world' + assert not script_obj.can_cancel - script.call_from_config(self.hass, { + +async def test_firing_event_template(hass): + """Test the firing of events.""" + event = 'test_event' + context = Context() + calls = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + calls.append(event) + + hass.bus.async_listen(event, record_event) + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA({ + 'event': event, + 'event_data_template': { + 'dict': { + 1: '{{ is_world }}', + 2: '{{ is_world }}{{ is_world }}', + 3: '{{ is_world }}{{ is_world }}{{ is_world }}', + }, + 'list': [ + '{{ is_world }}', '{{ is_world }}{{ is_world }}' + ] + } + })) + + await script_obj.async_run({'is_world': 'yes'}, context=context) + + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data == { + 'dict': { + 1: 'yes', + 2: 'yesyes', + 3: 'yesyesyes', + }, + 'list': ['yes', 'yesyes'] + } + assert not script_obj.can_cancel + + +async def test_calling_service(hass): + """Test the calling of a service.""" + calls = [] + context = Context() + + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + hass.services.async_register('test', 'script', record_call) + + hass.async_add_job( + ft.partial(script.call_from_config, hass, { 'service': 'test.script', 'data': { 'hello': 'world' } - }, context=context) + }, context=context)) - self.hass.block_till_done() + await hass.async_block_till_done() - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data.get('hello') == 'world' + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get('hello') == 'world' - def test_calling_service_template(self): - """Test the calling of a service.""" - calls = [] - context = Context() - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) +async def test_calling_service_template(hass): + """Test the calling of a service.""" + calls = [] + context = Context() - self.hass.services.register('test', 'script', record_call) + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) - script.call_from_config(self.hass, { + hass.services.async_register('test', 'script', record_call) + + hass.async_add_job( + ft.partial(script.call_from_config, hass, { 'service_template': """ {% if True %} test.script @@ -159,626 +151,644 @@ class TestScriptHelper(unittest.TestCase): {% endif %} """ } - }, {'is_world': 'yes'}, context=context) - - self.hass.block_till_done() - - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data.get('hello') == 'world' - - def test_delay(self): - """Test the delay.""" - event = 'test_event' - events = [] - context = Context() - delay_alias = 'delay step' - - @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': {'seconds': 5}, 'alias': delay_alias}, - {'event': event}])) - - script_obj.run(context=context) - self.hass.block_till_done() - - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == delay_alias - assert len(events) == 1 - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - assert not script_obj.is_running - assert len(events) == 2 - assert events[0].context is context - assert events[1].context is context - - def test_delay_template(self): - """Test the delay as a template.""" - event = 'test_event' - events = [] - delay_alias = 'delay step' - - @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': '00:00:{{ 5 }}', 'alias': delay_alias}, - {'event': event}])) - - script_obj.run() - self.hass.block_till_done() - - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == delay_alias - assert len(events) == 1 - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - 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_delay_complex_template(self): - """Test the delay with a working complex template.""" - event = 'test_event' - events = [] - delay_alias = 'delay step' - - @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': { - 'seconds': '{{ 5 }}'}, - 'alias': delay_alias}, - {'event': event}])) - - script_obj.run() - self.hass.block_till_done() - - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == delay_alias - assert len(events) == 1 - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - assert not script_obj.is_running - assert len(events) == 2 - - def test_delay_complex_invalid_template(self): - """Test the delay with a complex 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': { - 'seconds': '{{ 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' - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - self.hass.bus.listen(event, record_event) + }, {'is_world': 'yes'}, context=context)) - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'delay': {'seconds': 5}}, - {'event': event}])) - - script_obj.run() - self.hass.block_till_done() + await hass.async_block_till_done() - assert script_obj.is_running - assert len(events) == 0 - - script_obj.stop() + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get('hello') == 'world' - assert not script_obj.is_running - # Make sure the script is really stopped. - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_delay(hass): + """Test the delay.""" + event = 'test_event' + events = [] + context = Context() + delay_alias = 'delay step' - assert not script_obj.is_running - assert len(events) == 0 + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - def test_wait_template(self): - """Test the wait template.""" - event = 'test_event' - events = [] - context = Context() - wait_alias = 'wait step' + hass.bus.async_listen(event, record_event) - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': {'seconds': 5}, 'alias': delay_alias}, + {'event': event}])) - self.hass.bus.listen(event, record_event) + await script_obj.async_run(context=context) + await hass.async_block_till_done() - self.hass.states.set('switch.test', 'on') + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == delay_alias + assert len(events) == 1 - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - {'wait_template': "{{states.switch.test.state == 'off'}}", - 'alias': wait_alias}, - {'event': event}])) + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - script_obj.run(context=context) - self.hass.block_till_done() + assert not script_obj.is_running + assert len(events) == 2 + assert events[0].context is context + assert events[1].context is context - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == wait_alias - assert len(events) == 1 - self.hass.states.set('switch.test', 'off') - self.hass.block_till_done() +async def test_delay_template(hass): + """Test the delay as a template.""" + event = 'test_event' + events = [] + delay_alias = 'delay step' - assert not script_obj.is_running - assert len(events) == 2 - assert events[0].context is context - assert events[1].context is context + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - def test_wait_template_cancel(self): - """Test the wait template cancel action.""" - event = 'test_event' - events = [] - wait_alias = 'wait step' + hass.bus.async_listen(event, record_event) - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '00:00:{{ 5 }}', 'alias': delay_alias}, + {'event': event}])) - self.hass.bus.listen(event, record_event) + await script_obj.async_run() + await hass.async_block_till_done() - self.hass.states.set('switch.test', 'on') + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == delay_alias + assert len(events) == 1 - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - {'wait_template': "{{states.switch.test.state == 'off'}}", - 'alias': wait_alias}, - {'event': event}])) + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - script_obj.run() - self.hass.block_till_done() + assert not script_obj.is_running + assert len(events) == 2 - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == wait_alias - assert len(events) == 1 - script_obj.stop() +async def test_delay_invalid_template(hass): + """Test the delay as a template that fails.""" + event = 'test_event' + events = [] - assert not script_obj.is_running - assert len(events) == 1 + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - self.hass.states.set('switch.test', 'off') - self.hass.block_till_done() + hass.bus.async_listen(event, record_event) - assert not script_obj.is_running - assert len(events) == 1 + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': '{{ invalid_delay }}'}, + {'delay': {'seconds': 5}}, + {'event': event}])) - def test_wait_template_not_schedule(self): - """Test the wait template with correct condition.""" - event = 'test_event' - events = [] + with mock.patch.object(script, '_LOGGER') as mock_logger: + await script_obj.async_run() + await hass.async_block_till_done() + assert mock_logger.error.called - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) + assert not script_obj.is_running + assert len(events) == 1 - self.hass.bus.listen(event, record_event) - self.hass.states.set('switch.test', 'on') +async def test_delay_complex_template(hass): + """Test the delay with a working complex template.""" + event = 'test_event' + events = [] + delay_alias = 'delay step' - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - {'wait_template': "{{states.switch.test.state == 'on'}}"}, - {'event': event}])) + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - script_obj.run() - self.hass.block_till_done() + hass.bus.async_listen(event, record_event) - assert not script_obj.is_running - assert script_obj.can_cancel - assert len(events) == 2 + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': { + 'seconds': '{{ 5 }}'}, + 'alias': delay_alias}, + {'event': event}])) - def test_wait_template_timeout_halt(self): - """Test the wait template, halt on timeout.""" - event = 'test_event' - events = [] - wait_alias = 'wait step' + await script_obj.async_run() + await hass.async_block_till_done() - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == delay_alias + assert len(events) == 1 - self.hass.bus.listen(event, record_event) + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - self.hass.states.set('switch.test', 'on') + assert not script_obj.is_running + assert len(events) == 2 - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - { - 'wait_template': "{{states.switch.test.state == 'off'}}", - 'continue_on_timeout': False, - 'timeout': 5, - 'alias': wait_alias + +async def test_delay_complex_invalid_template(hass): + """Test the delay with a complex template that fails.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': { + 'seconds': '{{ invalid_delay }}' + }}, + {'delay': { + 'seconds': '{{ 5 }}' + }}, + {'event': event}])) + + with mock.patch.object(script, '_LOGGER') as mock_logger: + await script_obj.async_run() + await hass.async_block_till_done() + assert mock_logger.error.called + + assert not script_obj.is_running + assert len(events) == 1 + + +async def test_cancel_while_delay(hass): + """Test the cancelling while the delay is present.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'delay': {'seconds': 5}}, + {'event': event}])) + + await script_obj.async_run() + await hass.async_block_till_done() + + assert script_obj.is_running + assert len(events) == 0 + + script_obj.async_stop() + + assert not script_obj.is_running + + # Make sure the script is really stopped. + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 0 + + +async def test_wait_template(hass): + """Test the wait template.""" + event = 'test_event' + events = [] + context = Context() + wait_alias = 'wait step' + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'wait_template': "{{states.switch.test.state == 'off'}}", + 'alias': wait_alias}, + {'event': event}])) + + await script_obj.async_run(context=context) + await hass.async_block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == wait_alias + assert len(events) == 1 + + hass.states.async_set('switch.test', 'off') + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 2 + assert events[0].context is context + assert events[1].context is context + + +async def test_wait_template_cancel(hass): + """Test the wait template cancel action.""" + event = 'test_event' + events = [] + wait_alias = 'wait step' + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'wait_template': "{{states.switch.test.state == 'off'}}", + 'alias': wait_alias}, + {'event': event}])) + + await script_obj.async_run() + await hass.async_block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == wait_alias + assert len(events) == 1 + + script_obj.async_stop() + + assert not script_obj.is_running + assert len(events) == 1 + + hass.states.async_set('switch.test', 'off') + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 1 + + +async def test_wait_template_not_schedule(hass): + """Test the wait template with correct condition.""" + event = 'test_event' + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'wait_template': "{{states.switch.test.state == 'on'}}"}, + {'event': event}])) + + await script_obj.async_run() + await hass.async_block_till_done() + + assert not script_obj.is_running + assert script_obj.can_cancel + assert len(events) == 2 + + +async def test_wait_template_timeout_halt(hass): + """Test the wait template, halt on timeout.""" + event = 'test_event' + events = [] + wait_alias = 'wait step' + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + { + 'wait_template': "{{states.switch.test.state == 'off'}}", + 'continue_on_timeout': False, + 'timeout': 5, + 'alias': wait_alias + }, + {'event': event}])) + + await script_obj.async_run() + await hass.async_block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == wait_alias + assert len(events) == 1 + + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 1 + + +async def test_wait_template_timeout_continue(hass): + """Test the wait template with continuing the script.""" + event = 'test_event' + events = [] + wait_alias = 'wait step' + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + { + 'wait_template': "{{states.switch.test.state == 'off'}}", + 'timeout': 5, + 'continue_on_timeout': True, + 'alias': wait_alias + }, + {'event': event}])) + + await script_obj.async_run() + await hass.async_block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == wait_alias + assert len(events) == 1 + + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 2 + + +async def test_wait_template_timeout_default(hass): + """Test the wait template with default contiune.""" + event = 'test_event' + events = [] + wait_alias = 'wait step' + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + { + 'wait_template': "{{states.switch.test.state == 'off'}}", + 'timeout': 5, + 'alias': wait_alias + }, + {'event': event}])) + + await script_obj.async_run() + await hass.async_block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == wait_alias + assert len(events) == 1 + + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 2 + + +async def test_wait_template_variables(hass): + """Test the wait template with variables.""" + event = 'test_event' + events = [] + wait_alias = 'wait step' + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + hass.bus.async_listen(event, record_event) + + hass.states.async_set('switch.test', 'on') + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'wait_template': "{{is_state(data, 'off')}}", + 'alias': wait_alias}, + {'event': event}])) + + await script_obj.async_run({ + 'data': 'switch.test' + }) + await hass.async_block_till_done() + + assert script_obj.is_running + assert script_obj.can_cancel + assert script_obj.last_action == wait_alias + assert len(events) == 1 + + hass.states.async_set('switch.test', 'off') + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 2 + + +async def test_passing_variables_to_script(hass): + """Test if we can pass variables to script.""" + calls = [] + + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + hass.services.async_register('test', 'script', record_call) + + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + { + 'service': 'test.script', + 'data_template': { + 'hello': '{{ greeting }}', }, - {'event': event}])) - - script_obj.run() - self.hass.block_till_done() - - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == wait_alias - assert len(events) == 1 - - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() - - assert not script_obj.is_running - assert len(events) == 1 - - def test_wait_template_timeout_continue(self): - """Test the wait template with continuing the script.""" - event = 'test_event' - events = [] - wait_alias = 'wait step' - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - self.hass.bus.listen(event, record_event) - - self.hass.states.set('switch.test', 'on') - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - { - 'wait_template': "{{states.switch.test.state == 'off'}}", - 'timeout': 5, - 'continue_on_timeout': True, - 'alias': wait_alias + }, + {'delay': '{{ delay_period }}'}, + { + 'service': 'test.script', + 'data_template': { + 'hello': '{{ greeting2 }}', }, - {'event': event}])) + }])) - script_obj.run() - self.hass.block_till_done() + await script_obj.async_run({ + 'greeting': 'world', + 'greeting2': 'universe', + 'delay_period': '00:00:05' + }) - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == wait_alias - assert len(events) == 1 + await hass.async_block_till_done() - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + assert script_obj.is_running + assert len(calls) == 1 + assert calls[-1].data['hello'] == 'world' - assert not script_obj.is_running - assert len(events) == 2 + future = dt_util.utcnow() + timedelta(seconds=5) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - def test_wait_template_timeout_default(self): - """Test the wait template with default contiune.""" - event = 'test_event' - events = [] - wait_alias = 'wait step' + assert not script_obj.is_running + assert len(calls) == 2 + assert calls[-1].data['hello'] == 'universe' - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - self.hass.bus.listen(event, record_event) +async def test_condition(hass): + """Test if we can use conditions in a script.""" + event = 'test_event' + events = [] - self.hass.states.set('switch.test', 'on') + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - { - 'wait_template': "{{states.switch.test.state == 'off'}}", - 'timeout': 5, - 'alias': wait_alias - }, - {'event': event}])) + hass.bus.async_listen(event, record_event) - script_obj.run() - self.hass.block_till_done() + hass.states.async_set('test.entity', 'hello') - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == wait_alias - assert len(events) == 1 + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + { + 'condition': 'template', + 'value_template': '{{ states.test.entity.state == "hello" }}', + }, + {'event': event}, + ])) - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() + await script_obj.async_run() + await hass.async_block_till_done() + assert len(events) == 2 - assert not script_obj.is_running - assert len(events) == 2 + hass.states.async_set('test.entity', 'goodbye') - def test_wait_template_variables(self): - """Test the wait template with variables.""" - event = 'test_event' - events = [] - wait_alias = 'wait step' + await script_obj.async_run() + await hass.async_block_till_done() + assert len(events) == 3 - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - self.hass.bus.listen(event, record_event) +@asynctest.patch('homeassistant.helpers.script.condition.async_from_config') +async def test_condition_created_once(async_from_config, hass): + """Test that the conditions do not get created multiple times.""" + event = 'test_event' + events = [] - self.hass.states.set('switch.test', 'on') + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - {'wait_template': "{{is_state(data, 'off')}}", - 'alias': wait_alias}, - {'event': event}])) + hass.bus.async_listen(event, record_event) - script_obj.run({ - 'data': 'switch.test' - }) - self.hass.block_till_done() + hass.states.async_set('test.entity', 'hello') - assert script_obj.is_running - assert script_obj.can_cancel - assert script_obj.last_action == wait_alias - assert len(events) == 1 + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + { + 'condition': 'template', + 'value_template': '{{ states.test.entity.state == "hello" }}', + }, + {'event': event}, + ])) - self.hass.states.set('switch.test', 'off') - self.hass.block_till_done() + await script_obj.async_run() + await script_obj.async_run() + await hass.async_block_till_done() + assert async_from_config.call_count == 1 + assert len(script_obj._config_cache) == 1 - assert not script_obj.is_running - assert len(events) == 2 - def test_passing_variables_to_script(self): - """Test if we can pass variables to script.""" - calls = [] +async def test_all_conditions_cached(hass): + """Test that multiple conditions get cached.""" + event = 'test_event' + events = [] - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) - self.hass.services.register('test', 'script', record_call) + hass.bus.async_listen(event, record_event) - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - { - 'service': 'test.script', - 'data_template': { - 'hello': '{{ greeting }}', - }, - }, - {'delay': '{{ delay_period }}'}, - { - 'service': 'test.script', - 'data_template': { - 'hello': '{{ greeting2 }}', - }, - }])) + hass.states.async_set('test.entity', 'hello') - script_obj.run({ - 'greeting': 'world', - 'greeting2': 'universe', - 'delay_period': '00:00:05' - }) + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + { + 'condition': 'template', + 'value_template': '{{ states.test.entity.state == "hello" }}', + }, + { + 'condition': 'template', + 'value_template': '{{ states.test.entity.state != "hello" }}', + }, + {'event': event}, + ])) - self.hass.block_till_done() + await script_obj.async_run() + await hass.async_block_till_done() + assert len(script_obj._config_cache) == 2 - assert script_obj.is_running - assert len(calls) == 1 - assert calls[-1].data['hello'] == 'world' - future = dt_util.utcnow() + timedelta(seconds=5) - fire_time_changed(self.hass, future) - self.hass.block_till_done() +async def test_last_triggered(hass): + """Test the last_triggered.""" + event = 'test_event' - assert not script_obj.is_running - assert len(calls) == 2 - assert calls[-1].data['hello'] == 'universe' + script_obj = script.Script(hass, cv.SCRIPT_SCHEMA([ + {'event': event}, + {'delay': {'seconds': 5}}, + {'event': event}])) - def test_condition(self): - """Test if we can use conditions in a script.""" - event = 'test_event' - events = [] + assert script_obj.last_triggered is None - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) + time = dt_util.utcnow() + with mock.patch('homeassistant.helpers.script.date_util.utcnow', + return_value=time): + await script_obj.async_run() + await hass.async_block_till_done() - self.hass.bus.listen(event, record_event) - - self.hass.states.set('test.entity', 'hello') - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - { - 'condition': 'template', - 'value_template': '{{ states.test.entity.state == "hello" }}', - }, - {'event': event}, - ])) - - script_obj.run() - self.hass.block_till_done() - assert len(events) == 2 - - self.hass.states.set('test.entity', 'goodbye') - - script_obj.run() - self.hass.block_till_done() - assert len(events) == 3 - - @mock.patch('homeassistant.helpers.script.condition.async_from_config') - def test_condition_created_once(self, async_from_config): - """Test that the conditions do not get created multiple times.""" - event = 'test_event' - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - self.hass.bus.listen(event, record_event) - - self.hass.states.set('test.entity', 'hello') - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - { - 'condition': 'template', - 'value_template': '{{ states.test.entity.state == "hello" }}', - }, - {'event': event}, - ])) - - script_obj.run() - script_obj.run() - self.hass.block_till_done() - assert async_from_config.call_count == 1 - assert len(script_obj._config_cache) == 1 - - def test_all_conditions_cached(self): - """Test that multiple conditions get cached.""" - event = 'test_event' - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - self.hass.bus.listen(event, record_event) - - self.hass.states.set('test.entity', 'hello') - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - { - 'condition': 'template', - 'value_template': '{{ states.test.entity.state == "hello" }}', - }, - { - 'condition': 'template', - 'value_template': '{{ states.test.entity.state != "hello" }}', - }, - {'event': event}, - ])) - - script_obj.run() - self.hass.block_till_done() - assert len(script_obj._config_cache) == 2 - - def test_last_triggered(self): - """Test the last_triggered.""" - event = 'test_event' - - script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ - {'event': event}, - {'delay': {'seconds': 5}}, - {'event': event}])) - - assert script_obj.last_triggered is None - - time = dt_util.utcnow() - with mock.patch('homeassistant.helpers.script.date_util.utcnow', - return_value=time): - script_obj.run() - self.hass.block_till_done() - - assert script_obj.last_triggered == time + assert script_obj.last_triggered == time async def test_propagate_error_service_not_found(hass): @@ -873,18 +883,20 @@ def test_log_exception(): script_obj._exception_step = 1 for exc, msg in ( - (vol.Invalid("Invalid number"), 'Invalid data'), - (exceptions.TemplateError(jinja2.TemplateError('Unclosed bracket')), - 'Error rendering template'), - (exceptions.Unauthorized(), 'Unauthorized'), - (exceptions.ServiceNotFound('light', 'turn_on'), 'Service not found'), - (ValueError("Cannot parse JSON"), 'Unknown error'), + (vol.Invalid("Invalid number"), 'Invalid data'), + (exceptions.TemplateError( + jinja2.TemplateError('Unclosed bracket')), + 'Error rendering template'), + (exceptions.Unauthorized(), 'Unauthorized'), + (exceptions.ServiceNotFound('light', 'turn_on'), + 'Service not found'), + (ValueError("Cannot parse JSON"), 'Unknown error'), ): logger = mock.Mock() script_obj.async_log_exception(logger, 'Test error', exc) assert len(logger.mock_calls) == 1 - p_format, p_msg_base, p_error_desc, p_action_type, p_step, p_error = \ + _, _, p_error_desc, p_action_type, p_step, p_error = \ logger.mock_calls[0][1] assert p_error_desc == msg diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 10b053528ab..bc4e50f611c 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,13 +1,12 @@ """Test state helpers.""" import asyncio from datetime import timedelta -import unittest from unittest.mock import patch +import pytest + import homeassistant.core as ha -from homeassistant.setup import async_setup_component from homeassistant.const import (SERVICE_TURN_ON, SERVICE_TURN_OFF) -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.util import dt as dt_util from homeassistant.helpers import state from homeassistant.const import ( @@ -18,8 +17,7 @@ from homeassistant.const import ( from homeassistant.components.sun import (STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON) -from tests.common import get_test_home_assistant, mock_service -import pytest +from tests.common import async_mock_service @asyncio.coroutine @@ -82,141 +80,134 @@ def test_call_to_component(hass): context=context) -class TestStateHelpers(unittest.TestCase): - """Test the Home Assistant event helpers.""" +async def test_get_changed_since(hass): + """Test get_changed_since.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=5) + point3 = point2 + timedelta(seconds=5) - def setUp(self): # pylint: disable=invalid-name - """Run when tests are started.""" - self.hass = get_test_home_assistant() - run_coroutine_threadsafe(async_setup_component( - self.hass, 'homeassistant', {}), self.hass.loop).result() + with patch('homeassistant.core.dt_util.utcnow', return_value=point1): + hass.states.async_set('light.test', 'on') + state1 = hass.states.get('light.test') - def tearDown(self): # pylint: disable=invalid-name - """Stop when tests are finished.""" - self.hass.stop() + with patch('homeassistant.core.dt_util.utcnow', return_value=point2): + hass.states.async_set('light.test2', 'on') + state2 = hass.states.get('light.test2') - def test_get_changed_since(self): - """Test get_changed_since.""" - point1 = dt_util.utcnow() - point2 = point1 + timedelta(seconds=5) - point3 = point2 + timedelta(seconds=5) + with patch('homeassistant.core.dt_util.utcnow', return_value=point3): + hass.states.async_set('light.test3', 'on') + state3 = hass.states.get('light.test3') - with patch('homeassistant.core.dt_util.utcnow', return_value=point1): - self.hass.states.set('light.test', 'on') - state1 = self.hass.states.get('light.test') + assert [state2, state3] == \ + state.get_changed_since([state1, state2, state3], point2) - with patch('homeassistant.core.dt_util.utcnow', return_value=point2): - self.hass.states.set('light.test2', 'on') - state2 = self.hass.states.get('light.test2') - with patch('homeassistant.core.dt_util.utcnow', return_value=point3): - self.hass.states.set('light.test3', 'on') - state3 = self.hass.states.get('light.test3') +async def test_reproduce_with_no_entity(hass): + """Test reproduce_state with no entity.""" + calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - assert [state2, state3] == \ - state.get_changed_since([state1, state2, state3], point2) + await state.async_reproduce_state(hass, ha.State('light.test', 'on')) - def test_reproduce_with_no_entity(self): - """Test reproduce_state with no entity.""" - calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + await hass.async_block_till_done() - state.reproduce_state(self.hass, ha.State('light.test', 'on')) + assert len(calls) == 0 + assert hass.states.get('light.test') is None - self.hass.block_till_done() - assert len(calls) == 0 - assert self.hass.states.get('light.test') is None +async def test_reproduce_turn_on(hass): + """Test reproduce_state with SERVICE_TURN_ON.""" + calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - def test_reproduce_turn_on(self): - """Test reproduce_state with SERVICE_TURN_ON.""" - calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + hass.states.async_set('light.test', 'off') - self.hass.states.set('light.test', 'off') + await state.async_reproduce_state(hass, ha.State('light.test', 'on')) - state.reproduce_state(self.hass, ha.State('light.test', 'on')) + await hass.async_block_till_done() - self.hass.block_till_done() + assert len(calls) > 0 + last_call = calls[-1] + assert last_call.domain == 'light' + assert SERVICE_TURN_ON == last_call.service + assert ['light.test'] == last_call.data.get('entity_id') - assert len(calls) > 0 - last_call = calls[-1] - assert 'light' == last_call.domain - assert SERVICE_TURN_ON == last_call.service - assert ['light.test'] == last_call.data.get('entity_id') - def test_reproduce_turn_off(self): - """Test reproduce_state with SERVICE_TURN_OFF.""" - calls = mock_service(self.hass, 'light', SERVICE_TURN_OFF) +async def test_reproduce_turn_off(hass): + """Test reproduce_state with SERVICE_TURN_OFF.""" + calls = async_mock_service(hass, 'light', SERVICE_TURN_OFF) - self.hass.states.set('light.test', 'on') + hass.states.async_set('light.test', 'on') - state.reproduce_state(self.hass, ha.State('light.test', 'off')) + await state.async_reproduce_state(hass, ha.State('light.test', 'off')) - self.hass.block_till_done() + await hass.async_block_till_done() - assert len(calls) > 0 - last_call = calls[-1] - assert 'light' == last_call.domain - assert SERVICE_TURN_OFF == last_call.service - assert ['light.test'] == last_call.data.get('entity_id') + assert len(calls) > 0 + last_call = calls[-1] + assert last_call.domain == 'light' + assert SERVICE_TURN_OFF == last_call.service + assert ['light.test'] == last_call.data.get('entity_id') - def test_reproduce_complex_data(self): - """Test reproduce_state with complex service data.""" - calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) - self.hass.states.set('light.test', 'off') +async def test_reproduce_complex_data(hass): + """Test reproduce_state with complex service data.""" + calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - complex_data = ['hello', {'11': '22'}] + hass.states.async_set('light.test', 'off') - state.reproduce_state(self.hass, ha.State('light.test', 'on', { - 'complex': complex_data - })) + complex_data = ['hello', {'11': '22'}] - self.hass.block_till_done() + await state.async_reproduce_state(hass, ha.State('light.test', 'on', { + 'complex': complex_data + })) - assert len(calls) > 0 - last_call = calls[-1] - assert 'light' == last_call.domain - assert SERVICE_TURN_ON == last_call.service - assert complex_data == last_call.data.get('complex') + await hass.async_block_till_done() - def test_reproduce_bad_state(self): - """Test reproduce_state with bad state.""" - calls = mock_service(self.hass, 'light', SERVICE_TURN_ON) + assert len(calls) > 0 + last_call = calls[-1] + assert last_call.domain == 'light' + assert SERVICE_TURN_ON == last_call.service + assert complex_data == last_call.data.get('complex') - self.hass.states.set('light.test', 'off') - state.reproduce_state(self.hass, ha.State('light.test', 'bad')) +async def test_reproduce_bad_state(hass): + """Test reproduce_state with bad state.""" + calls = async_mock_service(hass, 'light', SERVICE_TURN_ON) - self.hass.block_till_done() + hass.states.async_set('light.test', 'off') - assert len(calls) == 0 - assert 'off' == self.hass.states.get('light.test').state + await state.async_reproduce_state(hass, ha.State('light.test', 'bad')) - def test_as_number_states(self): - """Test state_as_number with states.""" - zero_states = (STATE_OFF, STATE_CLOSED, STATE_UNLOCKED, - STATE_BELOW_HORIZON, STATE_NOT_HOME) - one_states = (STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_ABOVE_HORIZON, - STATE_HOME) - for _state in zero_states: - assert 0 == state.state_as_number( - ha.State('domain.test', _state, {})) - for _state in one_states: - assert 1 == state.state_as_number( - ha.State('domain.test', _state, {})) + await hass.async_block_till_done() - def test_as_number_coercion(self): - """Test state_as_number with number.""" - for _state in ('0', '0.0', 0, 0.0): - assert 0.0 == state.state_as_number( - ha.State('domain.test', _state, {})) - for _state in ('1', '1.0', 1, 1.0): - assert 1.0 == state.state_as_number( - ha.State('domain.test', _state, {})) + assert len(calls) == 0 + assert hass.states.get('light.test').state == 'off' - def test_as_number_invalid_cases(self): - """Test state_as_number with invalid cases.""" - for _state in ('', 'foo', 'foo.bar', None, False, True, object, - object()): - with pytest.raises(ValueError): - state.state_as_number(ha.State('domain.test', _state, {})) + +async def test_as_number_states(hass): + """Test state_as_number with states.""" + zero_states = (STATE_OFF, STATE_CLOSED, STATE_UNLOCKED, + STATE_BELOW_HORIZON, STATE_NOT_HOME) + one_states = (STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_ABOVE_HORIZON, + STATE_HOME) + for _state in zero_states: + assert state.state_as_number(ha.State('domain.test', _state, {})) == 0 + for _state in one_states: + assert state.state_as_number(ha.State('domain.test', _state, {})) == 1 + + +async def test_as_number_coercion(hass): + """Test state_as_number with number.""" + for _state in ('0', '0.0', 0, 0.0): + assert state.state_as_number( + ha.State('domain.test', _state, {})) == 0.0 + for _state in ('1', '1.0', 1, 1.0): + assert state.state_as_number( + ha.State('domain.test', _state, {})) == 1.0 + + +async def test_as_number_invalid_cases(hass): + """Test state_as_number with invalid cases.""" + for _state in ('', 'foo', 'foo.bar', None, False, True, object, + object()): + with pytest.raises(ValueError): + state.state_as_number(ha.State('domain.test', _state, {})) diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index af639e69432..51978194b03 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,6 +1,5 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access -import unittest from unittest.mock import patch from datetime import timedelta, datetime @@ -8,223 +7,214 @@ from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET import homeassistant.util.dt as dt_util import homeassistant.helpers.sun as sun -from tests.common import get_test_home_assistant + +def test_next_events(hass): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral + + astral = Astral() + utc_today = utc_now.date() + + latitude = hass.config.latitude + longitude = hass.config.longitude + + mod = -1 + while True: + next_dawn = (astral.dawn_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_dawn > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_dusk = (astral.dusk_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_dusk > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_midnight = (astral.solar_midnight_utc( + utc_today + timedelta(days=mod), longitude)) + if next_midnight > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_noon = (astral.solar_noon_utc( + utc_today + timedelta(days=mod), longitude)) + if next_noon > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_rising = (astral.sunrise_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_rising > utc_now: + break + mod += 1 + + mod = -1 + while True: + next_setting = (astral.sunset_utc( + utc_today + timedelta(days=mod), latitude, longitude)) + if next_setting > utc_now: + break + mod += 1 + + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + assert next_dawn == sun.get_astral_event_next( + hass, 'dawn') + assert next_dusk == sun.get_astral_event_next( + hass, 'dusk') + assert next_midnight == sun.get_astral_event_next( + hass, 'solar_midnight') + assert next_noon == sun.get_astral_event_next( + hass, 'solar_noon') + assert next_rising == sun.get_astral_event_next( + hass, SUN_EVENT_SUNRISE) + assert next_setting == sun.get_astral_event_next( + hass, SUN_EVENT_SUNSET) -# pylint: disable=invalid-name -class TestSun(unittest.TestCase): - """Test the sun helpers.""" +def test_date_events(hass): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() + astral = Astral() + utc_today = utc_now.date() - def tearDown(self): - """Stop everything that was started.""" - self.hass.stop() + latitude = hass.config.latitude + longitude = hass.config.longitude - def test_next_events(self): - """Test retrieving next sun events.""" - utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral + dawn = astral.dawn_utc(utc_today, latitude, longitude) + dusk = astral.dusk_utc(utc_today, latitude, longitude) + midnight = astral.solar_midnight_utc(utc_today, longitude) + noon = astral.solar_noon_utc(utc_today, longitude) + sunrise = astral.sunrise_utc(utc_today, latitude, longitude) + sunset = astral.sunset_utc(utc_today, latitude, longitude) - astral = Astral() - utc_today = utc_now.date() + assert dawn == sun.get_astral_event_date( + hass, 'dawn', utc_today) + assert dusk == sun.get_astral_event_date( + hass, 'dusk', utc_today) + assert midnight == sun.get_astral_event_date( + hass, 'solar_midnight', utc_today) + assert noon == sun.get_astral_event_date( + hass, 'solar_noon', utc_today) + assert sunrise == sun.get_astral_event_date( + hass, SUN_EVENT_SUNRISE, utc_today) + assert sunset == sun.get_astral_event_date( + hass, SUN_EVENT_SUNSET, utc_today) - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude - mod = -1 - while True: - next_dawn = (astral.dawn_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_dawn > utc_now: - break - mod += 1 +def test_date_events_default_date(hass): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral - mod = -1 - while True: - next_dusk = (astral.dusk_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_dusk > utc_now: - break - mod += 1 + astral = Astral() + utc_today = utc_now.date() - mod = -1 - while True: - next_midnight = (astral.solar_midnight_utc( - utc_today + timedelta(days=mod), longitude)) - if next_midnight > utc_now: - break - mod += 1 + latitude = hass.config.latitude + longitude = hass.config.longitude - mod = -1 - while True: - next_noon = (astral.solar_noon_utc( - utc_today + timedelta(days=mod), longitude)) - if next_noon > utc_now: - break - mod += 1 - - mod = -1 - while True: - next_rising = (astral.sunrise_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_rising > utc_now: - break - mod += 1 - - mod = -1 - while True: - next_setting = (astral.sunset_utc( - utc_today + timedelta(days=mod), latitude, longitude)) - if next_setting > utc_now: - break - mod += 1 - - with patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=utc_now): - assert next_dawn == sun.get_astral_event_next( - self.hass, 'dawn') - assert next_dusk == sun.get_astral_event_next( - self.hass, 'dusk') - assert next_midnight == sun.get_astral_event_next( - self.hass, 'solar_midnight') - assert next_noon == sun.get_astral_event_next( - self.hass, 'solar_noon') - assert next_rising == sun.get_astral_event_next( - self.hass, SUN_EVENT_SUNRISE) - assert next_setting == sun.get_astral_event_next( - self.hass, SUN_EVENT_SUNSET) - - def test_date_events(self): - """Test retrieving next sun events.""" - utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral - - astral = Astral() - utc_today = utc_now.date() - - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude - - dawn = astral.dawn_utc(utc_today, latitude, longitude) - dusk = astral.dusk_utc(utc_today, latitude, longitude) - midnight = astral.solar_midnight_utc(utc_today, longitude) - noon = astral.solar_noon_utc(utc_today, longitude) - sunrise = astral.sunrise_utc(utc_today, latitude, longitude) - sunset = astral.sunset_utc(utc_today, latitude, longitude) + dawn = astral.dawn_utc(utc_today, latitude, longitude) + dusk = astral.dusk_utc(utc_today, latitude, longitude) + midnight = astral.solar_midnight_utc(utc_today, longitude) + noon = astral.solar_noon_utc(utc_today, longitude) + sunrise = astral.sunrise_utc(utc_today, latitude, longitude) + sunset = astral.sunset_utc(utc_today, latitude, longitude) + with patch('homeassistant.util.dt.now', return_value=utc_now): assert dawn == sun.get_astral_event_date( - self.hass, 'dawn', utc_today) + hass, 'dawn', utc_today) assert dusk == sun.get_astral_event_date( - self.hass, 'dusk', utc_today) + hass, 'dusk', utc_today) assert midnight == sun.get_astral_event_date( - self.hass, 'solar_midnight', utc_today) + hass, 'solar_midnight', utc_today) assert noon == sun.get_astral_event_date( - self.hass, 'solar_noon', utc_today) + hass, 'solar_noon', utc_today) assert sunrise == sun.get_astral_event_date( - self.hass, SUN_EVENT_SUNRISE, utc_today) + hass, SUN_EVENT_SUNRISE, utc_today) assert sunset == sun.get_astral_event_date( - self.hass, SUN_EVENT_SUNSET, utc_today) + hass, SUN_EVENT_SUNSET, utc_today) - def test_date_events_default_date(self): - """Test retrieving next sun events.""" - utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral - astral = Astral() - utc_today = utc_now.date() +def test_date_events_accepts_datetime(hass): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) + from astral import Astral - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude + astral = Astral() + utc_today = utc_now.date() - dawn = astral.dawn_utc(utc_today, latitude, longitude) - dusk = astral.dusk_utc(utc_today, latitude, longitude) - midnight = astral.solar_midnight_utc(utc_today, longitude) - noon = astral.solar_noon_utc(utc_today, longitude) - sunrise = astral.sunrise_utc(utc_today, latitude, longitude) - sunset = astral.sunset_utc(utc_today, latitude, longitude) + latitude = hass.config.latitude + longitude = hass.config.longitude - with patch('homeassistant.util.dt.now', return_value=utc_now): - assert dawn == sun.get_astral_event_date( - self.hass, 'dawn', utc_today) - assert dusk == sun.get_astral_event_date( - self.hass, 'dusk', utc_today) - assert midnight == sun.get_astral_event_date( - self.hass, 'solar_midnight', utc_today) - assert noon == sun.get_astral_event_date( - self.hass, 'solar_noon', utc_today) - assert sunrise == sun.get_astral_event_date( - self.hass, SUN_EVENT_SUNRISE, utc_today) - assert sunset == sun.get_astral_event_date( - self.hass, SUN_EVENT_SUNSET, utc_today) + dawn = astral.dawn_utc(utc_today, latitude, longitude) + dusk = astral.dusk_utc(utc_today, latitude, longitude) + midnight = astral.solar_midnight_utc(utc_today, longitude) + noon = astral.solar_noon_utc(utc_today, longitude) + sunrise = astral.sunrise_utc(utc_today, latitude, longitude) + sunset = astral.sunset_utc(utc_today, latitude, longitude) - def test_date_events_accepts_datetime(self): - """Test retrieving next sun events.""" - utc_now = datetime(2016, 11, 1, 8, 0, 0, tzinfo=dt_util.UTC) - from astral import Astral + assert dawn == sun.get_astral_event_date( + hass, 'dawn', utc_now) + assert dusk == sun.get_astral_event_date( + hass, 'dusk', utc_now) + assert midnight == sun.get_astral_event_date( + hass, 'solar_midnight', utc_now) + assert noon == sun.get_astral_event_date( + hass, 'solar_noon', utc_now) + assert sunrise == sun.get_astral_event_date( + hass, SUN_EVENT_SUNRISE, utc_now) + assert sunset == sun.get_astral_event_date( + hass, SUN_EVENT_SUNSET, utc_now) - astral = Astral() - utc_today = utc_now.date() - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude +def test_is_up(hass): + """Test retrieving next sun events.""" + utc_now = datetime(2016, 11, 1, 12, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + assert not sun.is_up(hass) - dawn = astral.dawn_utc(utc_today, latitude, longitude) - dusk = astral.dusk_utc(utc_today, latitude, longitude) - midnight = astral.solar_midnight_utc(utc_today, longitude) - noon = astral.solar_noon_utc(utc_today, longitude) - sunrise = astral.sunrise_utc(utc_today, latitude, longitude) - sunset = astral.sunset_utc(utc_today, latitude, longitude) + utc_now = datetime(2016, 11, 1, 18, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.helpers.condition.dt_util.utcnow', + return_value=utc_now): + assert sun.is_up(hass) - assert dawn == sun.get_astral_event_date( - self.hass, 'dawn', utc_now) - assert dusk == sun.get_astral_event_date( - self.hass, 'dusk', utc_now) - assert midnight == sun.get_astral_event_date( - self.hass, 'solar_midnight', utc_now) - assert noon == sun.get_astral_event_date( - self.hass, 'solar_noon', utc_now) - assert sunrise == sun.get_astral_event_date( - self.hass, SUN_EVENT_SUNRISE, utc_now) - assert sunset == sun.get_astral_event_date( - self.hass, SUN_EVENT_SUNSET, utc_now) - def test_is_up(self): - """Test retrieving next sun events.""" - utc_now = datetime(2016, 11, 1, 12, 0, 0, tzinfo=dt_util.UTC) - with patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=utc_now): - assert not sun.is_up(self.hass) +def test_norway_in_june(hass): + """Test location in Norway where the sun doesn't set in summer.""" + hass.config.latitude = 69.6 + hass.config.longitude = 18.8 - utc_now = datetime(2016, 11, 1, 18, 0, 0, tzinfo=dt_util.UTC) - with patch('homeassistant.helpers.condition.dt_util.utcnow', - return_value=utc_now): - assert sun.is_up(self.hass) + june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) - def test_norway_in_june(self): - """Test location in Norway where the sun doesn't set in summer.""" - self.hass.config.latitude = 69.6 - self.hass.config.longitude = 18.8 + print(sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, + datetime(2017, 7, 25))) + print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, + datetime(2017, 7, 25))) - june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) + print(sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, + datetime(2017, 7, 26))) + print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, + datetime(2017, 7, 26))) - print(sun.get_astral_event_date(self.hass, SUN_EVENT_SUNRISE, - datetime(2017, 7, 25))) - print(sun.get_astral_event_date(self.hass, SUN_EVENT_SUNSET, - datetime(2017, 7, 25))) - - print(sun.get_astral_event_date(self.hass, SUN_EVENT_SUNRISE, - datetime(2017, 7, 26))) - print(sun.get_astral_event_date(self.hass, SUN_EVENT_SUNSET, - datetime(2017, 7, 26))) - - assert sun.get_astral_event_next(self.hass, SUN_EVENT_SUNRISE, june) \ - == datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) - assert sun.get_astral_event_next(self.hass, SUN_EVENT_SUNSET, june) \ - == datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) - assert sun.get_astral_event_date(self.hass, SUN_EVENT_SUNRISE, june) \ - is None - assert sun.get_astral_event_date(self.hass, SUN_EVENT_SUNSET, june) \ - is None + assert sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, june) \ + == datetime(2016, 7, 25, 23, 23, 39, tzinfo=dt_util.UTC) + assert sun.get_astral_event_next(hass, SUN_EVENT_SUNSET, june) \ + == datetime(2016, 7, 26, 22, 19, 1, tzinfo=dt_util.UTC) + assert sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, june) \ + is None + assert sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, june) \ + is None diff --git a/tests/helpers/test_temperature.py b/tests/helpers/test_temperature.py index f6bee7b12ae..a506288b627 100644 --- a/tests/helpers/test_temperature.py +++ b/tests/helpers/test_temperature.py @@ -1,50 +1,34 @@ """Tests Home Assistant temperature helpers.""" -import unittest - -from tests.common import get_test_home_assistant +import pytest from homeassistant.const import ( TEMP_CELSIUS, PRECISION_WHOLE, TEMP_FAHRENHEIT, PRECISION_HALVES, PRECISION_TENTHS) from homeassistant.helpers.temperature import display_temp -from homeassistant.util.unit_system import METRIC_SYSTEM -import pytest TEMP = 24.636626 -class TestHelpersTemperature(unittest.TestCase): - """Set up the temperature tests.""" +def test_temperature_not_a_number(hass): + """Test that temperature is a number.""" + temp = "Temperature" + with pytest.raises(Exception) as exception: + display_temp(hass, temp, TEMP_CELSIUS, PRECISION_HALVES) - def setUp(self): - """Set up the tests.""" - self.hass = get_test_home_assistant() - self.hass.config.unit_system = METRIC_SYSTEM + assert "Temperature is not a number: {}".format(temp) \ + in str(exception) - def tearDown(self): - """Stop down stuff we started.""" - self.hass.stop() - def test_temperature_not_a_number(self): - """Test that temperature is a number.""" - temp = "Temperature" - with pytest.raises(Exception) as exception: - display_temp(self.hass, temp, TEMP_CELSIUS, PRECISION_HALVES) +def test_celsius_halves(hass): + """Test temperature to celsius rounding to halves.""" + assert display_temp(hass, TEMP, TEMP_CELSIUS, PRECISION_HALVES) == 24.5 - assert "Temperature is not a number: {}".format(temp) \ - in str(exception) - def test_celsius_halves(self): - """Test temperature to celsius rounding to halves.""" - assert 24.5 == display_temp( - self.hass, TEMP, TEMP_CELSIUS, PRECISION_HALVES) +def test_celsius_tenths(hass): + """Test temperature to celsius rounding to tenths.""" + assert display_temp(hass, TEMP, TEMP_CELSIUS, PRECISION_TENTHS) == 24.6 - def test_celsius_tenths(self): - """Test temperature to celsius rounding to tenths.""" - assert 24.6 == display_temp( - self.hass, TEMP, TEMP_CELSIUS, PRECISION_TENTHS) - def test_fahrenheit_wholes(self): - """Test temperature to fahrenheit rounding to wholes.""" - assert -4 == display_temp( - self.hass, TEMP, TEMP_FAHRENHEIT, PRECISION_WHOLE) +def test_fahrenheit_wholes(hass): + """Test temperature to fahrenheit rounding to wholes.""" + assert display_temp(hass, TEMP, TEMP_FAHRENHEIT, PRECISION_WHOLE) == -4 diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index e0aeb09976d..d543bf65bb7 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -1,12 +1,12 @@ """Test Home Assistant template helper methods.""" -import asyncio from datetime import datetime -import unittest import random import math -import pytz from unittest.mock import patch +import pytest +import pytz + from homeassistant.components import group from homeassistant.exceptions import TemplateError from homeassistant.helpers import template @@ -21,1021 +21,1030 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant -import pytest + +def _set_up_units(hass): + """Set up the tests.""" + hass.config.units = UnitSystem('custom', TEMP_CELSIUS, + LENGTH_METERS, VOLUME_LITERS, + MASS_GRAMS, PRESSURE_PA) -class TestHelpersTemplate(unittest.TestCase): - """Test the Template.""" +def test_referring_states_by_entity_id(hass): + """Test referring states by entity id.""" + hass.states.async_set('test.object', 'happy') + assert template.Template( + '{{ states.test.object.state }}', hass).async_render() == 'happy' - # pylint: disable=invalid-name - def setUp(self): - """Set up the tests.""" - self.hass = get_test_home_assistant() - self.hass.config.units = UnitSystem('custom', TEMP_CELSIUS, - LENGTH_METERS, VOLUME_LITERS, - MASS_GRAMS, PRESSURE_PA) - # pylint: disable=invalid-name - def tearDown(self): - """Stop down stuff we started.""" - self.hass.stop() +def test_iterating_all_states(hass): + """Test iterating all states.""" + hass.states.async_set('test.object', 'happy') + hass.states.async_set('sensor.temperature', 10) - def test_referring_states_by_entity_id(self): - """Test referring states by entity id.""" - self.hass.states.set('test.object', 'happy') - assert 'happy' == \ - template.Template( - '{{ states.test.object.state }}', self.hass).render() + assert template.Template( + '{% for state in states %}{{ state.state }}{% endfor %}', + hass).async_render() == '10happy' - def test_iterating_all_states(self): - """Test iterating all states.""" - self.hass.states.set('test.object', 'happy') - self.hass.states.set('sensor.temperature', 10) - assert '10happy' == \ - template.Template( - '{% for state in states %}{{ state.state }}{% endfor %}', - self.hass).render() +def test_iterating_domain_states(hass): + """Test iterating domain states.""" + hass.states.async_set('test.object', 'happy') + hass.states.async_set('sensor.back_door', 'open') + hass.states.async_set('sensor.temperature', 10) - def test_iterating_domain_states(self): - """Test iterating domain states.""" - self.hass.states.set('test.object', 'happy') - self.hass.states.set('sensor.back_door', 'open') - self.hass.states.set('sensor.temperature', 10) - - assert 'open10' == \ - template.Template(""" + assert template.Template(""" {% for state in states.sensor %}{{ state.state }}{% endfor %} - """, self.hass).render() + """, hass).async_render() == 'open10' - def test_float(self): - """Test float.""" - self.hass.states.set('sensor.temperature', '12') - assert '12.0' == \ - template.Template( - '{{ float(states.sensor.temperature.state) }}', - self.hass).render() +def test_float(hass): + """Test float.""" + hass.states.async_set('sensor.temperature', '12') - assert 'True' == \ - template.Template( - '{{ float(states.sensor.temperature.state) > 11 }}', - self.hass).render() + assert template.Template( + '{{ float(states.sensor.temperature.state) }}', + hass).async_render() == '12.0' - def test_rounding_value(self): - """Test rounding value.""" - self.hass.states.set('sensor.temperature', 12.78) + assert template.Template( + '{{ float(states.sensor.temperature.state) > 11 }}', + hass).async_render() == 'True' - assert '12.8' == \ - template.Template( - '{{ states.sensor.temperature.state | round(1) }}', - self.hass).render() - assert '128' == \ - template.Template( - '{{ states.sensor.temperature.state | multiply(10) | round }}', - self.hass).render() +def test_rounding_value(hass): + """Test rounding value.""" + hass.states.async_set('sensor.temperature', 12.78) - assert '12.7' == \ - template.Template( - '{{ states.sensor.temperature.state | round(1, "floor") }}', - self.hass).render() + assert template.Template( + '{{ states.sensor.temperature.state | round(1) }}', + hass).async_render() == '12.8' - assert '12.8' == \ - template.Template( - '{{ states.sensor.temperature.state | round(1, "ceil") }}', - self.hass).render() + assert template.Template( + '{{ states.sensor.temperature.state | multiply(10) | round }}', + hass).async_render() == '128' - def test_rounding_value_get_original_value_on_error(self): - """Test rounding value get original value on error.""" - assert 'None' == \ - template.Template('{{ None | round }}', self.hass).render() + assert template.Template( + '{{ states.sensor.temperature.state | round(1, "floor") }}', + hass).async_render() == '12.7' - assert 'no_number' == \ - template.Template( - '{{ "no_number" | round }}', self.hass).render() + assert template.Template( + '{{ states.sensor.temperature.state | round(1, "ceil") }}', + hass).async_render() == '12.8' - def test_multiply(self): - """Test multiply.""" - tests = { - None: 'None', - 10: '100', - '"abcd"': 'abcd' - } - for inp, out in tests.items(): - assert out == \ - template.Template('{{ %s | multiply(10) | round }}' % inp, - self.hass).render() +def test_rounding_value_get_original_value_on_error(hass): + """Test rounding value get original value on error.""" + assert template.Template('{{ None | round }}', hass).async_render() == \ + 'None' - def test_logarithm(self): - """Test logarithm.""" - tests = [ - (4, 2, '2.0'), - (1000, 10, '3.0'), - (math.e, '', '1.0'), - ('"invalid"', '_', 'invalid'), - (10, '"invalid"', '10.0'), - ] + assert template.Template( + '{{ "no_number" | round }}', hass).async_render() == 'no_number' - for value, base, expected in tests: - assert expected == \ - template.Template( - '{{ %s | log(%s) | round(1) }}' % (value, base), - self.hass).render() - assert expected == \ - template.Template( - '{{ log(%s, %s) | round(1) }}' % (value, base), - self.hass).render() +def test_multiply(hass): + """Test multiply.""" + tests = { + None: 'None', + 10: '100', + '"abcd"': 'abcd' + } - 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 inp, out in tests.items(): + assert template.Template('{{ %s | multiply(10) | round }}' % inp, + hass).async_render() == out - for value, expected in tests: - assert 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') - ] +def test_logarithm(hass): + """Test logarithm.""" + tests = [ + (4, 2, '2.0'), + (1000, 10, '3.0'), + (math.e, '', '1.0'), + ('"invalid"', '_', 'invalid'), + (10, '"invalid"', '10.0'), + ] - for value, expected in tests: - assert expected == \ - template.Template( - '{{ %s | cos | round(3) }}' % value, - self.hass).render() + for value, base, expected in tests: + assert template.Template( + '{{ %s | log(%s) | round(1) }}' % (value, base), + hass).async_render() == expected - 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') - ] + assert template.Template( + '{{ log(%s, %s) | round(1) }}' % (value, base), + hass).async_render() == expected - for value, expected in tests: - assert 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'), - ] +def test_sine(hass): + """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: - assert expected == \ - template.Template( - '{{ %s | sqrt | round(3) }}' % value, - self.hass).render() + for value, expected in tests: + assert template.Template( + '{{ %s | sin | round(3) }}' % value, + hass).async_render() == expected - def test_strptime(self): - """Test the parse timestamp method.""" - tests = [ - ('2016-10-19 15:22:05.588122 UTC', - '%Y-%m-%d %H:%M:%S.%f %Z', None), - ('2016-10-19 15:22:05.588122+0100', - '%Y-%m-%d %H:%M:%S.%f%z', None), - ('2016-10-19 15:22:05.588122', - '%Y-%m-%d %H:%M:%S.%f', None), - ('2016-10-19', '%Y-%m-%d', None), - ('2016', '%Y', None), - ('15:22:05', '%H:%M:%S', None), - ('1469119144', '%Y', '1469119144'), - ('invalid', '%Y', 'invalid') - ] - for inp, fmt, expected in tests: - if expected is None: - expected = datetime.strptime(inp, fmt) +def test_cos(hass): + """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') + ] - temp = '{{ strptime(\'%s\', \'%s\') }}' % (inp, fmt) + for value, expected in tests: + assert template.Template( + '{{ %s | cos | round(3) }}' % value, + hass).async_render() == expected - assert str(expected) == \ - template.Template(temp, self.hass).render() - def test_timestamp_custom(self): - """Test the timestamps to custom filter.""" - now = dt_util.utcnow() - tests = [ - (None, None, None, 'None'), - (1469119144, None, True, '2016-07-21 16:39:04'), - (1469119144, '%Y', True, '2016'), - (1469119144, 'invalid', True, 'invalid'), - (dt_util.as_timestamp(now), None, False, - now.strftime('%Y-%m-%d %H:%M:%S')) - ] +def test_tan(hass): + """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 inp, fmt, local, out in tests: - if fmt: - fil = 'timestamp_custom(\'{}\')'.format(fmt) - elif fmt and local: - fil = 'timestamp_custom(\'{0}\', {1})'.format(fmt, local) - else: - fil = 'timestamp_custom' + for value, expected in tests: + assert template.Template( + '{{ %s | tan | round(3) }}' % value, + hass).async_render() == expected - assert out == template.Template( - '{{ %s | %s }}' % (inp, fil), self.hass).render() - def test_timestamp_local(self): - """Test the timestamps to local filter.""" - tests = { - None: 'None', - 1469119144: '2016-07-21 16:39:04', - } +def test_sqrt(hass): + """Test square root.""" + tests = [ + (0, '0.0'), + (1, '1.0'), + (2, '1.414'), + (10, '3.162'), + (100, '10.0'), + ] - for inp, out in tests.items(): - assert out == \ - template.Template('{{ %s | timestamp_local }}' % inp, - self.hass).render() + for value, expected in tests: + assert template.Template( + '{{ %s | sqrt | round(3) }}' % value, + hass).async_render() == expected - def test_min(self): - """Test the min filter.""" - assert '1' == \ - template.Template('{{ [1, 2, 3] | min }}', - self.hass).render() - def test_max(self): - """Test the max filter.""" - assert '3' == \ - template.Template('{{ [1, 2, 3] | max }}', - self.hass).render() +def test_strptime(hass): + """Test the parse timestamp method.""" + tests = [ + ('2016-10-19 15:22:05.588122 UTC', + '%Y-%m-%d %H:%M:%S.%f %Z', None), + ('2016-10-19 15:22:05.588122+0100', + '%Y-%m-%d %H:%M:%S.%f%z', None), + ('2016-10-19 15:22:05.588122', + '%Y-%m-%d %H:%M:%S.%f', None), + ('2016-10-19', '%Y-%m-%d', None), + ('2016', '%Y', None), + ('15:22:05', '%H:%M:%S', None), + ('1469119144', '%Y', '1469119144'), + ('invalid', '%Y', 'invalid') + ] - def test_base64_encode(self): - """Test the base64_encode filter.""" - self.assertEqual( - 'aG9tZWFzc2lzdGFudA==', - template.Template('{{ "homeassistant" | base64_encode }}', - self.hass).render()) + for inp, fmt, expected in tests: + if expected is None: + expected = datetime.strptime(inp, fmt) - def test_base64_decode(self): - """Test the base64_decode filter.""" - self.assertEqual( - 'homeassistant', - template.Template('{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}', - self.hass).render()) + temp = '{{ strptime(\'%s\', \'%s\') }}' % (inp, fmt) - def test_ordinal(self): - """Test the ordinal filter.""" - tests = [ - (1, '1st'), - (2, '2nd'), - (3, '3rd'), - (4, '4th'), - (5, '5th'), - ] + assert template.Template(temp, hass).async_render() == str(expected) - for value, expected in tests: - self.assertEqual( - expected, - template.Template( - '{{ %s | ordinal }}' % value, - self.hass).render()) - def test_timestamp_utc(self): - """Test the timestamps to local filter.""" - now = dt_util.utcnow() - tests = { - None: 'None', - 1469119144: '2016-07-21 16:39:04', - dt_util.as_timestamp(now): - now.strftime('%Y-%m-%d %H:%M:%S') - } +def test_timestamp_custom(hass): + """Test the timestamps to custom filter.""" + now = dt_util.utcnow() + tests = [ + (None, None, None, 'None'), + (1469119144, None, True, '2016-07-21 16:39:04'), + (1469119144, '%Y', True, '2016'), + (1469119144, 'invalid', True, 'invalid'), + (dt_util.as_timestamp(now), None, False, + now.strftime('%Y-%m-%d %H:%M:%S')) + ] - for inp, out in tests.items(): - assert out == \ - template.Template('{{ %s | timestamp_utc }}' % inp, - self.hass).render() + for inp, fmt, local, out in tests: + if fmt: + fil = 'timestamp_custom(\'{}\')'.format(fmt) + elif fmt and local: + fil = 'timestamp_custom(\'{0}\', {1})'.format(fmt, local) + else: + fil = 'timestamp_custom' - def test_as_timestamp(self): - """Test the as_timestamp function.""" - assert "None" == \ - template.Template( - '{{ as_timestamp("invalid") }}', self.hass).render() - self.hass.mock = None - assert "None" == \ - template.Template('{{ as_timestamp(states.mock) }}', - self.hass).render() + assert template.Template( + '{{ %s | %s }}' % (inp, fil), hass).async_render() == out - tpl = '{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", ' \ - '"%Y-%m-%dT%H:%M:%S%z")) }}' - assert "1706951424.0" == \ - template.Template(tpl, self.hass).render() - @patch.object(random, 'choice') - def test_random_every_time(self, test_choice): - """Ensure the random filter runs every time, not just once.""" - tpl = template.Template('{{ [1,2] | random }}', self.hass) - test_choice.return_value = 'foo' - assert 'foo' == tpl.render() - test_choice.return_value = 'bar' - assert 'bar' == tpl.render() +def test_timestamp_local(hass): + """Test the timestamps to local filter.""" + tests = { + None: 'None', + 1469119144: '2016-07-21 16:39:04', + } - def test_passing_vars_as_keywords(self): - """Test passing variables as keywords.""" - assert '127' == \ - template.Template('{{ hello }}', self.hass).render(hello=127) + for inp, out in tests.items(): + assert template.Template('{{ %s | timestamp_local }}' % inp, + hass).async_render() == out - def test_passing_vars_as_vars(self): - """Test passing variables as variables.""" - assert '127' == \ - template.Template('{{ hello }}', self.hass).render({'hello': 127}) - def test_passing_vars_as_list(self): - """Test passing variables as list.""" - assert "['foo', 'bar']" == \ - template.render_complex(template.Template('{{ hello }}', - self.hass), {'hello': ['foo', 'bar']}) +def test_min(hass): + """Test the min filter.""" + assert template.Template('{{ [1, 2, 3] | min }}', + hass).async_render() == '1' - def test_passing_vars_as_list_element(self): - """Test passing variables as list.""" - assert 'bar' == \ - template.render_complex(template.Template('{{ hello[1] }}', - self.hass), - {'hello': ['foo', 'bar']}) - def test_passing_vars_as_dict_element(self): - """Test passing variables as list.""" - assert 'bar' == \ - template.render_complex(template.Template('{{ hello.foo }}', - self.hass), - {'hello': {'foo': 'bar'}}) +def test_max(hass): + """Test the max filter.""" + assert template.Template('{{ [1, 2, 3] | max }}', + hass).async_render() == '3' - def test_passing_vars_as_dict(self): - """Test passing variables as list.""" - assert "{'foo': 'bar'}" == \ - template.render_complex(template.Template('{{ hello }}', - self.hass), {'hello': {'foo': 'bar'}}) - def test_render_with_possible_json_value_with_valid_json(self): - """Render with possible JSON value with valid JSON.""" - tpl = template.Template('{{ value_json.hello }}', self.hass) - assert 'world' == \ - tpl.render_with_possible_json_value('{"hello": "world"}') +def test_base64_encode(hass): + """Test the base64_encode filter.""" + assert template.Template('{{ "homeassistant" | base64_encode }}', + hass).async_render() == 'aG9tZWFzc2lzdGFudA==' - def test_render_with_possible_json_value_with_invalid_json(self): - """Render with possible JSON value with invalid JSON.""" - tpl = template.Template('{{ value_json }}', self.hass) - assert '' == \ - tpl.render_with_possible_json_value('{ I AM NOT JSON }') - def test_render_with_possible_json_value_with_template_error_value(self): - """Render with possible JSON value with template error value.""" - tpl = template.Template('{{ non_existing.variable }}', self.hass) - assert '-' == \ - tpl.render_with_possible_json_value('hello', '-') +def test_base64_decode(hass): + """Test the base64_decode filter.""" + assert template.Template('{{ "aG9tZWFzc2lzdGFudA==" | base64_decode }}', + hass).async_render() == 'homeassistant' - def test_render_with_possible_json_value_with_missing_json_value(self): - """Render with possible JSON value with unknown JSON object.""" - tpl = template.Template('{{ value_json.goodbye }}', self.hass) - assert '' == \ - tpl.render_with_possible_json_value('{"hello": "world"}') - def test_render_with_possible_json_value_valid_with_is_defined(self): - """Render with possible JSON value with known JSON object.""" - tpl = template.Template('{{ value_json.hello|is_defined }}', self.hass) - assert 'world' == \ - tpl.render_with_possible_json_value('{"hello": "world"}') +def test_ordinal(hass): + """Test the ordinal filter.""" + tests = [ + (1, '1st'), + (2, '2nd'), + (3, '3rd'), + (4, '4th'), + (5, '5th'), + ] - def test_render_with_possible_json_value_undefined_json(self): - """Render with possible JSON value with unknown JSON object.""" - tpl = template.Template('{{ value_json.bye|is_defined }}', self.hass) - assert '{"hello": "world"}' == \ - tpl.render_with_possible_json_value('{"hello": "world"}') + for value, expected in tests: + assert template.Template( + '{{ %s | ordinal }}' % value, + hass).async_render() == expected - def test_render_with_possible_json_value_undefined_json_error_value(self): - """Render with possible JSON value with unknown JSON object.""" - tpl = template.Template('{{ value_json.bye|is_defined }}', self.hass) - assert '' == \ - tpl.render_with_possible_json_value('{"hello": "world"}', '') - def test_render_with_possible_json_value_non_string_value(self): - """Render with possible JSON value with non-string value.""" - tpl = template.Template(""" +def test_timestamp_utc(hass): + """Test the timestamps to local filter.""" + now = dt_util.utcnow() + tests = { + None: 'None', + 1469119144: '2016-07-21 16:39:04', + dt_util.as_timestamp(now): + now.strftime('%Y-%m-%d %H:%M:%S') + } + + for inp, out in tests.items(): + assert template.Template('{{ %s | timestamp_utc }}' % inp, + hass).async_render() == out + + +def test_as_timestamp(hass): + """Test the as_timestamp function.""" + assert template.Template('{{ as_timestamp("invalid") }}', + hass).async_render() == "None" + hass.mock = None + assert template.Template('{{ as_timestamp(states.mock) }}', + hass).async_render() == "None" + + tpl = '{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", ' \ + '"%Y-%m-%dT%H:%M:%S%z")) }}' + assert template.Template(tpl, hass).async_render() == "1706951424.0" + + +@patch.object(random, 'choice') +def test_random_every_time(test_choice, hass): + """Ensure the random filter runs every time, not just once.""" + tpl = template.Template('{{ [1,2] | random }}', hass) + test_choice.return_value = 'foo' + assert tpl.async_render() == 'foo' + test_choice.return_value = 'bar' + assert tpl.async_render() == 'bar' + + +def test_passing_vars_as_keywords(hass): + """Test passing variables as keywords.""" + assert template.Template( + '{{ hello }}', hass).async_render(hello=127) == '127' + + +def test_passing_vars_as_vars(hass): + """Test passing variables as variables.""" + assert template.Template( + '{{ hello }}', hass).async_render({'hello': 127}) == '127' + + +def test_passing_vars_as_list(hass): + """Test passing variables as list.""" + assert template.render_complex( + template.Template('{{ hello }}', + hass), {'hello': ['foo', 'bar']}) == "['foo', 'bar']" + + +def test_passing_vars_as_list_element(hass): + """Test passing variables as list.""" + assert template.render_complex(template.Template('{{ hello[1] }}', + hass), + {'hello': ['foo', 'bar']}) == 'bar' + + +def test_passing_vars_as_dict_element(hass): + """Test passing variables as list.""" + assert template.render_complex(template.Template('{{ hello.foo }}', + hass), + {'hello': {'foo': 'bar'}}) == 'bar' + + +def test_passing_vars_as_dict(hass): + """Test passing variables as list.""" + assert template.render_complex( + template.Template('{{ hello }}', + hass), {'hello': {'foo': 'bar'}}) == "{'foo': 'bar'}" + + +def test_render_with_possible_json_value_with_valid_json(hass): + """Render with possible JSON value with valid JSON.""" + tpl = template.Template('{{ value_json.hello }}', hass) + assert tpl.async_render_with_possible_json_value( + '{"hello": "world"}') == 'world' + + +def test_render_with_possible_json_value_with_invalid_json(hass): + """Render with possible JSON value with invalid JSON.""" + tpl = template.Template('{{ value_json }}', hass) + assert tpl.async_render_with_possible_json_value('{ I AM NOT JSON }') == '' + + +def test_render_with_possible_json_value_with_template_error_value(hass): + """Render with possible JSON value with template error value.""" + tpl = template.Template('{{ non_existing.variable }}', hass) + assert tpl.async_render_with_possible_json_value('hello', '-') == '-' + + +def test_render_with_possible_json_value_with_missing_json_value(hass): + """Render with possible JSON value with unknown JSON object.""" + tpl = template.Template('{{ value_json.goodbye }}', hass) + assert tpl.async_render_with_possible_json_value( + '{"hello": "world"}') == '' + + +def test_render_with_possible_json_value_valid_with_is_defined(hass): + """Render with possible JSON value with known JSON object.""" + tpl = template.Template('{{ value_json.hello|is_defined }}', hass) + assert tpl.async_render_with_possible_json_value( + '{"hello": "world"}') == 'world' + + +def test_render_with_possible_json_value_undefined_json(hass): + """Render with possible JSON value with unknown JSON object.""" + tpl = template.Template('{{ value_json.bye|is_defined }}', hass) + assert tpl.async_render_with_possible_json_value( + '{"hello": "world"}') == '{"hello": "world"}' + + +def test_render_with_possible_json_value_undefined_json_error_value(hass): + """Render with possible JSON value with unknown JSON object.""" + tpl = template.Template('{{ value_json.bye|is_defined }}', hass) + assert tpl.async_render_with_possible_json_value( + '{"hello": "world"}', '') == '' + + +def test_render_with_possible_json_value_non_string_value(hass): + """Render with possible JSON value with non-string value.""" + tpl = template.Template(""" {{ strptime(value~'+0000', '%Y-%m-%d %H:%M:%S%z') }} - """, self.hass) - value = datetime(2019, 1, 18, 12, 13, 14) - expected = str(pytz.utc.localize(value)) - assert expected == \ - tpl.render_with_possible_json_value(value) + """, hass) + value = datetime(2019, 1, 18, 12, 13, 14) + expected = str(pytz.utc.localize(value)) + assert tpl.async_render_with_possible_json_value(value) == expected - def test_raise_exception_on_error(self): - """Test raising an exception on error.""" - with pytest.raises(TemplateError): - template.Template('{{ invalid_syntax').ensure_valid() - def test_if_state_exists(self): - """Test if state exists works.""" - self.hass.states.set('test.object', 'available') - tpl = template.Template( - '{% if states.test.object %}exists{% else %}not exists{% endif %}', - self.hass) - assert 'exists' == tpl.render() +def test_raise_exception_on_error(hass): + """Test raising an exception on error.""" + with pytest.raises(TemplateError): + template.Template('{{ invalid_syntax').ensure_valid() - def test_is_state(self): - """Test is_state method.""" - self.hass.states.set('test.object', 'available') - tpl = template.Template(""" + +def test_if_state_exists(hass): + """Test if state exists works.""" + hass.states.async_set('test.object', 'available') + tpl = template.Template( + '{% if states.test.object %}exists{% else %}not exists{% endif %}', + hass) + assert tpl.async_render() == 'exists' + + +def test_is_state(hass): + """Test is_state method.""" + hass.states.async_set('test.object', 'available') + tpl = template.Template(""" {% if is_state("test.object", "available") %}yes{% else %}no{% endif %} - """, self.hass) - assert 'yes' == tpl.render() + """, hass) + assert tpl.async_render() == 'yes' - tpl = template.Template(""" + tpl = template.Template(""" {{ is_state("test.noobject", "available") }} - """, self.hass) - assert 'False' == tpl.render() + """, hass) + assert tpl.async_render() == 'False' - def test_is_state_attr(self): - """Test is_state_attr method.""" - self.hass.states.set('test.object', 'available', {'mode': 'on'}) - tpl = template.Template(""" + +def test_is_state_attr(hass): + """Test is_state_attr method.""" + hass.states.async_set('test.object', 'available', {'mode': 'on'}) + tpl = template.Template(""" {% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %} - """, self.hass) - assert 'yes' == tpl.render() + """, hass) + assert tpl.async_render() == 'yes' - tpl = template.Template(""" + tpl = template.Template(""" {{ is_state_attr("test.noobject", "mode", "on") }} - """, self.hass) - assert 'False' == tpl.render() + """, hass) + assert tpl.async_render() == 'False' - def test_state_attr(self): - """Test state_attr method.""" - self.hass.states.set('test.object', 'available', {'mode': 'on'}) - tpl = template.Template(""" + +def test_state_attr(hass): + """Test state_attr method.""" + hass.states.async_set('test.object', 'available', {'mode': 'on'}) + tpl = template.Template(""" {% if state_attr("test.object", "mode") == "on" %}yes{% else %}no{% endif %} - """, self.hass) - assert 'yes' == tpl.render() + """, hass) + assert tpl.async_render() == 'yes' - tpl = template.Template(""" + tpl = template.Template(""" {{ state_attr("test.noobject", "mode") == None }} - """, self.hass) - assert 'True' == tpl.render() + """, hass) + assert tpl.async_render() == 'True' - def test_states_function(self): - """Test using states as a function.""" - self.hass.states.set('test.object', 'available') - tpl = template.Template('{{ states("test.object") }}', self.hass) - assert 'available' == tpl.render() - tpl2 = template.Template('{{ states("test.object2") }}', self.hass) - assert 'unknown' == tpl2.render() +def test_states_function(hass): + """Test using states as a function.""" + hass.states.async_set('test.object', 'available') + tpl = template.Template('{{ states("test.object") }}', hass) + assert tpl.async_render() == 'available' - @patch('homeassistant.helpers.template.TemplateEnvironment.' - 'is_safe_callable', return_value=True) - def test_now(self, mock_is_safe): - """Test now method.""" - now = dt_util.now() - with patch.dict(template.ENV.globals, {'now': lambda: now}): - assert now.isoformat() == \ - template.Template('{{ now().isoformat() }}', - self.hass).render() + tpl2 = template.Template('{{ states("test.object2") }}', hass) + assert tpl2.async_render() == 'unknown' - @patch('homeassistant.helpers.template.TemplateEnvironment.' - 'is_safe_callable', return_value=True) - def test_utcnow(self, mock_is_safe): - """Test utcnow method.""" - now = dt_util.utcnow() - with patch.dict(template.ENV.globals, {'utcnow': lambda: now}): - assert now.isoformat() == \ - template.Template('{{ utcnow().isoformat() }}', - self.hass).render() - def test_regex_match(self): - """Test regex_match method.""" - tpl = template.Template(r""" +@patch('homeassistant.helpers.template.TemplateEnvironment.' + 'is_safe_callable', return_value=True) +def test_now(mock_is_safe, hass): + """Test now method.""" + now = dt_util.now() + with patch.dict(template.ENV.globals, {'now': lambda: now}): + assert now.isoformat() == \ + template.Template('{{ now().isoformat() }}', + hass).async_render() + + +@patch('homeassistant.helpers.template.TemplateEnvironment.' + 'is_safe_callable', return_value=True) +def test_utcnow(mock_is_safe, hass): + """Test utcnow method.""" + now = dt_util.utcnow() + with patch.dict(template.ENV.globals, {'utcnow': lambda: now}): + assert now.isoformat() == \ + template.Template('{{ utcnow().isoformat() }}', + hass).async_render() + + +def test_regex_match(hass): + """Test regex_match method.""" + tpl = template.Template(r""" {{ '123-456-7890' | regex_match('(\\d{3})-(\\d{3})-(\\d{4})') }} - """, self.hass) - assert 'True' == tpl.render() + """, hass) + assert tpl.async_render() == 'True' - tpl = template.Template(""" + tpl = template.Template(""" {{ 'home assistant test' | regex_match('Home', True) }} - """, self.hass) - assert 'True' == tpl.render() + """, hass) + assert tpl.async_render() == 'True' - tpl = template.Template(""" - {{ 'Another home assistant test' | regex_match('home') }} - """, self.hass) - assert 'False' == tpl.render() + tpl = template.Template(""" + {{ 'Another home assistant test' | regex_match('home') }} + """, hass) + assert tpl.async_render() == 'False' - def test_regex_search(self): - """Test regex_search method.""" - tpl = template.Template(r""" + +def test_regex_search(hass): + """Test regex_search method.""" + tpl = template.Template(r""" {{ '123-456-7890' | regex_search('(\\d{3})-(\\d{3})-(\\d{4})') }} - """, self.hass) - assert 'True' == tpl.render() + """, hass) + assert tpl.async_render() == 'True' - tpl = template.Template(""" + tpl = template.Template(""" {{ 'home assistant test' | regex_search('Home', True) }} - """, self.hass) - assert 'True' == tpl.render() + """, hass) + assert tpl.async_render() == 'True' - tpl = template.Template(""" - {{ 'Another home assistant test' | regex_search('home') }} - """, self.hass) - assert 'True' == tpl.render() + tpl = template.Template(""" + {{ 'Another home assistant test' | regex_search('home') }} + """, hass) + assert tpl.async_render() == 'True' - def test_regex_replace(self): - """Test regex_replace method.""" - tpl = template.Template(r""" + +def test_regex_replace(hass): + """Test regex_replace method.""" + tpl = template.Template(r""" {{ 'Hello World' | regex_replace('(Hello\\s)',) }} - """, self.hass) - assert 'World' == tpl.render() + """, hass) + assert tpl.async_render() == 'World' - def test_regex_findall_index(self): - """Test regex_findall_index method.""" - tpl = template.Template(""" + +def test_regex_findall_index(hass): + """Test regex_findall_index method.""" + tpl = template.Template(""" {{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 0) }} - """, self.hass) - assert 'JFK' == tpl.render() + """, hass) + assert tpl.async_render() == 'JFK' - tpl = template.Template(""" + tpl = template.Template(""" {{ 'Flight from JFK to LHR' | regex_findall_index('([A-Z]{3})', 1) }} - """, self.hass) - assert 'LHR' == tpl.render() + """, hass) + assert tpl.async_render() == 'LHR' - def test_bitwise_and(self): - """Test bitwise_and method.""" - tpl = template.Template(""" + +def test_bitwise_and(hass): + """Test bitwise_and method.""" + tpl = template.Template(""" {{ 8 | bitwise_and(8) }} - """, self.hass) - assert str(8 & 8) == tpl.render() - tpl = template.Template(""" + """, hass) + assert tpl.async_render() == str(8 & 8) + tpl = template.Template(""" {{ 10 | bitwise_and(2) }} - """, self.hass) - assert str(10 & 2) == tpl.render() - tpl = template.Template(""" + """, hass) + assert tpl.async_render() == str(10 & 2) + tpl = template.Template(""" {{ 8 | bitwise_and(2) }} - """, self.hass) - assert str(8 & 2) == tpl.render() + """, hass) + assert tpl.async_render() == str(8 & 2) - def test_bitwise_or(self): - """Test bitwise_or method.""" - tpl = template.Template(""" + +def test_bitwise_or(hass): + """Test bitwise_or method.""" + tpl = template.Template(""" {{ 8 | bitwise_or(8) }} - """, self.hass) - assert str(8 | 8) == tpl.render() - tpl = template.Template(""" + """, hass) + assert tpl.async_render() == str(8 | 8) + tpl = template.Template(""" {{ 10 | bitwise_or(2) }} - """, self.hass) - assert str(10 | 2) == tpl.render() - tpl = template.Template(""" + """, hass) + assert tpl.async_render() == str(10 | 2) + tpl = template.Template(""" {{ 8 | bitwise_or(2) }} - """, self.hass) - assert str(8 | 2) == tpl.render() + """, hass) + assert tpl.async_render() == str(8 | 2) - def test_distance_function_with_1_state(self): - """Test distance function with 1 state.""" - self.hass.states.set('test.object', 'happy', { - 'latitude': 32.87336, - 'longitude': -117.22943, - }) - tpl = template.Template('{{ distance(states.test.object) | round }}', - self.hass) - assert '187' == tpl.render() - def test_distance_function_with_2_states(self): - """Test distance function with 2 states.""" - self.hass.states.set('test.object', 'happy', { - 'latitude': 32.87336, - 'longitude': -117.22943, - }) - self.hass.states.set('test.object_2', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - tpl = template.Template( - '{{ distance(states.test.object, states.test.object_2) | round }}', - self.hass) - assert '187' == tpl.render() +def test_distance_function_with_1_state(hass): + """Test distance function with 1 state.""" + _set_up_units(hass) + hass.states.async_set('test.object', 'happy', { + 'latitude': 32.87336, + 'longitude': -117.22943, + }) + tpl = template.Template('{{ distance(states.test.object) | round }}', + hass) + assert tpl.async_render() == '187' - def test_distance_function_with_1_coord(self): - """Test distance function with 1 coord.""" - tpl = template.Template( - '{{ distance("32.87336", "-117.22943") | round }}', self.hass) - assert '187' == \ - tpl.render() - def test_distance_function_with_2_coords(self): - """Test distance function with 2 coords.""" - assert '187' == \ - template.Template( - '{{ distance("32.87336", "-117.22943", %s, %s) | round }}' - % (self.hass.config.latitude, self.hass.config.longitude), - self.hass).render() +def test_distance_function_with_2_states(hass): + """Test distance function with 2 states.""" + _set_up_units(hass) + hass.states.async_set('test.object', 'happy', { + 'latitude': 32.87336, + 'longitude': -117.22943, + }) + hass.states.async_set('test.object_2', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) + tpl = template.Template( + '{{ distance(states.test.object, states.test.object_2) | round }}', + hass) + assert tpl.async_render() == '187' - def test_distance_function_with_1_state_1_coord(self): - """Test distance function with 1 state 1 coord.""" - self.hass.states.set('test.object_2', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - tpl = template.Template( - '{{ distance("32.87336", "-117.22943", states.test.object_2) ' - '| round }}', self.hass) - assert '187' == tpl.render() - tpl2 = template.Template( - '{{ distance(states.test.object_2, "32.87336", "-117.22943") ' - '| round }}', self.hass) - assert '187' == tpl2.render() +def test_distance_function_with_1_coord(hass): + """Test distance function with 1 coord.""" + _set_up_units(hass) + tpl = template.Template( + '{{ distance("32.87336", "-117.22943") | round }}', hass) + assert tpl.async_render() == '187' - def test_distance_function_return_None_if_invalid_state(self): - """Test distance function return None if invalid state.""" - self.hass.states.set('test.object_2', 'happy', { - 'latitude': 10, - }) - tpl = template.Template('{{ distance(states.test.object_2) | round }}', - self.hass) - assert 'None' == \ - tpl.render() - def test_distance_function_return_None_if_invalid_coord(self): - """Test distance function return None if invalid coord.""" - assert 'None' == \ - template.Template( - '{{ distance("123", "abc") }}', self.hass).render() +def test_distance_function_with_2_coords(hass): + """Test distance function with 2 coords.""" + _set_up_units(hass) + assert template.Template( + '{{ distance("32.87336", "-117.22943", %s, %s) | round }}' + % (hass.config.latitude, hass.config.longitude), + hass).async_render() == '187' - assert 'None' == \ - template.Template('{{ distance("123") }}', self.hass).render() - self.hass.states.set('test.object_2', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - tpl = template.Template('{{ distance("123", states.test_object_2) }}', - self.hass) - assert 'None' == \ - tpl.render() +def test_distance_function_with_1_state_1_coord(hass): + """Test distance function with 1 state 1 coord.""" + _set_up_units(hass) + hass.states.async_set('test.object_2', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) + tpl = template.Template( + '{{ distance("32.87336", "-117.22943", states.test.object_2) ' + '| round }}', hass) + assert tpl.async_render() == '187' - def test_distance_function_with_2_entity_ids(self): - """Test distance function with 2 entity ids.""" - self.hass.states.set('test.object', 'happy', { - 'latitude': 32.87336, - 'longitude': -117.22943, - }) - self.hass.states.set('test.object_2', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - tpl = template.Template( - '{{ distance("test.object", "test.object_2") | round }}', - self.hass) - assert '187' == tpl.render() + tpl2 = template.Template( + '{{ distance(states.test.object_2, "32.87336", "-117.22943") ' + '| round }}', hass) + assert tpl2.async_render() == '187' - def test_distance_function_with_1_entity_1_coord(self): - """Test distance function with 1 entity_id and 1 coord.""" - self.hass.states.set('test.object', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - tpl = template.Template( - '{{ distance("test.object", "32.87336", "-117.22943") | round }}', - self.hass) - assert '187' == tpl.render() - def test_closest_function_home_vs_domain(self): - """Test closest function home vs domain.""" - self.hass.states.set('test_domain.object', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) +def test_distance_function_return_none_if_invalid_state(hass): + """Test distance function return None if invalid state.""" + hass.states.async_set('test.object_2', 'happy', { + 'latitude': 10, + }) + tpl = template.Template('{{ distance(states.test.object_2) | round }}', + hass) + assert tpl.async_render() == 'None' - self.hass.states.set('not_test_domain.but_closer', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - assert 'test_domain.object' == \ - template.Template('{{ closest(states.test_domain).entity_id }}', - self.hass).render() +def test_distance_function_return_none_if_invalid_coord(hass): + """Test distance function return None if invalid coord.""" + assert template.Template( + '{{ distance("123", "abc") }}', hass).async_render() == 'None' - def test_closest_function_home_vs_all_states(self): - """Test closest function home vs all states.""" - self.hass.states.set('test_domain.object', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) + assert template.Template('{{ distance("123") }}', hass).async_render() == \ + 'None' - self.hass.states.set('test_domain_2.and_closer', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) + hass.states.async_set('test.object_2', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) + tpl = template.Template('{{ distance("123", states.test_object_2) }}', + hass) + assert tpl.async_render() == 'None' - assert 'test_domain_2.and_closer' == \ - template.Template('{{ closest(states).entity_id }}', - self.hass).render() - def test_closest_function_home_vs_group_entity_id(self): - """Test closest function home vs group entity id.""" - self.hass.states.set('test_domain.object', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) +def test_distance_function_with_2_entity_ids(hass): + """Test distance function with 2 entity ids.""" + _set_up_units(hass) + hass.states.async_set('test.object', 'happy', { + 'latitude': 32.87336, + 'longitude': -117.22943, + }) + hass.states.async_set('test.object_2', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) + tpl = template.Template( + '{{ distance("test.object", "test.object_2") | round }}', + hass) + assert tpl.async_render() == '187' - self.hass.states.set('not_in_group.but_closer', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) - group.Group.create_group( - self.hass, 'location group', ['test_domain.object']) +def test_distance_function_with_1_entity_1_coord(hass): + """Test distance function with 1 entity_id and 1 coord.""" + _set_up_units(hass) + hass.states.async_set('test.object', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) + tpl = template.Template( + '{{ distance("test.object", "32.87336", "-117.22943") | round }}', + hass) + assert tpl.async_render() == '187' - assert 'test_domain.object' == \ - template.Template( - '{{ closest("group.location_group").entity_id }}', - self.hass).render() - def test_closest_function_home_vs_group_state(self): - """Test closest function home vs group state.""" - self.hass.states.set('test_domain.object', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) +def test_closest_function_home_vs_domain(hass): + """Test closest function home vs domain.""" + hass.states.async_set('test_domain.object', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) - self.hass.states.set('not_in_group.but_closer', 'happy', { - 'latitude': self.hass.config.latitude, - 'longitude': self.hass.config.longitude, - }) + hass.states.async_set('not_test_domain.but_closer', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) - group.Group.create_group( - self.hass, 'location group', ['test_domain.object']) + assert template.Template('{{ closest(states.test_domain).entity_id }}', + hass).async_render() == 'test_domain.object' - assert 'test_domain.object' == \ - template.Template( - '{{ closest(states.group.location_group).entity_id }}', - self.hass).render() - def test_closest_function_to_coord(self): - """Test closest function to coord.""" - self.hass.states.set('test_domain.closest_home', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) +def test_closest_function_home_vs_all_states(hass): + """Test closest function home vs all states.""" + hass.states.async_set('test_domain.object', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) - self.hass.states.set('test_domain.closest_zone', 'happy', { - 'latitude': self.hass.config.latitude + 0.2, - 'longitude': self.hass.config.longitude + 0.2, - }) + hass.states.async_set('test_domain_2.and_closer', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) - self.hass.states.set('zone.far_away', 'zoning', { - 'latitude': self.hass.config.latitude + 0.3, - 'longitude': self.hass.config.longitude + 0.3, - }) + assert template.Template('{{ closest(states).entity_id }}', + hass).async_render() == 'test_domain_2.and_closer' - tpl = template.Template( - '{{ closest("%s", %s, states.test_domain).entity_id }}' - % (self.hass.config.latitude + 0.3, - self.hass.config.longitude + 0.3), self.hass) - assert 'test_domain.closest_zone' == \ - tpl.render() +async def test_closest_function_home_vs_group_entity_id(hass): + """Test closest function home vs group entity id.""" + hass.states.async_set('test_domain.object', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) - def test_closest_function_to_entity_id(self): - """Test closest function to entity id.""" - self.hass.states.set('test_domain.closest_home', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) + hass.states.async_set('not_in_group.but_closer', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) - self.hass.states.set('test_domain.closest_zone', 'happy', { - 'latitude': self.hass.config.latitude + 0.2, - 'longitude': self.hass.config.longitude + 0.2, - }) + await group.Group.async_create_group( + hass, 'location group', ['test_domain.object']) - self.hass.states.set('zone.far_away', 'zoning', { - 'latitude': self.hass.config.latitude + 0.3, - 'longitude': self.hass.config.longitude + 0.3, - }) + assert template.Template( + '{{ closest("group.location_group").entity_id }}', + hass).async_render() == 'test_domain.object' - assert 'test_domain.closest_zone' == \ - template.Template( - '{{ closest("zone.far_away", ' - 'states.test_domain).entity_id }}', self.hass).render() - def test_closest_function_to_state(self): - """Test closest function to state.""" - self.hass.states.set('test_domain.closest_home', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) +async def test_closest_function_home_vs_group_state(hass): + """Test closest function home vs group state.""" + hass.states.async_set('test_domain.object', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) - self.hass.states.set('test_domain.closest_zone', 'happy', { - 'latitude': self.hass.config.latitude + 0.2, - 'longitude': self.hass.config.longitude + 0.2, - }) + hass.states.async_set('not_in_group.but_closer', 'happy', { + 'latitude': hass.config.latitude, + 'longitude': hass.config.longitude, + }) - self.hass.states.set('zone.far_away', 'zoning', { - 'latitude': self.hass.config.latitude + 0.3, - 'longitude': self.hass.config.longitude + 0.3, - }) + await group.Group.async_create_group( + hass, 'location group', ['test_domain.object']) - assert 'test_domain.closest_zone' == \ - template.Template( - '{{ closest(states.zone.far_away, ' - 'states.test_domain).entity_id }}', self.hass).render() + assert template.Template( + '{{ closest(states.group.location_group).entity_id }}', + hass).async_render() == 'test_domain.object' - def test_closest_function_invalid_state(self): - """Test closest function invalid state.""" - self.hass.states.set('test_domain.closest_home', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) - for state in ('states.zone.non_existing', '"zone.non_existing"'): - assert 'None' == \ - template.Template('{{ closest(%s, states) }}' % state, - self.hass).render() +def test_closest_function_to_coord(hass): + """Test closest function to coord.""" + hass.states.async_set('test_domain.closest_home', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) - def test_closest_function_state_with_invalid_location(self): - """Test closest function state with invalid location.""" - self.hass.states.set('test_domain.closest_home', 'happy', { - 'latitude': 'invalid latitude', - 'longitude': self.hass.config.longitude + 0.1, - }) + hass.states.async_set('test_domain.closest_zone', 'happy', { + 'latitude': hass.config.latitude + 0.2, + 'longitude': hass.config.longitude + 0.2, + }) - assert 'None' == \ - template.Template( + hass.states.async_set('zone.far_away', 'zoning', { + 'latitude': hass.config.latitude + 0.3, + 'longitude': hass.config.longitude + 0.3, + }) + + tpl = template.Template( + '{{ closest("%s", %s, states.test_domain).entity_id }}' + % (hass.config.latitude + 0.3, + hass.config.longitude + 0.3), hass) + + assert tpl.async_render() == 'test_domain.closest_zone' + + +def test_closest_function_to_entity_id(hass): + """Test closest function to entity id.""" + hass.states.async_set('test_domain.closest_home', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) + + hass.states.async_set('test_domain.closest_zone', 'happy', { + 'latitude': hass.config.latitude + 0.2, + 'longitude': hass.config.longitude + 0.2, + }) + + hass.states.async_set('zone.far_away', 'zoning', { + 'latitude': hass.config.latitude + 0.3, + 'longitude': hass.config.longitude + 0.3, + }) + + assert template.Template( + '{{ closest("zone.far_away", ' + 'states.test_domain).entity_id }}', hass).async_render() == \ + 'test_domain.closest_zone' + + +def test_closest_function_to_state(hass): + """Test closest function to state.""" + hass.states.async_set('test_domain.closest_home', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) + + hass.states.async_set('test_domain.closest_zone', 'happy', { + 'latitude': hass.config.latitude + 0.2, + 'longitude': hass.config.longitude + 0.2, + }) + + hass.states.async_set('zone.far_away', 'zoning', { + 'latitude': hass.config.latitude + 0.3, + 'longitude': hass.config.longitude + 0.3, + }) + + assert template.Template( + '{{ closest(states.zone.far_away, ' + 'states.test_domain).entity_id }}', hass).async_render() == \ + 'test_domain.closest_zone' + + +def test_closest_function_invalid_state(hass): + """Test closest function invalid state.""" + hass.states.async_set('test_domain.closest_home', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) + + for state in ('states.zone.non_existing', '"zone.non_existing"'): + assert template.Template('{{ closest(%s, states) }}' % state, + hass).async_render() == 'None' + + +def test_closest_function_state_with_invalid_location(hass): + """Test closest function state with invalid location.""" + hass.states.async_set('test_domain.closest_home', 'happy', { + 'latitude': 'invalid latitude', + 'longitude': hass.config.longitude + 0.1, + }) + + assert template.Template( '{{ closest(states.test_domain.closest_home, ' - 'states) }}', self.hass).render() + 'states) }}', hass).async_render() == 'None' - def test_closest_function_invalid_coordinates(self): - """Test closest function invalid coordinates.""" - self.hass.states.set('test_domain.closest_home', 'happy', { - 'latitude': self.hass.config.latitude + 0.1, - 'longitude': self.hass.config.longitude + 0.1, - }) - assert 'None' == \ - template.Template('{{ closest("invalid", "coord", states) }}', - self.hass).render() +def test_closest_function_invalid_coordinates(hass): + """Test closest function invalid coordinates.""" + hass.states.async_set('test_domain.closest_home', 'happy', { + 'latitude': hass.config.latitude + 0.1, + 'longitude': hass.config.longitude + 0.1, + }) - def test_closest_function_no_location_states(self): - """Test closest function without location states.""" - assert '' == \ - template.Template('{{ closest(states).entity_id }}', - self.hass).render() + assert template.Template('{{ closest("invalid", "coord", states) }}', + hass).async_render() == 'None' - def test_extract_entities_none_exclude_stuff(self): - """Test extract entities function with none or exclude stuff.""" - assert [] == template.extract_entities(None) - assert [] == template.extract_entities("mdi:water") +def test_closest_function_no_location_states(hass): + """Test closest function without location states.""" + assert template.Template('{{ closest(states).entity_id }}', + hass).async_render() == '' - assert MATCH_ALL == \ - template.extract_entities( - '{{ closest(states.zone.far_away, ' - 'states.test_domain).entity_id }}') - assert MATCH_ALL == \ - template.extract_entities( - '{{ distance("123", states.test_object_2) }}') +def test_extract_entities_none_exclude_stuff(hass): + """Test extract entities function with none or exclude stuff.""" + assert template.extract_entities(None) == [] - def test_extract_entities_no_match_entities(self): - """Test extract entities function with none entities stuff.""" - assert MATCH_ALL == \ - template.extract_entities( - "{{ value_json.tst | timestamp_custom('%Y' True) }}") + assert template.extract_entities("mdi:water") == [] - assert MATCH_ALL == \ - template.extract_entities(""" + assert template.extract_entities( + '{{ closest(states.zone.far_away, ' + 'states.test_domain).entity_id }}') == MATCH_ALL + + assert template.extract_entities( + '{{ distance("123", states.test_object_2) }}') == MATCH_ALL + + +def test_extract_entities_no_match_entities(hass): + """Test extract entities function with none entities stuff.""" + assert template.extract_entities( + "{{ value_json.tst | timestamp_custom('%Y' True) }}") == MATCH_ALL + + assert template.extract_entities(""" {% for state in states.sensor %} - {{ state.entity_id }}={{ state.state }},d +{{ state.entity_id }}={{ state.state }},d {% endfor %} - """) + """) == MATCH_ALL - def test_extract_entities_match_entities(self): - """Test extract entities function with entities stuff.""" - assert ['device_tracker.phone_1'] == \ - template.extract_entities(""" + +def test_extract_entities_match_entities(hass): + """Test extract entities function with entities stuff.""" + assert template.extract_entities(""" {% if is_state('device_tracker.phone_1', 'home') %} - Ha, Hercules is home! +Ha, Hercules is home! {% else %} - Hercules is at {{ states('device_tracker.phone_1') }}. +Hercules is at {{ states('device_tracker.phone_1') }}. {% endif %} - """) + """) == ['device_tracker.phone_1'] - assert ['binary_sensor.garage_door'] == \ - template.extract_entities(""" + assert template.extract_entities(""" {{ as_timestamp(states.binary_sensor.garage_door.last_changed) }} - """) + """) == ['binary_sensor.garage_door'] - assert ['binary_sensor.garage_door'] == \ - template.extract_entities(""" + assert template.extract_entities(""" {{ states("binary_sensor.garage_door") }} - """) + """) == ['binary_sensor.garage_door'] - assert ['device_tracker.phone_2'] == \ - template.extract_entities(""" + assert template.extract_entities(""" {{ is_state_attr('device_tracker.phone_2', 'battery', 40) }} - """) + """) == ['device_tracker.phone_2'] - assert sorted([ - 'device_tracker.phone_1', - 'device_tracker.phone_2', - ]) == \ - sorted(template.extract_entities(""" + assert sorted([ + 'device_tracker.phone_1', + 'device_tracker.phone_2', + ]) == \ + sorted(template.extract_entities(""" {% if is_state('device_tracker.phone_1', 'home') %} - Ha, Hercules is home! +Ha, Hercules is home! {% elif states.device_tracker.phone_2.attributes.battery < 40 %} - Hercules you power goes done!. +Hercules you power goes done!. {% endif %} - """)) + """)) - assert sorted([ - 'sensor.pick_humidity', - 'sensor.pick_temperature', - ]) == \ - sorted(template.extract_entities(""" + assert sorted([ + 'sensor.pick_humidity', + 'sensor.pick_temperature', + ]) == \ + sorted(template.extract_entities(""" {{ - states.sensor.pick_temperature.state ~ „°C (“ ~ - states.sensor.pick_humidity.state ~ „ %“ +states.sensor.pick_temperature.state ~ „°C (“ ~ +states.sensor.pick_humidity.state ~ „ %“ }} - """)) + """)) - assert sorted([ - 'sensor.luftfeuchtigkeit_mean', - 'input_number.luftfeuchtigkeit', - ]) == \ - sorted(template.extract_entities( - "{% if (states('sensor.luftfeuchtigkeit_mean') | int)" - " > (states('input_number.luftfeuchtigkeit') | int +1.5)" - " %}true{% endif %}" - )) - - def test_extract_entities_with_variables(self): - """Test extract entities function with variables and entities stuff.""" - assert ['input_boolean.switch'] == \ - template.extract_entities( - "{{ is_state('input_boolean.switch', 'off') }}", {}) - - assert ['trigger.entity_id'] == \ - template.extract_entities( - "{{ is_state(trigger.entity_id, 'off') }}", {}) - - assert MATCH_ALL == \ - template.extract_entities( - "{{ is_state(data, 'off') }}", {}) - - assert ['input_boolean.switch'] == \ - template.extract_entities( - "{{ is_state(data, 'off') }}", - {'data': 'input_boolean.switch'}) - - assert ['input_boolean.switch'] == \ - template.extract_entities( - "{{ is_state(trigger.entity_id, 'off') }}", - {'trigger': {'entity_id': 'input_boolean.switch'}}) - - assert MATCH_ALL == \ - template.extract_entities( - "{{ is_state('media_player.' ~ where , 'playing') }}", - {'where': 'livingroom'}) - - def test_jinja_namespace(self): - """Test Jinja's namespace command can be used.""" - test_template = template.Template( - ( - "{% set ns = namespace(a_key='') %}" - "{% set ns.a_key = states.sensor.dummy.state %}" - "{{ ns.a_key }}" - ), - self.hass - ) - - self.hass.states.set('sensor.dummy', 'a value') - assert 'a value' == test_template.render() - - self.hass.states.set('sensor.dummy', 'another value') - assert 'another value' == test_template.render() + assert sorted([ + 'sensor.luftfeuchtigkeit_mean', + 'input_number.luftfeuchtigkeit', + ]) == \ + sorted(template.extract_entities( + "{% if (states('sensor.luftfeuchtigkeit_mean') | int)" + " > (states('input_number.luftfeuchtigkeit') | int +1.5)" + " %}true{% endif %}" + )) -@asyncio.coroutine -def test_state_with_unit(hass): +def test_extract_entities_with_variables(hass): + """Test extract entities function with variables and entities stuff.""" + assert template.extract_entities( + "{{ is_state('input_boolean.switch', 'off') }}", {}) == \ + ['input_boolean.switch'] + + assert template.extract_entities( + "{{ is_state(trigger.entity_id, 'off') }}", {}) == \ + ['trigger.entity_id'] + + assert template.extract_entities( + "{{ is_state(data, 'off') }}", {}) == MATCH_ALL + + assert template.extract_entities( + "{{ is_state(data, 'off') }}", + {'data': 'input_boolean.switch'}) == \ + ['input_boolean.switch'] + + assert template.extract_entities( + "{{ is_state(trigger.entity_id, 'off') }}", + {'trigger': {'entity_id': 'input_boolean.switch'}}) == \ + ['input_boolean.switch'] + + assert template.extract_entities( + "{{ is_state('media_player.' ~ where , 'playing') }}", + {'where': 'livingroom'}) == MATCH_ALL + + +def test_jinja_namespace(hass): + """Test Jinja's namespace command can be used.""" + test_template = template.Template( + ( + "{% set ns = namespace(a_key='') %}" + "{% set ns.a_key = states.sensor.dummy.state %}" + "{{ ns.a_key }}" + ), + hass + ) + + hass.states.async_set('sensor.dummy', 'a value') + assert test_template.async_render() == 'a value' + + hass.states.async_set('sensor.dummy', 'another value') + assert test_template.async_render() == 'another value' + + +async def test_state_with_unit(hass): """Test the state_with_unit property helper.""" hass.states.async_set('sensor.test', '23', { 'unit_of_measurement': 'beers', @@ -1064,8 +1073,7 @@ def test_state_with_unit(hass): assert tpl.async_render() == '' -@asyncio.coroutine -def test_length_of_states(hass): +async def test_length_of_states(hass): """Test fetching the length of states.""" hass.states.async_set('sensor.test', '23') hass.states.async_set('sensor.test2', 'wow') diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index dd0289d44be..b5c147c559f 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,8 +1,6 @@ """Test check_config script.""" -import asyncio import logging import os # noqa: F401 pylint: disable=unused-import -import unittest from unittest.mock import patch import homeassistant.scripts.check_config as check_config @@ -36,149 +34,138 @@ def normalize_yaml_files(check_dict): for key in sorted(check_dict['yaml_files'].keys())] -# pylint: disable=unsubscriptable-object -class TestCheckConfig(unittest.TestCase): - """Tests for the homeassistant.scripts.check_config module.""" +# pylint: disable=no-self-use,invalid-name +@patch('os.path.isfile', return_value=True) +def test_bad_core_config(isfile_patch, loop): + """Test a bad core config setup.""" + files = { + YAML_CONFIG_FILE: BAD_CORE_CONFIG, + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir()) + assert res['except'].keys() == {'homeassistant'} + assert res['except']['homeassistant'][1] == {'unit_system': 'bad'} - def setUp(self): - """Prepare the test.""" - # Somewhere in the tests our event loop gets killed, - # this ensures we have one. - try: - asyncio.get_event_loop() - except RuntimeError: - asyncio.set_event_loop(asyncio.new_event_loop()) - # Will allow seeing full diff - self.maxDiff = None # pylint: disable=invalid-name +@patch('os.path.isfile', return_value=True) +def test_config_platform_valid(isfile_patch, loop): + """Test a valid platform setup.""" + files = { + YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: demo', + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant', 'light'} + assert res['components']['light'] == [{'platform': 'demo'}] + assert res['except'] == {} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 - # pylint: disable=no-self-use,invalid-name - @patch('os.path.isfile', return_value=True) - def test_bad_core_config(self, isfile_patch): - """Test a bad core config setup.""" - files = { - YAML_CONFIG_FILE: BAD_CORE_CONFIG, + +@patch('os.path.isfile', return_value=True) +def test_component_platform_not_found(isfile_patch, loop): + """Test errors if component or platform not found.""" + # Make sure they don't exist + files = { + YAML_CONFIG_FILE: BASE_CONFIG + 'beer:', + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant'} + assert res['except'] == { + check_config.ERROR_STR: ['Integration not found: beer']} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 + + files = { + YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: beer', + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir()) + assert res['components'].keys() == {'homeassistant', 'light'} + assert res['components']['light'] == [] + assert res['except'] == { + check_config.ERROR_STR: [ + 'Integration beer not found when trying to verify its ' + 'light platform.', + ]} + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 + + +@patch('os.path.isfile', return_value=True) +def test_secrets(isfile_patch, loop): + """Test secrets config checking method.""" + secrets_path = get_test_config_dir('secrets.yaml') + + files = { + get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG + ( + 'http:\n' + ' api_password: !secret http_pw'), + secrets_path: ( + 'logger: debug\n' + 'http_pw: abc123'), + } + + with patch_yaml_files(files): + + res = check_config.check(get_test_config_dir(), True) + + assert res['except'] == {} + assert res['components'].keys() == {'homeassistant', 'http'} + assert res['components']['http'] == { + 'api_password': 'abc123', + 'cors_allowed_origins': [], + 'ip_ban_enabled': True, + 'login_attempts_threshold': -1, + 'server_host': '0.0.0.0', + 'server_port': 8123, + 'trusted_networks': [], + 'ssl_profile': 'modern', } - with patch_yaml_files(files): - res = check_config.check(get_test_config_dir()) - assert res['except'].keys() == {'homeassistant'} - assert res['except']['homeassistant'][1] == {'unit_system': 'bad'} + assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} + assert res['secrets'] == {'http_pw': 'abc123'} + assert normalize_yaml_files(res) == [ + '.../configuration.yaml', '.../secrets.yaml'] - @patch('os.path.isfile', return_value=True) - def test_config_platform_valid(self, isfile_patch): - """Test a valid platform setup.""" - files = { - YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: demo', - } - with patch_yaml_files(files): - res = check_config.check(get_test_config_dir()) - assert res['components'].keys() == {'homeassistant', 'light'} - assert res['components']['light'] == [{'platform': 'demo'}] - assert res['except'] == {} - assert res['secret_cache'] == {} - assert res['secrets'] == {} - assert len(res['yaml_files']) == 1 - @patch('os.path.isfile', return_value=True) - def test_component_platform_not_found(self, isfile_patch): - """Test errors if component or platform not found.""" - # Make sure they don't exist - files = { - YAML_CONFIG_FILE: BASE_CONFIG + 'beer:', - } - with patch_yaml_files(files): - res = check_config.check(get_test_config_dir()) - assert res['components'].keys() == {'homeassistant'} - assert res['except'] == { - check_config.ERROR_STR: ['Integration not found: beer']} - assert res['secret_cache'] == {} - assert res['secrets'] == {} - assert len(res['yaml_files']) == 1 +@patch('os.path.isfile', return_value=True) +def test_package_invalid(isfile_patch, loop): + """Test a valid platform setup.""" + files = { + YAML_CONFIG_FILE: BASE_CONFIG + ( + ' packages:\n' + ' p1:\n' + ' group: ["a"]'), + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir()) - files = { - YAML_CONFIG_FILE: BASE_CONFIG + 'light:\n platform: beer', - } - with patch_yaml_files(files): - res = check_config.check(get_test_config_dir()) - assert res['components'].keys() == {'homeassistant', 'light'} - assert res['components']['light'] == [] - assert res['except'] == { - check_config.ERROR_STR: [ - 'Integration beer not found when trying to verify its ' - 'light platform.', - ]} - assert res['secret_cache'] == {} - assert res['secrets'] == {} - assert len(res['yaml_files']) == 1 + assert res['except'].keys() == {'homeassistant.packages.p1.group'} + assert res['except']['homeassistant.packages.p1.group'][1] == \ + {'group': ['a']} + assert len(res['except']) == 1 + assert res['components'].keys() == {'homeassistant'} + assert len(res['components']) == 1 + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert len(res['yaml_files']) == 1 - @patch('os.path.isfile', return_value=True) - def test_secrets(self, isfile_patch): - """Test secrets config checking method.""" - secrets_path = get_test_config_dir('secrets.yaml') - files = { - get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG + ( - 'http:\n' - ' api_password: !secret http_pw'), - secrets_path: ( - 'logger: debug\n' - 'http_pw: abc123'), - } - - with patch_yaml_files(files): - - res = check_config.check(get_test_config_dir(), True) - - assert res['except'] == {} - assert res['components'].keys() == {'homeassistant', 'http'} - assert res['components']['http'] == { - 'api_password': 'abc123', - 'cors_allowed_origins': [], - 'ip_ban_enabled': True, - 'login_attempts_threshold': -1, - 'server_host': '0.0.0.0', - 'server_port': 8123, - 'trusted_networks': [], - 'ssl_profile': 'modern', - } - assert res['secret_cache'] == {secrets_path: {'http_pw': 'abc123'}} - assert res['secrets'] == {'http_pw': 'abc123'} - assert normalize_yaml_files(res) == [ - '.../configuration.yaml', '.../secrets.yaml'] - - @patch('os.path.isfile', return_value=True) - def test_package_invalid(self, isfile_patch): - """Test a valid platform setup.""" - files = { - YAML_CONFIG_FILE: BASE_CONFIG + ( - ' packages:\n' - ' p1:\n' - ' group: ["a"]'), - } - with patch_yaml_files(files): - res = check_config.check(get_test_config_dir()) - - assert res['except'].keys() == {'homeassistant.packages.p1.group'} - assert res['except']['homeassistant.packages.p1.group'][1] == \ - {'group': ['a']} - assert len(res['except']) == 1 - assert res['components'].keys() == {'homeassistant'} - assert len(res['components']) == 1 - assert res['secret_cache'] == {} - assert res['secrets'] == {} - assert len(res['yaml_files']) == 1 - - def test_bootstrap_error(self): - """Test a valid platform setup.""" - files = { - YAML_CONFIG_FILE: BASE_CONFIG + 'automation: !include no.yaml', - } - with patch_yaml_files(files): - res = check_config.check(get_test_config_dir(YAML_CONFIG_FILE)) - err = res['except'].pop(check_config.ERROR_STR) - assert len(err) == 1 - assert res['except'] == {} - assert res['components'] == {} # No components, load failed - assert res['secret_cache'] == {} - assert res['secrets'] == {} - assert res['yaml_files'] == {} +def test_bootstrap_error(loop): + """Test a valid platform setup.""" + files = { + YAML_CONFIG_FILE: BASE_CONFIG + 'automation: !include no.yaml', + } + with patch_yaml_files(files): + res = check_config.check(get_test_config_dir(YAML_CONFIG_FILE)) + err = res['except'].pop(check_config.ERROR_STR) + assert len(err) == 1 + assert res['except'] == {} + assert res['components'] == {} # No components, load failed + assert res['secret_cache'] == {} + assert res['secrets'] == {} + assert res['yaml_files'] == {} diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index bfb98e90f24..38aea0cd992 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -1,19 +1,15 @@ """Test script init.""" -import unittest from unittest.mock import patch import homeassistant.scripts as scripts -class TestScripts(unittest.TestCase): - """Tests homeassistant.scripts module.""" - - @patch('homeassistant.scripts.get_default_config_dir', - return_value='/default') - def test_config_per_platform(self, mock_def): - """Test config per platform method.""" - self.assertEqual(scripts.get_default_config_dir(), '/default') - self.assertEqual(scripts.extract_config_dir(), '/default') - self.assertEqual(scripts.extract_config_dir(['']), '/default') - self.assertEqual(scripts.extract_config_dir(['-c', '/arg']), '/arg') - self.assertEqual(scripts.extract_config_dir(['--config', '/a']), '/a') +@patch('homeassistant.scripts.get_default_config_dir', + return_value='/default') +def test_config_per_platform(mock_def): + """Test config per platform method.""" + assert scripts.get_default_config_dir() == '/default' + assert scripts.extract_config_dir() == '/default' + assert scripts.extract_config_dir(['']) == '/default' + assert scripts.extract_config_dir(['-c', '/arg']) == '/arg' + assert scripts.extract_config_dir(['--config', '/a']) == '/a' diff --git a/tests/test_config.py b/tests/test_config.py index e9ca2a6c806..c5711cdfafe 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access import asyncio import os -import unittest import unittest.mock as mock from collections import OrderedDict from ipaddress import ip_network @@ -23,7 +22,6 @@ from homeassistant.const import ( CONF_AUTH_PROVIDERS, CONF_AUTH_MFA_MODULES) from homeassistant.util import location as location_util, dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from homeassistant.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.entity import Entity from homeassistant.components.config.group import ( CONFIG_PATH as GROUP_CONFIG_PATH) @@ -36,7 +34,7 @@ from homeassistant.components.config.customize import ( import homeassistant.scripts.check_config as check_config from tests.common import ( - get_test_config_dir, get_test_home_assistant, patch_yaml_files) + get_test_config_dir, patch_yaml_files) CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) @@ -55,512 +53,508 @@ def create_file(path): pass -class TestConfig(unittest.TestCase): - """Test the configutils.""" +def teardown(): + """Clean up.""" + dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE - # pylint: disable=invalid-name - def setUp(self): - """Initialize a test Home Assistant instance.""" - self.hass = get_test_home_assistant() + if os.path.isfile(YAML_PATH): + os.remove(YAML_PATH) - # pylint: disable=invalid-name - def tearDown(self): - """Clean up.""" - dt_util.DEFAULT_TIME_ZONE = ORIG_TIMEZONE + if os.path.isfile(SECRET_PATH): + os.remove(SECRET_PATH) - if os.path.isfile(YAML_PATH): - os.remove(YAML_PATH) + if os.path.isfile(VERSION_PATH): + os.remove(VERSION_PATH) - if os.path.isfile(SECRET_PATH): - os.remove(SECRET_PATH) + if os.path.isfile(GROUP_PATH): + os.remove(GROUP_PATH) - if os.path.isfile(VERSION_PATH): - os.remove(VERSION_PATH) + if os.path.isfile(AUTOMATIONS_PATH): + os.remove(AUTOMATIONS_PATH) - if os.path.isfile(GROUP_PATH): - os.remove(GROUP_PATH) + if os.path.isfile(SCRIPTS_PATH): + os.remove(SCRIPTS_PATH) - if os.path.isfile(AUTOMATIONS_PATH): - os.remove(AUTOMATIONS_PATH) + if os.path.isfile(CUSTOMIZE_PATH): + os.remove(CUSTOMIZE_PATH) - if os.path.isfile(SCRIPTS_PATH): - os.remove(SCRIPTS_PATH) - if os.path.isfile(CUSTOMIZE_PATH): - os.remove(CUSTOMIZE_PATH) +def test_create_default_config(): + """Test creation of default config.""" + config_util.create_default_config(CONFIG_DIR, False) - self.hass.stop() + assert os.path.isfile(YAML_PATH) + assert os.path.isfile(SECRET_PATH) + assert os.path.isfile(VERSION_PATH) + assert os.path.isfile(GROUP_PATH) + assert os.path.isfile(AUTOMATIONS_PATH) + assert os.path.isfile(CUSTOMIZE_PATH) - # pylint: disable=no-self-use - def test_create_default_config(self): - """Test creation of default config.""" - config_util.create_default_config(CONFIG_DIR, False) - assert os.path.isfile(YAML_PATH) - assert os.path.isfile(SECRET_PATH) - assert os.path.isfile(VERSION_PATH) - assert os.path.isfile(GROUP_PATH) - assert os.path.isfile(AUTOMATIONS_PATH) - assert os.path.isfile(CUSTOMIZE_PATH) +def test_find_config_file_yaml(): + """Test if it finds a YAML config file.""" + create_file(YAML_PATH) - def test_find_config_file_yaml(self): - """Test if it finds a YAML config file.""" - create_file(YAML_PATH) + assert YAML_PATH == config_util.find_config_file(CONFIG_DIR) - assert YAML_PATH == config_util.find_config_file(CONFIG_DIR) - @mock.patch('builtins.print') - def test_ensure_config_exists_creates_config(self, mock_print): - """Test that calling ensure_config_exists. +@mock.patch('builtins.print') +def test_ensure_config_exists_creates_config(mock_print): + """Test that calling ensure_config_exists. - If not creates a new config file. - """ - config_util.ensure_config_exists(CONFIG_DIR, False) + If not creates a new config file. + """ + config_util.ensure_config_exists(CONFIG_DIR, False) - assert os.path.isfile(YAML_PATH) - assert mock_print.called + assert os.path.isfile(YAML_PATH) + assert mock_print.called - def test_ensure_config_exists_uses_existing_config(self): - """Test that calling ensure_config_exists uses existing config.""" - create_file(YAML_PATH) - config_util.ensure_config_exists(CONFIG_DIR, False) - with open(YAML_PATH) as f: - content = f.read() +def test_ensure_config_exists_uses_existing_config(): + """Test that calling ensure_config_exists uses existing config.""" + create_file(YAML_PATH) + config_util.ensure_config_exists(CONFIG_DIR, False) - # File created with create_file are empty - assert '' == content + with open(YAML_PATH) as f: + content = f.read() - def test_load_yaml_config_converts_empty_files_to_dict(self): - """Test that loading an empty file returns an empty dict.""" - create_file(YAML_PATH) + # File created with create_file are empty + assert content == '' - assert isinstance(config_util.load_yaml_config_file(YAML_PATH), dict) - def test_load_yaml_config_raises_error_if_not_dict(self): - """Test error raised when YAML file is not a dict.""" - with open(YAML_PATH, 'w') as f: - f.write('5') +def test_load_yaml_config_converts_empty_files_to_dict(): + """Test that loading an empty file returns an empty dict.""" + create_file(YAML_PATH) - with pytest.raises(HomeAssistantError): - config_util.load_yaml_config_file(YAML_PATH) + assert isinstance(config_util.load_yaml_config_file(YAML_PATH), dict) - def test_load_yaml_config_raises_error_if_malformed_yaml(self): - """Test error raised if invalid YAML.""" - with open(YAML_PATH, 'w') as f: - f.write(':') - with pytest.raises(HomeAssistantError): - config_util.load_yaml_config_file(YAML_PATH) +def test_load_yaml_config_raises_error_if_not_dict(): + """Test error raised when YAML file is not a dict.""" + with open(YAML_PATH, 'w') as f: + f.write('5') - def test_load_yaml_config_raises_error_if_unsafe_yaml(self): - """Test error raised if unsafe YAML.""" - with open(YAML_PATH, 'w') as f: - f.write('hello: !!python/object/apply:os.system') + with pytest.raises(HomeAssistantError): + config_util.load_yaml_config_file(YAML_PATH) - with pytest.raises(HomeAssistantError): - config_util.load_yaml_config_file(YAML_PATH) - def test_load_yaml_config_preserves_key_order(self): - """Test removal of library.""" - with open(YAML_PATH, 'w') as f: - f.write('hello: 2\n') - f.write('world: 1\n') +def test_load_yaml_config_raises_error_if_malformed_yaml(): + """Test error raised if invalid YAML.""" + with open(YAML_PATH, 'w') as f: + f.write(':') - assert [('hello', 2), ('world', 1)] == \ - list(config_util.load_yaml_config_file(YAML_PATH).items()) + with pytest.raises(HomeAssistantError): + config_util.load_yaml_config_file(YAML_PATH) - @mock.patch('homeassistant.util.location.detect_location_info', - return_value=location_util.LocationInfo( - '0.0.0.0', 'US', 'United States', 'CA', 'California', - 'San Diego', '92122', 'America/Los_Angeles', 32.8594, - -117.2073, True)) - @mock.patch('homeassistant.util.location.elevation', return_value=101) - @mock.patch('builtins.print') - def test_create_default_config_detect_location(self, mock_detect, - mock_elev, mock_print): - """Test that detect location sets the correct config keys.""" - config_util.ensure_config_exists(CONFIG_DIR) - config = config_util.load_yaml_config_file(YAML_PATH) +def test_load_yaml_config_raises_error_if_unsafe_yaml(): + """Test error raised if unsafe YAML.""" + with open(YAML_PATH, 'w') as f: + f.write('hello: !!python/object/apply:os.system') - assert DOMAIN in config + with pytest.raises(HomeAssistantError): + config_util.load_yaml_config_file(YAML_PATH) - ha_conf = config[DOMAIN] - expected_values = { - CONF_LATITUDE: 32.8594, - CONF_LONGITUDE: -117.2073, - CONF_ELEVATION: 101, - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - CONF_NAME: 'Home', - CONF_TIME_ZONE: 'America/Los_Angeles', - CONF_CUSTOMIZE: OrderedDict(), - } +def test_load_yaml_config_preserves_key_order(): + """Test removal of library.""" + with open(YAML_PATH, 'w') as f: + f.write('hello: 2\n') + f.write('world: 1\n') - assert expected_values == ha_conf - assert mock_print.called + assert [('hello', 2), ('world', 1)] == \ + list(config_util.load_yaml_config_file(YAML_PATH).items()) - @mock.patch('builtins.print') - def test_create_default_config_returns_none_if_write_error(self, - mock_print): - """Test the writing of a default configuration. - Non existing folder returns None. - """ - assert config_util.create_default_config( - os.path.join(CONFIG_DIR, 'non_existing_dir/'), False) is None - assert mock_print.called +@mock.patch('homeassistant.util.location.detect_location_info', + return_value=location_util.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True)) +@mock.patch('homeassistant.util.location.elevation', return_value=101) +@mock.patch('builtins.print') +def test_create_default_config_detect_location(mock_detect, + mock_elev, mock_print): + """Test that detect location sets the correct config keys.""" + config_util.ensure_config_exists(CONFIG_DIR) - # pylint: disable=no-self-use - def test_core_config_schema(self): - """Test core config schema.""" - for value in ( - {CONF_UNIT_SYSTEM: 'K'}, - {'time_zone': 'non-exist'}, - {'latitude': '91'}, - {'longitude': -181}, - {'customize': 'bla'}, - {'customize': {'light.sensor': 100}}, - {'customize': {'entity_id': []}}, - ): - with pytest.raises(MultipleInvalid): - config_util.CORE_CONFIG_SCHEMA(value) + config = config_util.load_yaml_config_file(YAML_PATH) - config_util.CORE_CONFIG_SCHEMA({ - 'name': 'Test name', - 'latitude': '-23.45', - 'longitude': '123.45', - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, - 'customize': { - 'sensor.temperature': { - 'hidden': True, - }, + assert DOMAIN in config + + ha_conf = config[DOMAIN] + + expected_values = { + CONF_LATITUDE: 32.8594, + CONF_LONGITUDE: -117.2073, + CONF_ELEVATION: 101, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_NAME: 'Home', + CONF_TIME_ZONE: 'America/Los_Angeles', + CONF_CUSTOMIZE: OrderedDict(), + } + + assert expected_values == ha_conf + assert mock_print.called + + +@mock.patch('builtins.print') +def test_create_default_config_returns_none_if_write_error(mock_print): + """Test the writing of a default configuration. + + Non existing folder returns None. + """ + assert config_util.create_default_config( + os.path.join(CONFIG_DIR, 'non_existing_dir/'), False) is None + assert mock_print.called + + +def test_core_config_schema(): + """Test core config schema.""" + for value in ( + {CONF_UNIT_SYSTEM: 'K'}, + {'time_zone': 'non-exist'}, + {'latitude': '91'}, + {'longitude': -181}, + {'customize': 'bla'}, + {'customize': {'light.sensor': 100}}, + {'customize': {'entity_id': []}}, + ): + with pytest.raises(MultipleInvalid): + config_util.CORE_CONFIG_SCHEMA(value) + + config_util.CORE_CONFIG_SCHEMA({ + 'name': 'Test name', + 'latitude': '-23.45', + 'longitude': '123.45', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + 'customize': { + 'sensor.temperature': { + 'hidden': True, }, + }, + }) + + +def test_customize_dict_schema(): + """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(): + """Test that customize_glob preserves order.""" + conf = config_util.CORE_CONFIG_SCHEMA( + {'customize_glob': OrderedDict()}) + assert isinstance(conf['customize_glob'], OrderedDict) + + +async def _compute_state(hass, config): + await config_util.async_process_ha_core_config(hass, config) + + entity = Entity() + entity.entity_id = 'test.test' + entity.hass = hass + entity.schedule_update_ha_state() + + await hass.async_block_till_done() + + return hass.states.get('test.test') + + +async def test_entity_customization(hass): + """Test entity customization through configuration.""" + config = {CONF_LATITUDE: 50, + CONF_LONGITUDE: 50, + CONF_NAME: 'Test', + CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} + + state = await _compute_state(hass, config) + + assert state.attributes['hidden'] + + +@mock.patch('homeassistant.config.shutil') +@mock.patch('homeassistant.config.os') +def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass): + """Test removal of library on upgrade from before 0.50.""" + ha_version = '0.49.0' + mock_os.path.isdir = mock.Mock(return_value=True) + mock_open = mock.mock_open() + with mock.patch('homeassistant.config.open', mock_open, create=True): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version + hass.config.path = mock.Mock() + config_util.process_ha_config_upgrade(hass) + hass_path = hass.config.path.return_value + + assert mock_os.path.isdir.call_count == 1 + assert mock_os.path.isdir.call_args == mock.call(hass_path) + assert mock_shutil.rmtree.call_count == 1 + assert mock_shutil.rmtree.call_args == mock.call(hass_path) + + +def test_process_config_upgrade(hass): + """Test update of version on upgrade.""" + ha_version = '0.92.0' + + mock_open = mock.mock_open() + with mock.patch('homeassistant.config.open', mock_open, create=True), \ + mock.patch.object(config_util, '__version__', '0.91.0'): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version + + config_util.process_ha_config_upgrade(hass) + + assert opened_file.write.call_count == 1 + assert opened_file.write.call_args == mock.call('0.91.0') + + +def test_config_upgrade_same_version(hass): + """Test no update of version on no upgrade.""" + ha_version = __version__ + + mock_open = mock.mock_open() + with mock.patch('homeassistant.config.open', mock_open, create=True): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version + + config_util.process_ha_config_upgrade(hass) + + assert opened_file.write.call_count == 0 + + +@mock.patch('homeassistant.config.find_config_file', mock.Mock()) +def test_config_upgrade_no_file(hass): + """Test update of version on upgrade, with no version file.""" + mock_open = mock.mock_open() + mock_open.side_effect = [FileNotFoundError(), + mock.DEFAULT, + mock.DEFAULT] + with mock.patch('homeassistant.config.open', mock_open, create=True): + opened_file = mock_open.return_value + # pylint: disable=no-member + config_util.process_ha_config_upgrade(hass) + assert opened_file.write.call_count == 1 + assert opened_file.write.call_args == mock.call(__version__) + + +@mock.patch('homeassistant.config.shutil') +@mock.patch('homeassistant.config.os') +@mock.patch('homeassistant.config.find_config_file', mock.Mock()) +def test_migrate_file_on_upgrade(mock_os, mock_shutil, hass): + """Test migrate of config files on upgrade.""" + ha_version = '0.7.0' + + mock_os.path.isdir = mock.Mock(return_value=True) + + mock_open = mock.mock_open() + + def _mock_isfile(filename): + return True + + with mock.patch('homeassistant.config.open', mock_open, create=True), \ + mock.patch('homeassistant.config.os.path.isfile', _mock_isfile): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version + + hass.config.path = mock.Mock() + + config_util.process_ha_config_upgrade(hass) + + assert mock_os.rename.call_count == 1 + + +@mock.patch('homeassistant.config.shutil') +@mock.patch('homeassistant.config.os') +@mock.patch('homeassistant.config.find_config_file', mock.Mock()) +def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass): + """Test not migrating config files on upgrade.""" + ha_version = '0.7.0' + + mock_os.path.isdir = mock.Mock(return_value=True) + + mock_open = mock.mock_open() + + def _mock_isfile(filename): + return False + + with mock.patch('homeassistant.config.open', mock_open, create=True), \ + mock.patch('homeassistant.config.os.path.isfile', _mock_isfile): + opened_file = mock_open.return_value + # pylint: disable=no-member + opened_file.readline.return_value = ha_version + + hass.config.path = mock.Mock() + + config_util.process_ha_config_upgrade(hass) + + assert mock_os.rename.call_count == 0 + + +async def test_loading_configuration(hass): + """Test loading core config onto hass object.""" + hass.config = mock.Mock() + + await config_util.async_process_ha_core_config(hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + 'time_zone': 'America/New_York', + 'whitelist_external_dirs': '/tmp', + }) + + assert hass.config.latitude == 60 + assert hass.config.longitude == 50 + assert hass.config.elevation == 25 + assert hass.config.location_name == 'Huis' + assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL + assert hass.config.time_zone.zone == 'America/New_York' + assert len(hass.config.whitelist_external_dirs) == 2 + assert '/tmp' in hass.config.whitelist_external_dirs + + +async def test_loading_configuration_temperature_unit(hass): + """Test backward compatibility when loading core config.""" + hass.config = mock.Mock() + + await config_util.async_process_ha_core_config(hass, { + 'latitude': 60, + 'longitude': 50, + 'elevation': 25, + 'name': 'Huis', + CONF_TEMPERATURE_UNIT: 'C', + 'time_zone': 'America/New_York', + }) + + assert hass.config.latitude == 60 + assert hass.config.longitude == 50 + assert hass.config.elevation == 25 + assert hass.config.location_name == 'Huis' + assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + assert hass.config.time_zone.zone == 'America/New_York' + + +async def test_loading_configuration_from_packages(hass): + """Test loading packages config onto hass object config.""" + hass.config = mock.Mock() + + await config_util.async_process_ha_core_config(hass, { + 'latitude': 39, + 'longitude': -1, + 'elevation': 500, + 'name': 'Huis', + CONF_TEMPERATURE_UNIT: 'C', + 'time_zone': 'Europe/Madrid', + 'packages': { + 'package_1': {'wake_on_lan': None}, + 'package_2': {'light': {'platform': 'hue'}, + 'media_extractor': None, + 'sun': None}}, + }) + + # Empty packages not allowed + with pytest.raises(MultipleInvalid): + await config_util.async_process_ha_core_config(hass, { + 'latitude': 39, + 'longitude': -1, + 'elevation': 500, + 'name': 'Huis', + CONF_TEMPERATURE_UNIT: 'C', + 'time_zone': 'Europe/Madrid', + 'packages': {'empty_package': None}, }) - 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) +@asynctest.mock.patch('homeassistant.util.location.detect_location_info', + autospec=True, return_value=location_util.LocationInfo( + '0.0.0.0', 'US', 'United States', 'CA', 'California', + 'San Diego', '92122', 'America/Los_Angeles', 32.8594, + -117.2073, True)) +@asynctest.mock.patch('homeassistant.util.location.elevation', + autospec=True, return_value=101) +async def test_discovering_configuration(mock_detect, mock_elevation, hass): + """Test auto discovery for missing core configs.""" + hass.config.latitude = None + hass.config.longitude = None + hass.config.elevation = None + hass.config.location_name = None + hass.config.time_zone = None - 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 - } + await config_util.async_process_ha_core_config(hass, {}) - def test_customize_glob_is_ordered(self): - """Test that customize_glob preserves order.""" - conf = config_util.CORE_CONFIG_SCHEMA( - {'customize_glob': OrderedDict()}) - assert isinstance(conf['customize_glob'], OrderedDict) + assert hass.config.latitude == 32.8594 + assert hass.config.longitude == -117.2073 + assert hass.config.elevation == 101 + assert hass.config.location_name == 'San Diego' + assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC + assert hass.config.units.is_metric + assert hass.config.time_zone.zone == 'America/Los_Angeles' - def _compute_state(self, config): - run_coroutine_threadsafe( - config_util.async_process_ha_core_config(self.hass, config), - self.hass.loop).result() - entity = Entity() - entity.entity_id = 'test.test' - entity.hass = self.hass - entity.schedule_update_ha_state() +@asynctest.mock.patch('homeassistant.util.location.detect_location_info', + autospec=True, return_value=None) +@asynctest.mock.patch('homeassistant.util.location.elevation', return_value=0) +async def test_discovering_configuration_auto_detect_fails(mock_detect, + mock_elevation, + hass): + """Test config remains unchanged if discovery fails.""" + hass.config = Config() + hass.config.config_dir = "/test/config" - self.hass.block_till_done() + await config_util.async_process_ha_core_config(hass, {}) - return self.hass.states.get('test.test') + blankConfig = Config() + assert hass.config.latitude == blankConfig.latitude + assert hass.config.longitude == blankConfig.longitude + assert hass.config.elevation == blankConfig.elevation + assert hass.config.location_name == blankConfig.location_name + assert hass.config.units == blankConfig.units + assert hass.config.time_zone == blankConfig.time_zone + assert len(hass.config.whitelist_external_dirs) == 1 + assert "/test/config/www" in hass.config.whitelist_external_dirs - def test_entity_customization(self): - """Test entity customization through configuration.""" - config = {CONF_LATITUDE: 50, - CONF_LONGITUDE: 50, - CONF_NAME: 'Test', - CONF_CUSTOMIZE: {'test.test': {'hidden': True}}} - state = self._compute_state(config) +@asynctest.mock.patch( + 'homeassistant.scripts.check_config.check_ha_config_file') +async def test_check_ha_config_file_correct(mock_check, hass): + """Check that restart propagates to stop.""" + mock_check.return_value = check_config.HomeAssistantConfig() + assert await config_util.async_check_ha_config_file(hass) is None - assert state.attributes['hidden'] - @mock.patch('homeassistant.config.shutil') - @mock.patch('homeassistant.config.os') - def test_remove_lib_on_upgrade(self, mock_os, mock_shutil): - """Test removal of library on upgrade from before 0.50.""" - ha_version = '0.49.0' - mock_os.path.isdir = mock.Mock(return_value=True) - mock_open = mock.mock_open() - with mock.patch('homeassistant.config.open', mock_open, create=True): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - self.hass.config.path = mock.Mock() - config_util.process_ha_config_upgrade(self.hass) - hass_path = self.hass.config.path.return_value +@asynctest.mock.patch( + 'homeassistant.scripts.check_config.check_ha_config_file') +async def test_check_ha_config_file_wrong(mock_check, hass): + """Check that restart with a bad config doesn't propagate to stop.""" + mock_check.return_value = check_config.HomeAssistantConfig() + mock_check.return_value.add_error("bad") - assert mock_os.path.isdir.call_count == 1 - assert mock_os.path.isdir.call_args == mock.call(hass_path) - assert mock_shutil.rmtree.call_count == 1 - assert mock_shutil.rmtree.call_args == mock.call(hass_path) - - def test_process_config_upgrade(self): - """Test update of version on upgrade.""" - ha_version = '0.92.0' - - mock_open = mock.mock_open() - with mock.patch('homeassistant.config.open', mock_open, create=True), \ - mock.patch.object(config_util, '__version__', '0.91.0'): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - config_util.process_ha_config_upgrade(self.hass) - - assert opened_file.write.call_count == 1 - assert opened_file.write.call_args == mock.call('0.91.0') - - def test_config_upgrade_same_version(self): - """Test no update of version on no upgrade.""" - ha_version = __version__ - - mock_open = mock.mock_open() - with mock.patch('homeassistant.config.open', mock_open, create=True): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - config_util.process_ha_config_upgrade(self.hass) - - assert opened_file.write.call_count == 0 - - @mock.patch('homeassistant.config.find_config_file', mock.Mock()) - def test_config_upgrade_no_file(self): - """Test update of version on upgrade, with no version file.""" - mock_open = mock.mock_open() - mock_open.side_effect = [FileNotFoundError(), - mock.DEFAULT, - mock.DEFAULT] - with mock.patch('homeassistant.config.open', mock_open, create=True): - opened_file = mock_open.return_value - # pylint: disable=no-member - config_util.process_ha_config_upgrade(self.hass) - assert opened_file.write.call_count == 1 - assert opened_file.write.call_args == mock.call(__version__) - - @mock.patch('homeassistant.config.shutil') - @mock.patch('homeassistant.config.os') - @mock.patch('homeassistant.config.find_config_file', mock.Mock()) - def test_migrate_file_on_upgrade(self, mock_os, mock_shutil): - """Test migrate of config files on upgrade.""" - ha_version = '0.7.0' - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return True - - with mock.patch('homeassistant.config.open', mock_open, create=True), \ - mock.patch( - 'homeassistant.config.os.path.isfile', _mock_isfile): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - self.hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(self.hass) - - assert mock_os.rename.call_count == 1 - - @mock.patch('homeassistant.config.shutil') - @mock.patch('homeassistant.config.os') - @mock.patch('homeassistant.config.find_config_file', mock.Mock()) - def test_migrate_no_file_on_upgrade(self, mock_os, mock_shutil): - """Test not migrating config files on upgrade.""" - ha_version = '0.7.0' - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return False - - with mock.patch('homeassistant.config.open', mock_open, create=True), \ - mock.patch( - 'homeassistant.config.os.path.isfile', _mock_isfile): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - self.hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(self.hass) - - assert mock_os.rename.call_count == 0 - - def test_loading_configuration(self): - """Test loading core config onto hass object.""" - self.hass.config = mock.Mock() - - run_coroutine_threadsafe( - config_util.async_process_ha_core_config(self.hass, { - 'latitude': 60, - 'longitude': 50, - 'elevation': 25, - 'name': 'Huis', - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - 'time_zone': 'America/New_York', - 'whitelist_external_dirs': '/tmp', - }), self.hass.loop).result() - - assert self.hass.config.latitude == 60 - assert self.hass.config.longitude == 50 - assert self.hass.config.elevation == 25 - assert self.hass.config.location_name == 'Huis' - assert self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL - assert self.hass.config.time_zone.zone == 'America/New_York' - assert len(self.hass.config.whitelist_external_dirs) == 2 - assert '/tmp' in self.hass.config.whitelist_external_dirs - - def test_loading_configuration_temperature_unit(self): - """Test backward compatibility when loading core config.""" - self.hass.config = mock.Mock() - - run_coroutine_threadsafe( - config_util.async_process_ha_core_config(self.hass, { - 'latitude': 60, - 'longitude': 50, - 'elevation': 25, - 'name': 'Huis', - CONF_TEMPERATURE_UNIT: 'C', - 'time_zone': 'America/New_York', - }), self.hass.loop).result() - - assert self.hass.config.latitude == 60 - assert self.hass.config.longitude == 50 - assert self.hass.config.elevation == 25 - assert self.hass.config.location_name == 'Huis' - assert self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert self.hass.config.time_zone.zone == 'America/New_York' - - def test_loading_configuration_from_packages(self): - """Test loading packages config onto hass object config.""" - self.hass.config = mock.Mock() - - run_coroutine_threadsafe( - config_util.async_process_ha_core_config(self.hass, { - 'latitude': 39, - 'longitude': -1, - 'elevation': 500, - 'name': 'Huis', - CONF_TEMPERATURE_UNIT: 'C', - 'time_zone': 'Europe/Madrid', - 'packages': { - 'package_1': {'wake_on_lan': None}, - 'package_2': {'light': {'platform': 'hue'}, - 'media_extractor': None, - 'sun': None}}, - }), self.hass.loop).result() - - # Empty packages not allowed - with pytest.raises(MultipleInvalid): - run_coroutine_threadsafe( - config_util.async_process_ha_core_config(self.hass, { - 'latitude': 39, - 'longitude': -1, - 'elevation': 500, - 'name': 'Huis', - CONF_TEMPERATURE_UNIT: 'C', - 'time_zone': 'Europe/Madrid', - 'packages': {'empty_package': None}, - }), self.hass.loop).result() - - @mock.patch('homeassistant.util.location.detect_location_info', - autospec=True, return_value=location_util.LocationInfo( - '0.0.0.0', 'US', 'United States', 'CA', 'California', - 'San Diego', '92122', 'America/Los_Angeles', 32.8594, - -117.2073, True)) - @mock.patch('homeassistant.util.location.elevation', - autospec=True, return_value=101) - def test_discovering_configuration(self, mock_detect, mock_elevation): - """Test auto discovery for missing core configs.""" - self.hass.config.latitude = None - self.hass.config.longitude = None - self.hass.config.elevation = None - self.hass.config.location_name = None - self.hass.config.time_zone = None - - run_coroutine_threadsafe( - config_util.async_process_ha_core_config( - self.hass, {}), self.hass.loop - ).result() - - assert self.hass.config.latitude == 32.8594 - assert self.hass.config.longitude == -117.2073 - assert self.hass.config.elevation == 101 - assert self.hass.config.location_name == 'San Diego' - assert self.hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert self.hass.config.units.is_metric - assert self.hass.config.time_zone.zone == 'America/Los_Angeles' - - @mock.patch('homeassistant.util.location.detect_location_info', - autospec=True, return_value=None) - @mock.patch('homeassistant.util.location.elevation', return_value=0) - def test_discovering_configuration_auto_detect_fails(self, mock_detect, - mock_elevation): - """Test config remains unchanged if discovery fails.""" - self.hass.config = Config() - self.hass.config.config_dir = "/test/config" - - run_coroutine_threadsafe( - config_util.async_process_ha_core_config( - self.hass, {}), self.hass.loop - ).result() - - blankConfig = Config() - assert self.hass.config.latitude == blankConfig.latitude - assert self.hass.config.longitude == blankConfig.longitude - assert self.hass.config.elevation == blankConfig.elevation - assert self.hass.config.location_name == blankConfig.location_name - assert self.hass.config.units == blankConfig.units - assert self.hass.config.time_zone == blankConfig.time_zone - assert len(self.hass.config.whitelist_external_dirs) == 1 - assert "/test/config/www" in self.hass.config.whitelist_external_dirs - - @asynctest.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.""" - mock_check.return_value = check_config.HomeAssistantConfig() - assert run_coroutine_threadsafe( - config_util.async_check_ha_config_file(self.hass), - self.hass.loop - ).result() is None - - @asynctest.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.""" - 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() == 'bad' + assert await config_util.async_check_ha_config_file(hass) == 'bad' @asynctest.mock.patch('homeassistant.config.os.path.isfile', diff --git a/tests/util/test_color.py b/tests/util/test_color.py index 14195d43821..ff382ae5c0a 100644 --- a/tests/util/test_color.py +++ b/tests/util/test_color.py @@ -1,10 +1,9 @@ """Test Home Assistant color util methods.""" -import unittest -import homeassistant.util.color as color_util - import pytest import voluptuous as vol +import homeassistant.util.color as color_util + GAMUT = color_util.GamutType(color_util.XYPoint(0.704, 0.296), color_util.XYPoint(0.2151, 0.7106), color_util.XYPoint(0.138, 0.08)) @@ -22,408 +21,340 @@ GAMUT_INVALID_4 = color_util.GamutType(color_util.XYPoint(0.1, 0.1), color_util.XYPoint(0.7, 0.7)) -class TestColorUtil(unittest.TestCase): - """Test color util methods.""" +# pylint: disable=invalid-name +def test_color_RGB_to_xy_brightness(): + """Test color_RGB_to_xy_brightness.""" + assert color_util.color_RGB_to_xy_brightness(0, 0, 0) == (0, 0, 0) + assert color_util.color_RGB_to_xy_brightness(255, 255, 255) == \ + (0.323, 0.329, 255) - # pylint: disable=invalid-name - def test_color_RGB_to_xy_brightness(self): - """Test color_RGB_to_xy_brightness.""" - assert (0, 0, 0) == \ - color_util.color_RGB_to_xy_brightness(0, 0, 0) - assert (0.323, 0.329, 255) == \ - color_util.color_RGB_to_xy_brightness(255, 255, 255) + assert color_util.color_RGB_to_xy_brightness(0, 0, 255) == \ + (0.136, 0.04, 12) - assert (0.136, 0.04, 12) == \ - color_util.color_RGB_to_xy_brightness(0, 0, 255) + assert color_util.color_RGB_to_xy_brightness(0, 255, 0) == \ + (0.172, 0.747, 170) - assert (0.172, 0.747, 170) == \ - color_util.color_RGB_to_xy_brightness(0, 255, 0) + assert color_util.color_RGB_to_xy_brightness(255, 0, 0) == \ + (0.701, 0.299, 72) - assert (0.701, 0.299, 72) == \ - color_util.color_RGB_to_xy_brightness(255, 0, 0) + assert color_util.color_RGB_to_xy_brightness(128, 0, 0) == \ + (0.701, 0.299, 16) - assert (0.701, 0.299, 16) == \ - color_util.color_RGB_to_xy_brightness(128, 0, 0) + assert color_util.color_RGB_to_xy_brightness(255, 0, 0, GAMUT) == \ + (0.7, 0.299, 72) - assert (0.7, 0.299, 72) == \ - color_util.color_RGB_to_xy_brightness(255, 0, 0, GAMUT) + assert color_util.color_RGB_to_xy_brightness(0, 255, 0, GAMUT) == \ + (0.215, 0.711, 170) - assert (0.215, 0.711, 170) == \ - color_util.color_RGB_to_xy_brightness(0, 255, 0, GAMUT) + assert color_util.color_RGB_to_xy_brightness(0, 0, 255, GAMUT) == \ + (0.138, 0.08, 12) - assert (0.138, 0.08, 12) == \ - color_util.color_RGB_to_xy_brightness(0, 0, 255, GAMUT) - def test_color_RGB_to_xy(self): - """Test color_RGB_to_xy.""" - assert (0, 0) == \ - color_util.color_RGB_to_xy(0, 0, 0) - assert (0.323, 0.329) == \ - color_util.color_RGB_to_xy(255, 255, 255) +def test_color_RGB_to_xy(): + """Test color_RGB_to_xy.""" + assert color_util.color_RGB_to_xy(0, 0, 0) == (0, 0) + assert color_util.color_RGB_to_xy(255, 255, 255) == (0.323, 0.329) - assert (0.136, 0.04) == \ - color_util.color_RGB_to_xy(0, 0, 255) + assert color_util.color_RGB_to_xy(0, 0, 255) == (0.136, 0.04) - assert (0.172, 0.747) == \ - color_util.color_RGB_to_xy(0, 255, 0) + assert color_util.color_RGB_to_xy(0, 255, 0) == (0.172, 0.747) - assert (0.701, 0.299) == \ - color_util.color_RGB_to_xy(255, 0, 0) + assert color_util.color_RGB_to_xy(255, 0, 0) == (0.701, 0.299) - assert (0.701, 0.299) == \ - color_util.color_RGB_to_xy(128, 0, 0) + assert color_util.color_RGB_to_xy(128, 0, 0) == (0.701, 0.299) - assert (0.138, 0.08) == \ - color_util.color_RGB_to_xy(0, 0, 255, GAMUT) + assert color_util.color_RGB_to_xy(0, 0, 255, GAMUT) == (0.138, 0.08) - assert (0.215, 0.711) == \ - color_util.color_RGB_to_xy(0, 255, 0, GAMUT) + assert color_util.color_RGB_to_xy(0, 255, 0, GAMUT) == (0.215, 0.711) - assert (0.7, 0.299) == \ - color_util.color_RGB_to_xy(255, 0, 0, GAMUT) + assert color_util.color_RGB_to_xy(255, 0, 0, GAMUT) == (0.7, 0.299) - def test_color_xy_brightness_to_RGB(self): - """Test color_xy_brightness_to_RGB.""" - assert (0, 0, 0) == \ - color_util.color_xy_brightness_to_RGB(1, 1, 0) - assert (194, 186, 169) == \ - color_util.color_xy_brightness_to_RGB(.35, .35, 128) +def test_color_xy_brightness_to_RGB(): + """Test color_xy_brightness_to_RGB.""" + assert color_util.color_xy_brightness_to_RGB(1, 1, 0) == (0, 0, 0) - assert (255, 243, 222) == \ - color_util.color_xy_brightness_to_RGB(.35, .35, 255) + assert color_util.color_xy_brightness_to_RGB(.35, .35, 128) == \ + (194, 186, 169) - assert (255, 0, 60) == \ - color_util.color_xy_brightness_to_RGB(1, 0, 255) + assert color_util.color_xy_brightness_to_RGB(.35, .35, 255) == \ + (255, 243, 222) - assert (0, 255, 0) == \ - color_util.color_xy_brightness_to_RGB(0, 1, 255) + assert color_util.color_xy_brightness_to_RGB(1, 0, 255) == (255, 0, 60) - assert (0, 63, 255) == \ - color_util.color_xy_brightness_to_RGB(0, 0, 255) + assert color_util.color_xy_brightness_to_RGB(0, 1, 255) == (0, 255, 0) - assert (255, 0, 3) == \ - color_util.color_xy_brightness_to_RGB(1, 0, 255, GAMUT) + assert color_util.color_xy_brightness_to_RGB(0, 0, 255) == (0, 63, 255) - assert (82, 255, 0) == \ - color_util.color_xy_brightness_to_RGB(0, 1, 255, GAMUT) + assert color_util.color_xy_brightness_to_RGB(1, 0, 255, GAMUT) == \ + (255, 0, 3) - assert (9, 85, 255) == \ - color_util.color_xy_brightness_to_RGB(0, 0, 255, GAMUT) + assert color_util.color_xy_brightness_to_RGB(0, 1, 255, GAMUT) == \ + (82, 255, 0) - def test_color_xy_to_RGB(self): - """Test color_xy_to_RGB.""" - assert (255, 243, 222) == \ - color_util.color_xy_to_RGB(.35, .35) + assert color_util.color_xy_brightness_to_RGB(0, 0, 255, GAMUT) == \ + (9, 85, 255) - assert (255, 0, 60) == \ - color_util.color_xy_to_RGB(1, 0) - assert (0, 255, 0) == \ - color_util.color_xy_to_RGB(0, 1) +def test_color_xy_to_RGB(): + """Test color_xy_to_RGB.""" + assert color_util.color_xy_to_RGB(.35, .35) == (255, 243, 222) - assert (0, 63, 255) == \ - color_util.color_xy_to_RGB(0, 0) + assert color_util.color_xy_to_RGB(1, 0) == (255, 0, 60) - assert (255, 0, 3) == \ - color_util.color_xy_to_RGB(1, 0, GAMUT) + assert color_util.color_xy_to_RGB(0, 1) == (0, 255, 0) - assert (82, 255, 0) == \ - color_util.color_xy_to_RGB(0, 1, GAMUT) + assert color_util.color_xy_to_RGB(0, 0) == (0, 63, 255) - assert (9, 85, 255) == \ - color_util.color_xy_to_RGB(0, 0, GAMUT) + assert color_util.color_xy_to_RGB(1, 0, GAMUT) == (255, 0, 3) - def test_color_RGB_to_hsv(self): - """Test color_RGB_to_hsv.""" - assert (0, 0, 0) == \ - color_util.color_RGB_to_hsv(0, 0, 0) + assert color_util.color_xy_to_RGB(0, 1, GAMUT) == (82, 255, 0) - assert (0, 0, 100) == \ - color_util.color_RGB_to_hsv(255, 255, 255) + assert color_util.color_xy_to_RGB(0, 0, GAMUT) == (9, 85, 255) - assert (240, 100, 100) == \ - color_util.color_RGB_to_hsv(0, 0, 255) - assert (120, 100, 100) == \ - color_util.color_RGB_to_hsv(0, 255, 0) +def test_color_RGB_to_hsv(): + """Test color_RGB_to_hsv.""" + assert color_util.color_RGB_to_hsv(0, 0, 0) == (0, 0, 0) - assert (0, 100, 100) == \ - color_util.color_RGB_to_hsv(255, 0, 0) + assert color_util.color_RGB_to_hsv(255, 255, 255) == (0, 0, 100) - def test_color_hsv_to_RGB(self): - """Test color_hsv_to_RGB.""" - assert (0, 0, 0) == \ - color_util.color_hsv_to_RGB(0, 0, 0) + assert color_util.color_RGB_to_hsv(0, 0, 255) == (240, 100, 100) - assert (255, 255, 255) == \ - color_util.color_hsv_to_RGB(0, 0, 100) + assert color_util.color_RGB_to_hsv(0, 255, 0) == (120, 100, 100) - assert (0, 0, 255) == \ - color_util.color_hsv_to_RGB(240, 100, 100) + assert color_util.color_RGB_to_hsv(255, 0, 0) == (0, 100, 100) - assert (0, 255, 0) == \ - color_util.color_hsv_to_RGB(120, 100, 100) - assert (255, 0, 0) == \ - color_util.color_hsv_to_RGB(0, 100, 100) +def test_color_hsv_to_RGB(): + """Test color_hsv_to_RGB.""" + assert color_util.color_hsv_to_RGB(0, 0, 0) == (0, 0, 0) - def test_color_hsb_to_RGB(self): - """Test color_hsb_to_RGB.""" - assert (0, 0, 0) == \ - color_util.color_hsb_to_RGB(0, 0, 0) + assert color_util.color_hsv_to_RGB(0, 0, 100) == (255, 255, 255) - assert (255, 255, 255) == \ - color_util.color_hsb_to_RGB(0, 0, 1.0) + assert color_util.color_hsv_to_RGB(240, 100, 100) == (0, 0, 255) - assert (0, 0, 255) == \ - color_util.color_hsb_to_RGB(240, 1.0, 1.0) + assert color_util.color_hsv_to_RGB(120, 100, 100) == (0, 255, 0) - assert (0, 255, 0) == \ - color_util.color_hsb_to_RGB(120, 1.0, 1.0) + assert color_util.color_hsv_to_RGB(0, 100, 100) == (255, 0, 0) - assert (255, 0, 0) == \ - color_util.color_hsb_to_RGB(0, 1.0, 1.0) - def test_color_xy_to_hs(self): - """Test color_xy_to_hs.""" - assert (47.294, 100) == \ - color_util.color_xy_to_hs(1, 1) +def test_color_hsb_to_RGB(): + """Test color_hsb_to_RGB.""" + assert color_util.color_hsb_to_RGB(0, 0, 0) == (0, 0, 0) - assert (38.182, 12.941) == \ - color_util.color_xy_to_hs(.35, .35) + assert color_util.color_hsb_to_RGB(0, 0, 1.0) == (255, 255, 255) - assert (345.882, 100) == \ - color_util.color_xy_to_hs(1, 0) + assert color_util.color_hsb_to_RGB(240, 1.0, 1.0) == (0, 0, 255) - assert (120, 100) == \ - color_util.color_xy_to_hs(0, 1) + assert color_util.color_hsb_to_RGB(120, 1.0, 1.0) == (0, 255, 0) - assert (225.176, 100) == \ - color_util.color_xy_to_hs(0, 0) + assert color_util.color_hsb_to_RGB(0, 1.0, 1.0) == (255, 0, 0) - assert (359.294, 100) == \ - color_util.color_xy_to_hs(1, 0, GAMUT) - assert (100.706, 100) == \ - color_util.color_xy_to_hs(0, 1, GAMUT) +def test_color_xy_to_hs(): + """Test color_xy_to_hs.""" + assert color_util.color_xy_to_hs(1, 1) == (47.294, 100) - assert (221.463, 96.471) == \ - color_util.color_xy_to_hs(0, 0, GAMUT) + assert color_util.color_xy_to_hs(.35, .35) == (38.182, 12.941) - def test_color_hs_to_xy(self): - """Test color_hs_to_xy.""" - assert (0.151, 0.343) == \ - color_util.color_hs_to_xy(180, 100) + assert color_util.color_xy_to_hs(1, 0) == (345.882, 100) - assert (0.356, 0.321) == \ - color_util.color_hs_to_xy(350, 12.5) + assert color_util.color_xy_to_hs(0, 1) == (120, 100) - assert (0.229, 0.474) == \ - color_util.color_hs_to_xy(140, 50) + assert color_util.color_xy_to_hs(0, 0) == (225.176, 100) - assert (0.474, 0.317) == \ - color_util.color_hs_to_xy(0, 40) + assert color_util.color_xy_to_hs(1, 0, GAMUT) == (359.294, 100) - assert (0.323, 0.329) == \ - color_util.color_hs_to_xy(360, 0) + assert color_util.color_xy_to_hs(0, 1, GAMUT) == (100.706, 100) - assert (0.7, 0.299) == \ - color_util.color_hs_to_xy(0, 100, GAMUT) + assert color_util.color_xy_to_hs(0, 0, GAMUT) == (221.463, 96.471) - assert (0.215, 0.711) == \ - color_util.color_hs_to_xy(120, 100, GAMUT) - assert (0.17, 0.34) == \ - color_util.color_hs_to_xy(180, 100, GAMUT) +def test_color_hs_to_xy(): + """Test color_hs_to_xy.""" + assert color_util.color_hs_to_xy(180, 100) == (0.151, 0.343) - assert (0.138, 0.08) == \ - color_util.color_hs_to_xy(240, 100, GAMUT) + assert color_util.color_hs_to_xy(350, 12.5) == (0.356, 0.321) - assert (0.7, 0.299) == \ - color_util.color_hs_to_xy(360, 100, GAMUT) + assert color_util.color_hs_to_xy(140, 50) == (0.229, 0.474) - def test_rgb_hex_to_rgb_list(self): - """Test rgb_hex_to_rgb_list.""" - assert [255, 255, 255] == \ - color_util.rgb_hex_to_rgb_list('ffffff') + assert color_util.color_hs_to_xy(0, 40) == (0.474, 0.317) - assert [0, 0, 0] == \ - color_util.rgb_hex_to_rgb_list('000000') + assert color_util.color_hs_to_xy(360, 0) == (0.323, 0.329) - assert [255, 255, 255, 255] == \ - color_util.rgb_hex_to_rgb_list('ffffffff') + assert color_util.color_hs_to_xy(0, 100, GAMUT) == (0.7, 0.299) - assert [0, 0, 0, 0] == \ - color_util.rgb_hex_to_rgb_list('00000000') + assert color_util.color_hs_to_xy(120, 100, GAMUT) == (0.215, 0.711) - assert [51, 153, 255] == \ - color_util.rgb_hex_to_rgb_list('3399ff') + assert color_util.color_hs_to_xy(180, 100, GAMUT) == (0.17, 0.34) - assert [51, 153, 255, 0] == \ - color_util.rgb_hex_to_rgb_list('3399ff00') + assert color_util.color_hs_to_xy(240, 100, GAMUT) == (0.138, 0.08) - def test_color_name_to_rgb_valid_name(self): - """Test color_name_to_rgb.""" - assert (255, 0, 0) == \ - color_util.color_name_to_rgb('red') + assert color_util.color_hs_to_xy(360, 100, GAMUT) == (0.7, 0.299) - assert (0, 0, 255) == \ - color_util.color_name_to_rgb('blue') - assert (0, 128, 0) == \ - color_util.color_name_to_rgb('green') +def test_rgb_hex_to_rgb_list(): + """Test rgb_hex_to_rgb_list.""" + assert [255, 255, 255] == \ + color_util.rgb_hex_to_rgb_list('ffffff') - # spaces in the name - assert (72, 61, 139) == \ - color_util.color_name_to_rgb('dark slate blue') + assert [0, 0, 0] == \ + color_util.rgb_hex_to_rgb_list('000000') - # spaces removed from name - assert (72, 61, 139) == \ - color_util.color_name_to_rgb('darkslateblue') - assert (72, 61, 139) == \ - color_util.color_name_to_rgb('dark slateblue') - assert (72, 61, 139) == \ - color_util.color_name_to_rgb('darkslate blue') + assert [255, 255, 255, 255] == \ + color_util.rgb_hex_to_rgb_list('ffffffff') - def test_color_name_to_rgb_unknown_name_raises_value_error(self): - """Test color_name_to_rgb.""" - with pytest.raises(ValueError): - color_util.color_name_to_rgb('not a color') + assert [0, 0, 0, 0] == \ + color_util.rgb_hex_to_rgb_list('00000000') - def test_color_rgb_to_rgbw(self): - """Test color_rgb_to_rgbw.""" - assert (0, 0, 0, 0) == \ - color_util.color_rgb_to_rgbw(0, 0, 0) + assert [51, 153, 255] == \ + color_util.rgb_hex_to_rgb_list('3399ff') - assert (0, 0, 0, 255) == \ - color_util.color_rgb_to_rgbw(255, 255, 255) + assert [51, 153, 255, 0] == \ + color_util.rgb_hex_to_rgb_list('3399ff00') - assert (255, 0, 0, 0) == \ - color_util.color_rgb_to_rgbw(255, 0, 0) - assert (0, 255, 0, 0) == \ - color_util.color_rgb_to_rgbw(0, 255, 0) +def test_color_name_to_rgb_valid_name(): + """Test color_name_to_rgb.""" + assert color_util.color_name_to_rgb('red') == (255, 0, 0) - assert (0, 0, 255, 0) == \ - color_util.color_rgb_to_rgbw(0, 0, 255) + assert color_util.color_name_to_rgb('blue') == (0, 0, 255) - assert (255, 127, 0, 0) == \ - color_util.color_rgb_to_rgbw(255, 127, 0) + assert color_util.color_name_to_rgb('green') == (0, 128, 0) - assert (255, 0, 0, 253) == \ - color_util.color_rgb_to_rgbw(255, 127, 127) + # spaces in the name + assert color_util.color_name_to_rgb('dark slate blue') == (72, 61, 139) - assert (0, 0, 0, 127) == \ - color_util.color_rgb_to_rgbw(127, 127, 127) + # spaces removed from name + assert color_util.color_name_to_rgb('darkslateblue') == (72, 61, 139) + assert color_util.color_name_to_rgb('dark slateblue') == (72, 61, 139) + assert color_util.color_name_to_rgb('darkslate blue') == (72, 61, 139) - def test_color_rgbw_to_rgb(self): - """Test color_rgbw_to_rgb.""" - assert (0, 0, 0) == \ - color_util.color_rgbw_to_rgb(0, 0, 0, 0) - assert (255, 255, 255) == \ - color_util.color_rgbw_to_rgb(0, 0, 0, 255) +def test_color_name_to_rgb_unknown_name_raises_value_error(): + """Test color_name_to_rgb.""" + with pytest.raises(ValueError): + color_util.color_name_to_rgb('not a color') - assert (255, 0, 0) == \ - color_util.color_rgbw_to_rgb(255, 0, 0, 0) - assert (0, 255, 0) == \ - color_util.color_rgbw_to_rgb(0, 255, 0, 0) +def test_color_rgb_to_rgbw(): + """Test color_rgb_to_rgbw.""" + assert color_util.color_rgb_to_rgbw(0, 0, 0) == (0, 0, 0, 0) - assert (0, 0, 255) == \ - color_util.color_rgbw_to_rgb(0, 0, 255, 0) + assert color_util.color_rgb_to_rgbw(255, 255, 255) == (0, 0, 0, 255) - assert (255, 127, 0) == \ - color_util.color_rgbw_to_rgb(255, 127, 0, 0) + assert color_util.color_rgb_to_rgbw(255, 0, 0) == (255, 0, 0, 0) - assert (255, 127, 127) == \ - color_util.color_rgbw_to_rgb(255, 0, 0, 253) + assert color_util.color_rgb_to_rgbw(0, 255, 0) == (0, 255, 0, 0) - assert (127, 127, 127) == \ - color_util.color_rgbw_to_rgb(0, 0, 0, 127) + assert color_util.color_rgb_to_rgbw(0, 0, 255) == (0, 0, 255, 0) - def test_color_rgb_to_hex(self): - """Test color_rgb_to_hex.""" - assert color_util.color_rgb_to_hex(255, 255, 255) == 'ffffff' - assert color_util.color_rgb_to_hex(0, 0, 0) == '000000' - assert color_util.color_rgb_to_hex(51, 153, 255) == '3399ff' - assert color_util.color_rgb_to_hex(255, 67.9204190, 0) == 'ff4400' + assert color_util.color_rgb_to_rgbw(255, 127, 0) == (255, 127, 0, 0) - def test_gamut(self): - """Test gamut functions.""" - assert color_util.check_valid_gamut(GAMUT) - assert not color_util.check_valid_gamut(GAMUT_INVALID_1) - assert not color_util.check_valid_gamut(GAMUT_INVALID_2) - assert not color_util.check_valid_gamut(GAMUT_INVALID_3) - assert not color_util.check_valid_gamut(GAMUT_INVALID_4) + assert color_util.color_rgb_to_rgbw(255, 127, 127) == (255, 0, 0, 253) + assert color_util.color_rgb_to_rgbw(127, 127, 127) == (0, 0, 0, 127) -class ColorTemperatureMiredToKelvinTests(unittest.TestCase): - """Test color_temperature_mired_to_kelvin.""" - def test_should_return_25000_kelvin_when_input_is_40_mired(self): - """Function should return 25000K if given 40 mired.""" - kelvin = color_util.color_temperature_mired_to_kelvin(40) - assert 25000 == kelvin +def test_color_rgbw_to_rgb(): + """Test color_rgbw_to_rgb.""" + assert color_util.color_rgbw_to_rgb(0, 0, 0, 0) == (0, 0, 0) - def test_should_return_5000_kelvin_when_input_is_200_mired(self): - """Function should return 5000K if given 200 mired.""" - kelvin = color_util.color_temperature_mired_to_kelvin(200) - assert 5000 == kelvin - - -class ColorTemperatureKelvinToMiredTests(unittest.TestCase): - """Test color_temperature_kelvin_to_mired.""" - - def test_should_return_40_mired_when_input_is_25000_kelvin(self): - """Function should return 40 mired when given 25000 Kelvin.""" - mired = color_util.color_temperature_kelvin_to_mired(25000) - assert 40 == mired - - def test_should_return_200_mired_when_input_is_5000_kelvin(self): - """Function should return 200 mired when given 5000 Kelvin.""" - mired = color_util.color_temperature_kelvin_to_mired(5000) - assert 200 == mired + assert color_util.color_rgbw_to_rgb(0, 0, 0, 255) == (255, 255, 255) + assert color_util.color_rgbw_to_rgb(255, 0, 0, 0) == (255, 0, 0) -class ColorTemperatureToRGB(unittest.TestCase): - """Test color_temperature_to_rgb.""" + assert color_util.color_rgbw_to_rgb(0, 255, 0, 0) == (0, 255, 0) - def test_returns_same_value_for_any_two_temperatures_below_1000(self): - """Function should return same value for 999 Kelvin and 0 Kelvin.""" - rgb_1 = color_util.color_temperature_to_rgb(999) - rgb_2 = color_util.color_temperature_to_rgb(0) - assert rgb_1 == rgb_2 + assert color_util.color_rgbw_to_rgb(0, 0, 255, 0) == (0, 0, 255) - def test_returns_same_value_for_any_two_temperatures_above_40000(self): - """Function should return same value for 40001K and 999999K.""" - rgb_1 = color_util.color_temperature_to_rgb(40001) - rgb_2 = color_util.color_temperature_to_rgb(999999) - assert rgb_1 == rgb_2 + assert color_util.color_rgbw_to_rgb(255, 127, 0, 0) == (255, 127, 0) - def test_should_return_pure_white_at_6600(self): - """ - Function should return red=255, blue=255, green=255 when given 6600K. + assert color_util.color_rgbw_to_rgb(255, 0, 0, 253) == (255, 127, 127) - 6600K is considered "pure white" light. - This is just a rough estimate because the formula itself is a "best - guess" approach. - """ - rgb = color_util.color_temperature_to_rgb(6600) - assert (255, 255, 255) == rgb - - def test_color_above_6600_should_have_more_blue_than_red_or_green(self): - """Function should return a higher blue value for blue-ish light.""" - rgb = color_util.color_temperature_to_rgb(6700) - assert rgb[2] > rgb[1] - assert rgb[2] > rgb[0] - - def test_color_below_6600_should_have_more_red_than_blue_or_green(self): - """Function should return a higher red value for red-ish light.""" - rgb = color_util.color_temperature_to_rgb(6500) - assert rgb[0] > rgb[1] - assert rgb[0] > rgb[2] + assert color_util.color_rgbw_to_rgb(0, 0, 0, 127) == (127, 127, 127) + + +def test_color_rgb_to_hex(): + """Test color_rgb_to_hex.""" + assert color_util.color_rgb_to_hex(255, 255, 255) == 'ffffff' + assert color_util.color_rgb_to_hex(0, 0, 0) == '000000' + assert color_util.color_rgb_to_hex(51, 153, 255) == '3399ff' + assert color_util.color_rgb_to_hex(255, 67.9204190, 0) == 'ff4400' + + +def test_gamut(): + """Test gamut functions.""" + assert color_util.check_valid_gamut(GAMUT) + assert not color_util.check_valid_gamut(GAMUT_INVALID_1) + assert not color_util.check_valid_gamut(GAMUT_INVALID_2) + assert not color_util.check_valid_gamut(GAMUT_INVALID_3) + assert not color_util.check_valid_gamut(GAMUT_INVALID_4) + + +def test_should_return_25000_kelvin_when_input_is_40_mired(): + """Function should return 25000K if given 40 mired.""" + kelvin = color_util.color_temperature_mired_to_kelvin(40) + assert kelvin == 25000 + + +def test_should_return_5000_kelvin_when_input_is_200_mired(): + """Function should return 5000K if given 200 mired.""" + kelvin = color_util.color_temperature_mired_to_kelvin(200) + assert kelvin == 5000 + + +def test_should_return_40_mired_when_input_is_25000_kelvin(): + """Function should return 40 mired when given 25000 Kelvin.""" + mired = color_util.color_temperature_kelvin_to_mired(25000) + assert mired == 40 + + +def test_should_return_200_mired_when_input_is_5000_kelvin(): + """Function should return 200 mired when given 5000 Kelvin.""" + mired = color_util.color_temperature_kelvin_to_mired(5000) + assert mired == 200 + + +def test_returns_same_value_for_any_two_temperatures_below_1000(): + """Function should return same value for 999 Kelvin and 0 Kelvin.""" + rgb_1 = color_util.color_temperature_to_rgb(999) + rgb_2 = color_util.color_temperature_to_rgb(0) + assert rgb_1 == rgb_2 + + +def test_returns_same_value_for_any_two_temperatures_above_40000(): + """Function should return same value for 40001K and 999999K.""" + rgb_1 = color_util.color_temperature_to_rgb(40001) + rgb_2 = color_util.color_temperature_to_rgb(999999) + assert rgb_1 == rgb_2 + + +def test_should_return_pure_white_at_6600(): + """ + Function should return red=255, blue=255, green=255 when given 6600K. + + 6600K is considered "pure white" light. + This is just a rough estimate because the formula itself is a "best + guess" approach. + """ + rgb = color_util.color_temperature_to_rgb(6600) + assert (255, 255, 255) == rgb + + +def test_color_above_6600_should_have_more_blue_than_red_or_green(): + """Function should return a higher blue value for blue-ish light.""" + rgb = color_util.color_temperature_to_rgb(6700) + assert rgb[2] > rgb[1] + assert rgb[2] > rgb[0] + + +def test_color_below_6600_should_have_more_red_than_blue_or_green(): + """Function should return a higher red value for red-ish light.""" + rgb = color_util.color_temperature_to_rgb(6500) + assert rgb[0] > rgb[1] + assert rgb[0] > rgb[2] def test_get_color_in_voluptuous(): diff --git a/tests/util/test_distance.py b/tests/util/test_distance.py index 162f1a2fa99..691a3e47bf7 100644 --- a/tests/util/test_distance.py +++ b/tests/util/test_distance.py @@ -1,79 +1,68 @@ """Test homeassistant distance utility functions.""" -import unittest +import pytest + import homeassistant.util.distance as distance_util from homeassistant.const import (LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_FEET, LENGTH_MILES) -import pytest - INVALID_SYMBOL = 'bob' VALID_SYMBOL = LENGTH_KILOMETERS -class TestDistanceUtil(unittest.TestCase): - """Test the distance utility functions.""" +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert distance_util.convert(5, LENGTH_KILOMETERS, LENGTH_KILOMETERS) == 5 + assert distance_util.convert(2, LENGTH_METERS, LENGTH_METERS) == 2 + assert distance_util.convert(10, LENGTH_MILES, LENGTH_MILES) == 10 + assert distance_util.convert(9, LENGTH_FEET, LENGTH_FEET) == 9 - def test_convert_same_unit(self): - """Test conversion from any unit to same unit.""" - assert 5 == distance_util.convert(5, LENGTH_KILOMETERS, - LENGTH_KILOMETERS) - assert 2 == distance_util.convert(2, LENGTH_METERS, - LENGTH_METERS) - assert 10 == distance_util.convert(10, LENGTH_MILES, LENGTH_MILES) - assert 9 == distance_util.convert(9, LENGTH_FEET, LENGTH_FEET) - def test_convert_invalid_unit(self): - """Test exception is thrown for invalid units.""" - with pytest.raises(ValueError): - distance_util.convert(5, INVALID_SYMBOL, - VALID_SYMBOL) +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + distance_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) - with pytest.raises(ValueError): - distance_util.convert(5, VALID_SYMBOL, - INVALID_SYMBOL) + with pytest.raises(ValueError): + distance_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) - def test_convert_nonnumeric_value(self): - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - distance_util.convert('a', LENGTH_KILOMETERS, LENGTH_METERS) - def test_convert_from_miles(self): - """Test conversion from miles to other units.""" - miles = 5 - assert distance_util.convert( - miles, LENGTH_MILES, LENGTH_KILOMETERS - ) == 8.04672 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_METERS) == \ - 8046.72 - assert distance_util.convert(miles, LENGTH_MILES, LENGTH_FEET) == \ - 26400.0008448 +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + distance_util.convert('a', LENGTH_KILOMETERS, LENGTH_METERS) - def test_convert_from_feet(self): - """Test conversion from feet to other units.""" - feet = 5000 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_KILOMETERS) == \ - 1.524 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_METERS) == \ - 1524 - assert distance_util.convert(feet, LENGTH_FEET, LENGTH_MILES) == \ - 0.9469694040000001 - def test_convert_from_kilometers(self): - """Test conversion from kilometers to other units.""" - km = 5 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_FEET) == \ - 16404.2 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_METERS) == \ - 5000 - assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_MILES) == \ - 3.106855 +def test_convert_from_miles(): + """Test conversion from miles to other units.""" + miles = 5 + assert distance_util.convert(miles, LENGTH_MILES, LENGTH_KILOMETERS) == \ + 8.04672 + assert distance_util.convert(miles, LENGTH_MILES, LENGTH_METERS) == 8046.72 + assert distance_util.convert(miles, LENGTH_MILES, LENGTH_FEET) == \ + 26400.0008448 - def test_convert_from_meters(self): - """Test conversion from meters to other units.""" - m = 5000 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_FEET) == \ - 16404.2 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_KILOMETERS) == \ - 5 - assert distance_util.convert(m, LENGTH_METERS, LENGTH_MILES) == \ - 3.106855 + +def test_convert_from_feet(): + """Test conversion from feet to other units.""" + feet = 5000 + assert distance_util.convert(feet, LENGTH_FEET, LENGTH_KILOMETERS) == 1.524 + assert distance_util.convert(feet, LENGTH_FEET, LENGTH_METERS) == 1524 + assert distance_util.convert(feet, LENGTH_FEET, LENGTH_MILES) == \ + 0.9469694040000001 + + +def test_convert_from_kilometers(): + """Test conversion from kilometers to other units.""" + km = 5 + assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_FEET) == 16404.2 + assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_METERS) == 5000 + assert distance_util.convert(km, LENGTH_KILOMETERS, LENGTH_MILES) == \ + 3.106855 + + +def test_convert_from_meters(): + """Test conversion from meters to other units.""" + m = 5000 + assert distance_util.convert(m, LENGTH_METERS, LENGTH_FEET) == 16404.2 + assert distance_util.convert(m, LENGTH_METERS, LENGTH_KILOMETERS) == 5 + assert distance_util.convert(m, LENGTH_METERS, LENGTH_MILES) == 3.106855 diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 52f55fff345..61f10ab1bf6 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -1,253 +1,266 @@ """Test Home Assistant date util methods.""" -import unittest from datetime import datetime, timedelta -import homeassistant.util.dt as dt_util import pytest +import homeassistant.util.dt as dt_util + +DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE TEST_TIME_ZONE = 'America/Los_Angeles' -class TestDateUtil(unittest.TestCase): - """Test util date methods.""" +def teardown(): + """Stop everything that was started.""" + dt_util.set_default_time_zone(DEFAULT_TIME_ZONE) - def setUp(self): - """Set up the tests.""" - self.orig_default_time_zone = dt_util.DEFAULT_TIME_ZONE - def tearDown(self): - """Stop everything that was started.""" - dt_util.set_default_time_zone(self.orig_default_time_zone) +def test_get_time_zone_retrieves_valid_time_zone(): + """Test getting a time zone.""" + time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) - def test_get_time_zone_retrieves_valid_time_zone(self): - """Test getting a time zone.""" - time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) + assert time_zone is not None + assert TEST_TIME_ZONE == time_zone.zone - assert time_zone is not None - assert TEST_TIME_ZONE == time_zone.zone - def test_get_time_zone_returns_none_for_garbage_time_zone(self): - """Test getting a non existing time zone.""" - time_zone = dt_util.get_time_zone("Non existing time zone") +def test_get_time_zone_returns_none_for_garbage_time_zone(): + """Test getting a non existing time zone.""" + time_zone = dt_util.get_time_zone("Non existing time zone") - assert time_zone is None + assert time_zone is None - def test_set_default_time_zone(self): - """Test setting default time zone.""" - time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) - dt_util.set_default_time_zone(time_zone) +def test_set_default_time_zone(): + """Test setting default time zone.""" + time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) - # We cannot compare the timezones directly because of DST - assert time_zone.zone == dt_util.now().tzinfo.zone + dt_util.set_default_time_zone(time_zone) - def test_utcnow(self): - """Test the UTC now method.""" - assert abs(dt_util.utcnow().replace(tzinfo=None)-datetime.utcnow()) < \ - timedelta(seconds=1) + # We cannot compare the timezones directly because of DST + assert time_zone.zone == dt_util.now().tzinfo.zone - def test_now(self): - """Test the now method.""" - dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) - assert abs( - dt_util.as_utc(dt_util.now()).replace( - tzinfo=None - ) - datetime.utcnow() - ) < timedelta(seconds=1) +def test_utcnow(): + """Test the UTC now method.""" + assert abs(dt_util.utcnow().replace(tzinfo=None)-datetime.utcnow()) < \ + timedelta(seconds=1) - def test_as_utc_with_naive_object(self): - """Test the now method.""" - utcnow = datetime.utcnow() - assert utcnow == dt_util.as_utc(utcnow).replace(tzinfo=None) +def test_now(): + """Test the now method.""" + dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) - def test_as_utc_with_utc_object(self): - """Test UTC time with UTC object.""" - utcnow = dt_util.utcnow() + assert abs( + dt_util.as_utc(dt_util.now()).replace( + tzinfo=None + ) - datetime.utcnow() + ) < timedelta(seconds=1) - assert utcnow == dt_util.as_utc(utcnow) - def test_as_utc_with_local_object(self): - """Test the UTC time with local object.""" - dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) - localnow = dt_util.now() - utcnow = dt_util.as_utc(localnow) +def test_as_utc_with_naive_object(): + """Test the now method.""" + utcnow = datetime.utcnow() - assert localnow == utcnow - assert localnow.tzinfo != utcnow.tzinfo + assert utcnow == dt_util.as_utc(utcnow).replace(tzinfo=None) - def test_as_local_with_naive_object(self): - """Test local time with native object.""" - now = dt_util.now() - assert abs(now-dt_util.as_local(datetime.utcnow())) < \ - timedelta(seconds=1) - def test_as_local_with_local_object(self): - """Test local with local object.""" - now = dt_util.now() - assert now == now +def test_as_utc_with_utc_object(): + """Test UTC time with UTC object.""" + utcnow = dt_util.utcnow() - def test_as_local_with_utc_object(self): - """Test local time with UTC object.""" - dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) + assert utcnow == dt_util.as_utc(utcnow) - utcnow = dt_util.utcnow() - localnow = dt_util.as_local(utcnow) - assert localnow == utcnow - assert localnow.tzinfo != utcnow.tzinfo +def test_as_utc_with_local_object(): + """Test the UTC time with local object.""" + dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) + localnow = dt_util.now() + utcnow = dt_util.as_utc(localnow) - def test_utc_from_timestamp(self): - """Test utc_from_timestamp method.""" - assert datetime(1986, 7, 9, tzinfo=dt_util.UTC) == \ - dt_util.utc_from_timestamp(521251200) + assert localnow == utcnow + assert localnow.tzinfo != utcnow.tzinfo - def test_as_timestamp(self): - """Test as_timestamp method.""" - ts = 1462401234 - utc_dt = dt_util.utc_from_timestamp(ts) - assert ts == dt_util.as_timestamp(utc_dt) - utc_iso = utc_dt.isoformat() - assert ts == dt_util.as_timestamp(utc_iso) - # confirm the ability to handle a string passed in - delta = dt_util.as_timestamp("2016-01-01 12:12:12") - delta -= dt_util.as_timestamp("2016-01-01 12:12:11") - assert 1 == delta +def test_as_local_with_naive_object(): + """Test local time with native object.""" + now = dt_util.now() + assert abs(now-dt_util.as_local(datetime.utcnow())) < \ + timedelta(seconds=1) - def test_parse_datetime_converts_correctly(self): - """Test parse_datetime converts strings.""" - assert \ - datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) == \ - dt_util.parse_datetime("1986-07-09T12:00:00Z") - utcnow = dt_util.utcnow() +def test_as_local_with_local_object(): + """Test local with local object.""" + now = dt_util.now() + assert now == now - assert utcnow == dt_util.parse_datetime(utcnow.isoformat()) - def test_parse_datetime_returns_none_for_incorrect_format(self): - """Test parse_datetime returns None if incorrect format.""" - assert dt_util.parse_datetime("not a datetime string") is None +def test_as_local_with_utc_object(): + """Test local time with UTC object.""" + dt_util.set_default_time_zone(dt_util.get_time_zone(TEST_TIME_ZONE)) - def test_get_age(self): - """Test get_age.""" - diff = dt_util.now() - timedelta(seconds=0) - assert dt_util.get_age(diff) == "0 seconds" + utcnow = dt_util.utcnow() + localnow = dt_util.as_local(utcnow) - diff = dt_util.now() - timedelta(seconds=1) - assert dt_util.get_age(diff) == "1 second" + assert localnow == utcnow + assert localnow.tzinfo != utcnow.tzinfo - diff = dt_util.now() - timedelta(seconds=30) - assert dt_util.get_age(diff) == "30 seconds" - diff = dt_util.now() - timedelta(minutes=5) - assert dt_util.get_age(diff) == "5 minutes" +def test_utc_from_timestamp(): + """Test utc_from_timestamp method.""" + assert datetime(1986, 7, 9, tzinfo=dt_util.UTC) == \ + dt_util.utc_from_timestamp(521251200) - diff = dt_util.now() - timedelta(minutes=1) - assert dt_util.get_age(diff) == "1 minute" - diff = dt_util.now() - timedelta(minutes=300) - assert dt_util.get_age(diff) == "5 hours" +def test_as_timestamp(): + """Test as_timestamp method.""" + ts = 1462401234 + utc_dt = dt_util.utc_from_timestamp(ts) + assert ts == dt_util.as_timestamp(utc_dt) + utc_iso = utc_dt.isoformat() + assert ts == dt_util.as_timestamp(utc_iso) - diff = dt_util.now() - timedelta(minutes=320) - assert dt_util.get_age(diff) == "5 hours" + # confirm the ability to handle a string passed in + delta = dt_util.as_timestamp("2016-01-01 12:12:12") + delta -= dt_util.as_timestamp("2016-01-01 12:12:11") + assert delta == 1 - diff = dt_util.now() - timedelta(minutes=2*60*24) - assert dt_util.get_age(diff) == "2 days" - diff = dt_util.now() - timedelta(minutes=32*60*24) - assert dt_util.get_age(diff) == "1 month" +def test_parse_datetime_converts_correctly(): + """Test parse_datetime converts strings.""" + assert \ + datetime(1986, 7, 9, 12, 0, 0, tzinfo=dt_util.UTC) == \ + dt_util.parse_datetime("1986-07-09T12:00:00Z") - diff = dt_util.now() - timedelta(minutes=365*60*24) - assert dt_util.get_age(diff) == "1 year" + utcnow = dt_util.utcnow() - def test_parse_time_expression(self): - """Test parse_time_expression.""" - assert [x for x in range(60)] == \ - dt_util.parse_time_expression('*', 0, 59) - assert [x for x in range(60)] == \ - dt_util.parse_time_expression(None, 0, 59) + assert utcnow == dt_util.parse_datetime(utcnow.isoformat()) - assert [x for x in range(0, 60, 5)] == \ - dt_util.parse_time_expression('/5', 0, 59) - assert [1, 2, 3] == \ - dt_util.parse_time_expression([2, 1, 3], 0, 59) +def test_parse_datetime_returns_none_for_incorrect_format(): + """Test parse_datetime returns None if incorrect format.""" + assert dt_util.parse_datetime("not a datetime string") is None - assert [x for x in range(24)] == \ - dt_util.parse_time_expression('*', 0, 23) - assert [42] == \ - dt_util.parse_time_expression(42, 0, 59) +def test_get_age(): + """Test get_age.""" + diff = dt_util.now() - timedelta(seconds=0) + assert dt_util.get_age(diff) == "0 seconds" - with pytest.raises(ValueError): - dt_util.parse_time_expression(61, 0, 60) + diff = dt_util.now() - timedelta(seconds=1) + assert dt_util.get_age(diff) == "1 second" - def test_find_next_time_expression_time_basic(self): - """Test basic stuff for find_next_time_expression_time.""" - def find(dt, hour, minute, second): - """Call test_find_next_time_expression_time.""" - seconds = dt_util.parse_time_expression(second, 0, 59) - minutes = dt_util.parse_time_expression(minute, 0, 59) - hours = dt_util.parse_time_expression(hour, 0, 23) + diff = dt_util.now() - timedelta(seconds=30) + assert dt_util.get_age(diff) == "30 seconds" - return dt_util.find_next_time_expression_time( - dt, seconds, minutes, hours) + diff = dt_util.now() - timedelta(minutes=5) + assert dt_util.get_age(diff) == "5 minutes" - assert datetime(2018, 10, 7, 10, 30, 0) == \ - find(datetime(2018, 10, 7, 10, 20, 0), '*', '/30', 0) + diff = dt_util.now() - timedelta(minutes=1) + assert dt_util.get_age(diff) == "1 minute" - assert datetime(2018, 10, 7, 10, 30, 0) == \ - find(datetime(2018, 10, 7, 10, 30, 0), '*', '/30', 0) + diff = dt_util.now() - timedelta(minutes=300) + assert dt_util.get_age(diff) == "5 hours" - assert datetime(2018, 10, 7, 12, 30, 30) == \ - find(datetime(2018, 10, 7, 10, 30, 0), '/3', '/30', [30, 45]) + diff = dt_util.now() - timedelta(minutes=320) + assert dt_util.get_age(diff) == "5 hours" - assert datetime(2018, 10, 8, 5, 0, 0) == \ - find(datetime(2018, 10, 7, 10, 30, 0), 5, 0, 0) + diff = dt_util.now() - timedelta(minutes=2*60*24) + assert dt_util.get_age(diff) == "2 days" - def test_find_next_time_expression_time_dst(self): - """Test daylight saving time for find_next_time_expression_time.""" - tz = dt_util.get_time_zone('Europe/Vienna') - dt_util.set_default_time_zone(tz) + diff = dt_util.now() - timedelta(minutes=32*60*24) + assert dt_util.get_age(diff) == "1 month" - def find(dt, hour, minute, second): - """Call test_find_next_time_expression_time.""" - seconds = dt_util.parse_time_expression(second, 0, 59) - minutes = dt_util.parse_time_expression(minute, 0, 59) - hours = dt_util.parse_time_expression(hour, 0, 23) + diff = dt_util.now() - timedelta(minutes=365*60*24) + assert dt_util.get_age(diff) == "1 year" - return dt_util.find_next_time_expression_time( - dt, seconds, minutes, hours) - # Entering DST, clocks are rolled forward - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == \ - find(tz.localize(datetime(2018, 3, 25, 1, 50, 0)), 2, 30, 0) +def test_parse_time_expression(): + """Test parse_time_expression.""" + assert [x for x in range(60)] == \ + dt_util.parse_time_expression('*', 0, 59) + assert [x for x in range(60)] == \ + dt_util.parse_time_expression(None, 0, 59) - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == \ - find(tz.localize(datetime(2018, 3, 25, 3, 50, 0)), 2, 30, 0) + assert [x for x in range(0, 60, 5)] == \ + dt_util.parse_time_expression('/5', 0, 59) - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == \ - find(tz.localize(datetime(2018, 3, 26, 1, 50, 0)), 2, 30, 0) + assert [1, 2, 3] == \ + dt_util.parse_time_expression([2, 1, 3], 0, 59) - # Leaving DST, clocks are rolled back - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == \ - find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False), - 2, 30, 0) + assert [x for x in range(24)] == \ + dt_util.parse_time_expression('*', 0, 23) - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == \ - find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), - 2, 30, 0) + assert [42] == \ + dt_util.parse_time_expression(42, 0, 59) - assert tz.localize(datetime(2018, 10, 28, 4, 30, 0), is_dst=False) == \ - find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), - 4, 30, 0) + with pytest.raises(ValueError): + dt_util.parse_time_expression(61, 0, 60) - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=True) == \ - find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True), - 2, 30, 0) - assert tz.localize(datetime(2018, 10, 29, 2, 30, 0)) == \ - find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False), - 2, 30, 0) +def test_find_next_time_expression_time_basic(): + """Test basic stuff for find_next_time_expression_time.""" + def find(dt, hour, minute, second): + """Call test_find_next_time_expression_time.""" + seconds = dt_util.parse_time_expression(second, 0, 59) + minutes = dt_util.parse_time_expression(minute, 0, 59) + hours = dt_util.parse_time_expression(hour, 0, 23) + + return dt_util.find_next_time_expression_time( + dt, seconds, minutes, hours) + + assert datetime(2018, 10, 7, 10, 30, 0) == \ + find(datetime(2018, 10, 7, 10, 20, 0), '*', '/30', 0) + + assert datetime(2018, 10, 7, 10, 30, 0) == \ + find(datetime(2018, 10, 7, 10, 30, 0), '*', '/30', 0) + + assert datetime(2018, 10, 7, 12, 30, 30) == \ + find(datetime(2018, 10, 7, 10, 30, 0), '/3', '/30', [30, 45]) + + assert datetime(2018, 10, 8, 5, 0, 0) == \ + find(datetime(2018, 10, 7, 10, 30, 0), 5, 0, 0) + + +def test_find_next_time_expression_time_dst(): + """Test daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone('Europe/Vienna') + dt_util.set_default_time_zone(tz) + + def find(dt, hour, minute, second): + """Call test_find_next_time_expression_time.""" + seconds = dt_util.parse_time_expression(second, 0, 59) + minutes = dt_util.parse_time_expression(minute, 0, 59) + hours = dt_util.parse_time_expression(hour, 0, 23) + + return dt_util.find_next_time_expression_time( + dt, seconds, minutes, hours) + + # Entering DST, clocks are rolled forward + assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == \ + find(tz.localize(datetime(2018, 3, 25, 1, 50, 0)), 2, 30, 0) + + assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == \ + find(tz.localize(datetime(2018, 3, 25, 3, 50, 0)), 2, 30, 0) + + assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == \ + find(tz.localize(datetime(2018, 3, 26, 1, 50, 0)), 2, 30, 0) + + # Leaving DST, clocks are rolled back + assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == \ + find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False), + 2, 30, 0) + + assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == \ + find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), + 2, 30, 0) + + assert tz.localize(datetime(2018, 10, 28, 4, 30, 0), is_dst=False) == \ + find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), + 4, 30, 0) + + assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=True) == \ + find(tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True), + 2, 30, 0) + + assert tz.localize(datetime(2018, 10, 29, 2, 30, 0)) == \ + find(tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False), + 2, 30, 0) diff --git a/tests/util/test_init.py b/tests/util/test_init.py index af957582ec0..42b53cea2d6 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,223 +1,228 @@ """Test Home Assistant util methods.""" -import unittest from unittest.mock import patch, MagicMock from datetime import datetime, timedelta -from homeassistant import util -import homeassistant.util.dt as dt_util import pytest +from homeassistant import util +import homeassistant.util.dt as dt_util -class TestUtil(unittest.TestCase): - """Test util methods.""" - def test_sanitize_filename(self): - """Test sanitize_filename.""" - assert "test" == util.sanitize_filename("test") - assert "test" == util.sanitize_filename("/test") - assert "test" == util.sanitize_filename("..test") - assert "test" == util.sanitize_filename("\\test") - assert "test" == util.sanitize_filename("\\../test") +def test_sanitize_filename(): + """Test sanitize_filename.""" + assert util.sanitize_filename("test") == 'test' + assert util.sanitize_filename("/test") == 'test' + assert util.sanitize_filename("..test") == 'test' + assert util.sanitize_filename("\\test") == 'test' + assert util.sanitize_filename("\\../test") == 'test' - def test_sanitize_path(self): - """Test sanitize_path.""" - assert "test/path" == util.sanitize_path("test/path") - assert "test/path" == util.sanitize_path("~test/path") - assert "//test/path" == util.sanitize_path("~/../test/path") - def test_slugify(self): - """Test slugify.""" - assert "t_est" == util.slugify("T-!@#$!#@$!$est") - assert "test_more" == util.slugify("Test More") - assert "test_more" == util.slugify("Test_(More)") - assert "test_more" == util.slugify("Tèst_Mörê") - assert "b8_27_eb_00_00_00" == util.slugify("B8:27:EB:00:00:00") - assert "test_com" == util.slugify("test.com") - assert "greg_phone_exp_wayp1" == \ - util.slugify("greg_phone - exp_wayp1") - assert "we_are_we_are_a_test_calendar" == \ - util.slugify("We are, we are, a... Test Calendar") - assert "test_aouss_aou" == util.slugify("Tèst_äöüß_ÄÖÜ") - assert "ying_shi_ma" == util.slugify("影師嗎") - assert "keihuonto" == util.slugify("けいふぉんと") +def test_sanitize_path(): + """Test sanitize_path.""" + assert util.sanitize_path("test/path") == 'test/path' + assert util.sanitize_path("~test/path") == 'test/path' + assert util.sanitize_path("~/../test/path") == '//test/path' - def test_repr_helper(self): - """Test repr_helper.""" - assert "A" == util.repr_helper("A") - assert "5" == util.repr_helper(5) - assert "True" == util.repr_helper(True) - assert "test=1" == util.repr_helper({"test": 1}) - assert "1986-07-09T12:00:00+00:00" == \ - util.repr_helper(datetime(1986, 7, 9, 12, 0, 0)) - def test_convert(self): - """Test convert.""" - assert 5 == util.convert("5", int) - assert 5.0 == util.convert("5", float) - assert util.convert("True", bool) is True - assert 1 == util.convert("NOT A NUMBER", int, 1) - assert 1 == util.convert(None, int, 1) - assert 1 == util.convert(object, int, 1) +def test_slugify(): + """Test slugify.""" + assert util.slugify("T-!@#$!#@$!$est") == 't_est' + assert util.slugify("Test More") == 'test_more' + assert util.slugify("Test_(More)") == 'test_more' + assert util.slugify("Tèst_Mörê") == 'test_more' + assert util.slugify("B8:27:EB:00:00:00") == 'b8_27_eb_00_00_00' + assert util.slugify("test.com") == 'test_com' + assert util.slugify("greg_phone - exp_wayp1") == 'greg_phone_exp_wayp1' + assert util.slugify("We are, we are, a... Test Calendar") == \ + 'we_are_we_are_a_test_calendar' + assert util.slugify("Tèst_äöüß_ÄÖÜ") == 'test_aouss_aou' + assert util.slugify("影師嗎") == 'ying_shi_ma' + assert util.slugify("けいふぉんと") == 'keihuonto' - def test_ensure_unique_string(self): - """Test ensure_unique_string.""" - assert "Beer_3" == \ - util.ensure_unique_string("Beer", ["Beer", "Beer_2"]) - assert "Beer" == \ - util.ensure_unique_string("Beer", ["Wine", "Soda"]) - def test_ordered_enum(self): - """Test the ordered enum class.""" - class TestEnum(util.OrderedEnum): - """Test enum that can be ordered.""" +def test_repr_helper(): + """Test repr_helper.""" + assert util.repr_helper("A") == 'A' + assert util.repr_helper(5) == '5' + assert util.repr_helper(True) == 'True' + assert util.repr_helper({"test": 1}) == 'test=1' + assert util.repr_helper(datetime(1986, 7, 9, 12, 0, 0)) == \ + '1986-07-09T12:00:00+00:00' - FIRST = 1 - SECOND = 2 - THIRD = 3 - assert TestEnum.SECOND >= TestEnum.FIRST - assert TestEnum.SECOND >= TestEnum.SECOND - assert not (TestEnum.SECOND >= TestEnum.THIRD) +def test_convert(): + """Test convert.""" + assert util.convert("5", int) == 5 + assert util.convert("5", float) == 5.0 + assert util.convert("True", bool) is True + assert util.convert("NOT A NUMBER", int, 1) == 1 + assert util.convert(None, int, 1) == 1 + assert util.convert(object, int, 1) == 1 - assert TestEnum.SECOND > TestEnum.FIRST - assert not (TestEnum.SECOND > TestEnum.SECOND) - assert not (TestEnum.SECOND > TestEnum.THIRD) - assert not (TestEnum.SECOND <= TestEnum.FIRST) - assert TestEnum.SECOND <= TestEnum.SECOND - assert TestEnum.SECOND <= TestEnum.THIRD +def test_ensure_unique_string(): + """Test ensure_unique_string.""" + assert util.ensure_unique_string("Beer", ["Beer", "Beer_2"]) == 'Beer_3' + assert util.ensure_unique_string("Beer", ["Wine", "Soda"]) == 'Beer' - assert not (TestEnum.SECOND < TestEnum.FIRST) - assert not (TestEnum.SECOND < TestEnum.SECOND) - assert TestEnum.SECOND < TestEnum.THIRD - # Python will raise a TypeError if the <, <=, >, >= methods - # raise a NotImplemented error. - with pytest.raises(TypeError): - TestEnum.FIRST < 1 +def test_ordered_enum(): + """Test the ordered enum class.""" + class TestEnum(util.OrderedEnum): + """Test enum that can be ordered.""" - with pytest.raises(TypeError): - TestEnum.FIRST <= 1 + FIRST = 1 + SECOND = 2 + THIRD = 3 - with pytest.raises(TypeError): - TestEnum.FIRST > 1 + assert TestEnum.SECOND >= TestEnum.FIRST + assert TestEnum.SECOND >= TestEnum.SECOND + assert TestEnum.SECOND < TestEnum.THIRD - with pytest.raises(TypeError): - TestEnum.FIRST >= 1 + assert TestEnum.SECOND > TestEnum.FIRST + assert TestEnum.SECOND <= TestEnum.SECOND + assert TestEnum.SECOND <= TestEnum.THIRD - def test_throttle(self): - """Test the add cooldown decorator.""" - calls1 = [] - calls2 = [] + assert TestEnum.SECOND > TestEnum.FIRST + assert TestEnum.SECOND <= TestEnum.SECOND + assert TestEnum.SECOND <= TestEnum.THIRD - @util.Throttle(timedelta(seconds=4)) - def test_throttle1(): - calls1.append(1) + assert TestEnum.SECOND >= TestEnum.FIRST + assert TestEnum.SECOND >= TestEnum.SECOND + assert TestEnum.SECOND < TestEnum.THIRD - @util.Throttle(timedelta(seconds=4), timedelta(seconds=2)) - def test_throttle2(): - calls2.append(1) + # Python will raise a TypeError if the <, <=, >, >= methods + # raise a NotImplemented error. + with pytest.raises(TypeError): + TestEnum.FIRST < 1 - now = dt_util.utcnow() - plus3 = now + timedelta(seconds=3) - plus5 = plus3 + timedelta(seconds=2) + with pytest.raises(TypeError): + TestEnum.FIRST <= 1 - # Call first time and ensure methods got called + with pytest.raises(TypeError): + TestEnum.FIRST > 1 + + with pytest.raises(TypeError): + TestEnum.FIRST >= 1 + + +def test_throttle(): + """Test the add cooldown decorator.""" + calls1 = [] + calls2 = [] + + @util.Throttle(timedelta(seconds=4)) + def test_throttle1(): + calls1.append(1) + + @util.Throttle(timedelta(seconds=4), timedelta(seconds=2)) + def test_throttle2(): + calls2.append(1) + + now = dt_util.utcnow() + plus3 = now + timedelta(seconds=3) + plus5 = plus3 + timedelta(seconds=2) + + # Call first time and ensure methods got called + test_throttle1() + test_throttle2() + + assert len(calls1) == 1 + assert len(calls2) == 1 + + # Call second time. Methods should not get called + test_throttle1() + test_throttle2() + + assert len(calls1) == 1 + assert len(calls2) == 1 + + # Call again, overriding throttle, only first one should fire + test_throttle1(no_throttle=True) + test_throttle2(no_throttle=True) + + assert len(calls1) == 2 + assert len(calls2) == 1 + + with patch('homeassistant.util.utcnow', return_value=plus3): test_throttle1() test_throttle2() - assert 1 == len(calls1) - assert 1 == len(calls2) + assert len(calls1) == 2 + assert len(calls2) == 1 - # Call second time. Methods should not get called + with patch('homeassistant.util.utcnow', return_value=plus5): test_throttle1() test_throttle2() - assert 1 == len(calls1) - assert 1 == len(calls2) + assert len(calls1) == 3 + assert len(calls2) == 2 - # Call again, overriding throttle, only first one should fire - test_throttle1(no_throttle=True) - test_throttle2(no_throttle=True) - assert 2 == len(calls1) - assert 1 == len(calls2) +def test_throttle_per_instance(): + """Test that the throttle method is done per instance of a class.""" + class Tester: + """A tester class for the throttle.""" - with patch('homeassistant.util.utcnow', return_value=plus3): - test_throttle1() - test_throttle2() + @util.Throttle(timedelta(seconds=1)) + def hello(self): + """Test the throttle.""" + return True - assert 2 == len(calls1) - assert 1 == len(calls2) + assert Tester().hello() + assert Tester().hello() - with patch('homeassistant.util.utcnow', return_value=plus5): - test_throttle1() - test_throttle2() - assert 3 == len(calls1) - assert 2 == len(calls2) +def test_throttle_on_method(): + """Test that throttle works when wrapping a method.""" + class Tester: + """A tester class for the throttle.""" - def test_throttle_per_instance(self): - """Test that the throttle method is done per instance of a class.""" - class Tester: - """A tester class for the throttle.""" + def hello(self): + """Test the throttle.""" + return True - @util.Throttle(timedelta(seconds=1)) - def hello(self): - """Test the throttle.""" - return True + tester = Tester() + throttled = util.Throttle(timedelta(seconds=1))(tester.hello) - assert Tester().hello() - assert Tester().hello() + assert throttled() + assert throttled() is None - def test_throttle_on_method(self): - """Test that throttle works when wrapping a method.""" - class Tester: - """A tester class for the throttle.""" - def hello(self): - """Test the throttle.""" - return True +def test_throttle_on_two_method(): + """Test that throttle works when wrapping two methods.""" + class Tester: + """A test class for the throttle.""" - tester = Tester() - throttled = util.Throttle(timedelta(seconds=1))(tester.hello) + @util.Throttle(timedelta(seconds=1)) + def hello(self): + """Test the throttle.""" + return True - assert throttled() - assert throttled() is None + @util.Throttle(timedelta(seconds=1)) + def goodbye(self): + """Test the throttle.""" + return True - def test_throttle_on_two_method(self): - """Test that throttle works when wrapping two methods.""" - class Tester: - """A test class for the throttle.""" + tester = Tester() - @util.Throttle(timedelta(seconds=1)) - def hello(self): - """Test the throttle.""" - return True + assert tester.hello() + assert tester.goodbye() - @util.Throttle(timedelta(seconds=1)) - def goodbye(self): - """Test the throttle.""" - return True - tester = Tester() +@patch.object(util, 'random') +def test_get_random_string(mock_random): + """Test get random string.""" + results = ['A', 'B', 'C'] - assert tester.hello() - assert tester.goodbye() + def mock_choice(choices): + return results.pop(0) - @patch.object(util, 'random') - def test_get_random_string(self, mock_random): - """Test get random string.""" - results = ['A', 'B', 'C'] + generator = MagicMock() + generator.choice.side_effect = mock_choice + mock_random.SystemRandom.return_value = generator - def mock_choice(choices): - return results.pop(0) - - generator = MagicMock() - generator.choice.side_effect = mock_choice - mock_random.SystemRandom.return_value = generator - - assert util.get_random_string(length=3) == 'ABC' + assert util.get_random_string(length=3) == 'ABC' async def test_throttle_async(): diff --git a/tests/util/test_json.py b/tests/util/test_json.py index a7df74d9225..79e4613a2b4 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -2,15 +2,16 @@ from json import JSONEncoder import os import unittest +from unittest.mock import Mock import sys from tempfile import mkdtemp +import pytest + from homeassistant.util.json import ( SerializationError, load_json, save_json) from homeassistant.exceptions import HomeAssistantError -import pytest -from unittest.mock import Mock # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} @@ -19,75 +20,82 @@ TEST_JSON_B = {"a": "one", "B": 2} TEST_BAD_OBJECT = {("A",): 1} # Test data that can not be loaded as JSON TEST_BAD_SERIALIED = "THIS IS NOT JSON\n" +TMP_DIR = None -class TestJSON(unittest.TestCase): - """Test util.json save and load.""" +def setup(): + """Set up for tests.""" + global TMP_DIR + TMP_DIR = mkdtemp() - def setUp(self): - """Set up for tests.""" - self.tmp_dir = mkdtemp() - def tearDown(self): - """Clean up after tests.""" - for fname in os.listdir(self.tmp_dir): - os.remove(os.path.join(self.tmp_dir, fname)) - os.rmdir(self.tmp_dir) +def teardown(): + """Clean up after tests.""" + for fname in os.listdir(TMP_DIR): + os.remove(os.path.join(TMP_DIR, fname)) + os.rmdir(TMP_DIR) - def _path_for(self, leaf_name): - return os.path.join(self.tmp_dir, leaf_name+".json") - def test_save_and_load(self): - """Test saving and loading back.""" - fname = self._path_for("test1") - save_json(fname, TEST_JSON_A) - data = load_json(fname) - assert data == TEST_JSON_A +def _path_for(leaf_name): + return os.path.join(TMP_DIR, leaf_name+".json") - # Skipped on Windows - @unittest.skipIf(sys.platform.startswith('win'), - "private permissions not supported on Windows") - def test_save_and_load_private(self): - """Test we can load private files and that they are protected.""" - fname = self._path_for("test2") - save_json(fname, TEST_JSON_A, private=True) - data = load_json(fname) - assert data == TEST_JSON_A - stats = os.stat(fname) - assert stats.st_mode & 0o77 == 0 - def test_overwrite_and_reload(self): - """Test that we can overwrite an existing file and read back.""" - fname = self._path_for("test3") - save_json(fname, TEST_JSON_A) - save_json(fname, TEST_JSON_B) - data = load_json(fname) - assert data == TEST_JSON_B +def test_save_and_load(): + """Test saving and loading back.""" + fname = _path_for("test1") + save_json(fname, TEST_JSON_A) + data = load_json(fname) + assert data == TEST_JSON_A - def test_save_bad_data(self): - """Test error from trying to save unserialisable data.""" - fname = self._path_for("test4") - with pytest.raises(SerializationError): - save_json(fname, TEST_BAD_OBJECT) - def test_load_bad_data(self): - """Test error from trying to load unserialisable data.""" - fname = self._path_for("test5") - with open(fname, "w") as fh: - fh.write(TEST_BAD_SERIALIED) - with pytest.raises(HomeAssistantError): - load_json(fname) +# Skipped on Windows +@unittest.skipIf(sys.platform.startswith('win'), + "private permissions not supported on Windows") +def test_save_and_load_private(): + """Test we can load private files and that they are protected.""" + fname = _path_for("test2") + save_json(fname, TEST_JSON_A, private=True) + data = load_json(fname) + assert data == TEST_JSON_A + stats = os.stat(fname) + assert stats.st_mode & 0o77 == 0 - def test_custom_encoder(self): - """Test serializing with a custom encoder.""" - class MockJSONEncoder(JSONEncoder): - """Mock JSON encoder.""" - def default(self, o): - """Mock JSON encode method.""" - return "9" +def test_overwrite_and_reload(): + """Test that we can overwrite an existing file and read back.""" + fname = _path_for("test3") + save_json(fname, TEST_JSON_A) + save_json(fname, TEST_JSON_B) + data = load_json(fname) + assert data == TEST_JSON_B - fname = self._path_for("test6") - save_json(fname, Mock(), encoder=MockJSONEncoder) - data = load_json(fname) - self.assertEqual(data, "9") + +def test_save_bad_data(): + """Test error from trying to save unserialisable data.""" + fname = _path_for("test4") + with pytest.raises(SerializationError): + save_json(fname, TEST_BAD_OBJECT) + + +def test_load_bad_data(): + """Test error from trying to load unserialisable data.""" + fname = _path_for("test5") + with open(fname, "w") as fh: + fh.write(TEST_BAD_SERIALIED) + with pytest.raises(HomeAssistantError): + load_json(fname) + + +def test_custom_encoder(): + """Test serializing with a custom encoder.""" + class MockJSONEncoder(JSONEncoder): + """Mock JSON encoder.""" + + def default(self, o): + """Mock JSON encode method.""" + return "9" + + fname = _path_for("test6") + save_json(fname, Mock(), encoder=MockJSONEncoder) + data = load_json(fname) + assert data == "9" diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py index a3e6efb3754..245000761ad 100644 --- a/tests/util/test_pressure.py +++ b/tests/util/test_pressure.py @@ -1,5 +1,4 @@ """Test homeassistant pressure utility functions.""" -import unittest import pytest from homeassistant.const import (PRESSURE_PA, PRESSURE_HPA, PRESSURE_MBAR, @@ -10,57 +9,50 @@ INVALID_SYMBOL = 'bob' VALID_SYMBOL = PRESSURE_PA -class TestPressureUtil(unittest.TestCase): - """Test the pressure utility functions.""" +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert pressure_util.convert(2, PRESSURE_PA, PRESSURE_PA) == 2 + assert pressure_util.convert(3, PRESSURE_HPA, PRESSURE_HPA) == 3 + assert pressure_util.convert(4, PRESSURE_MBAR, PRESSURE_MBAR) == 4 + assert pressure_util.convert(5, PRESSURE_INHG, PRESSURE_INHG) == 5 - def test_convert_same_unit(self): - """Test conversion from any unit to same unit.""" - assert pressure_util.convert(2, PRESSURE_PA, PRESSURE_PA) == 2 - assert pressure_util.convert(3, PRESSURE_HPA, PRESSURE_HPA) == 3 - assert pressure_util.convert(4, PRESSURE_MBAR, PRESSURE_MBAR) == 4 - assert pressure_util.convert(5, PRESSURE_INHG, PRESSURE_INHG) == 5 - def test_convert_invalid_unit(self): - """Test exception is thrown for invalid units.""" - with pytest.raises(ValueError): - pressure_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + pressure_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) - with pytest.raises(ValueError): - pressure_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + with pytest.raises(ValueError): + pressure_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) - def test_convert_nonnumeric_value(self): - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - pressure_util.convert('a', PRESSURE_HPA, PRESSURE_INHG) - def test_convert_from_hpascals(self): - """Test conversion from hPA to other units.""" - hpascals = 1000 - self.assertAlmostEqual( - pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PSI), - 14.5037743897) - self.assertAlmostEqual( - pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_INHG), - 29.5299801647) - self.assertAlmostEqual( - pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PA), - 100000) - self.assertAlmostEqual( - pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_MBAR), - 1000) +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + pressure_util.convert('a', PRESSURE_HPA, PRESSURE_INHG) - def test_convert_from_inhg(self): - """Test conversion from inHg to other units.""" - inhg = 30 - self.assertAlmostEqual( - pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PSI), - 14.7346266155) - self.assertAlmostEqual( - pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_HPA), - 1015.9167) - self.assertAlmostEqual( - pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PA), - 101591.67) - self.assertAlmostEqual( - pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_MBAR), - 1015.9167) + +def test_convert_from_hpascals(): + """Test conversion from hPA to other units.""" + hpascals = 1000 + assert pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PSI) == \ + pytest.approx(14.5037743897) + assert pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_INHG) == \ + pytest.approx(29.5299801647) + assert pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PA) == \ + pytest.approx(100000) + assert pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_MBAR) == \ + pytest.approx(1000) + + +def test_convert_from_inhg(): + """Test conversion from inHg to other units.""" + inhg = 30 + assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PSI) == \ + pytest.approx(14.7346266155) + assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_HPA) == \ + pytest.approx(1015.9167) + assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PA) == \ + pytest.approx(101591.67) + assert pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_MBAR) == \ + pytest.approx(1015.9167) diff --git a/tests/util/test_ruamel_yaml.py b/tests/util/test_ruamel_yaml.py index 4ac8b85ae98..907cebc7f14 100644 --- a/tests/util/test_ruamel_yaml.py +++ b/tests/util/test_ruamel_yaml.py @@ -1,6 +1,5 @@ """Test Home Assistant ruamel.yaml loader.""" import os -import unittest from tempfile import mkdtemp import pytest @@ -114,45 +113,51 @@ views: cards: !include cards.yaml """ +TMP_DIR = None -class TestYAML(unittest.TestCase): - """Test lovelace.yaml save and load.""" - def setUp(self): - """Set up for tests.""" - self.tmp_dir = mkdtemp() - self.yaml = YAML(typ='rt') +def setup(): + """Set up for tests.""" + global TMP_DIR + TMP_DIR = mkdtemp() - def tearDown(self): - """Clean up after tests.""" - for fname in os.listdir(self.tmp_dir): - os.remove(os.path.join(self.tmp_dir, fname)) - os.rmdir(self.tmp_dir) - def _path_for(self, leaf_name): - return os.path.join(self.tmp_dir, leaf_name+".yaml") +def teardown(): + """Clean up after tests.""" + for fname in os.listdir(TMP_DIR): + os.remove(os.path.join(TMP_DIR, fname)) + os.rmdir(TMP_DIR) - def test_save_and_load(self): - """Test saving and loading back.""" - fname = self._path_for("test1") - open(fname, "w+").close() - util_yaml.save_yaml(fname, self.yaml.load(TEST_YAML_A)) - data = util_yaml.load_yaml(fname, True) - assert data == self.yaml.load(TEST_YAML_A) - def test_overwrite_and_reload(self): - """Test that we can overwrite an existing file and read back.""" - fname = self._path_for("test2") - open(fname, "w+").close() - util_yaml.save_yaml(fname, self.yaml.load(TEST_YAML_A)) - util_yaml.save_yaml(fname, self.yaml.load(TEST_YAML_B)) - data = util_yaml.load_yaml(fname, True) - assert data == self.yaml.load(TEST_YAML_B) +def _path_for(leaf_name): + return os.path.join(TMP_DIR, leaf_name+".yaml") - def test_load_bad_data(self): - """Test error from trying to load unserialisable data.""" - fname = self._path_for("test3") - with open(fname, "w") as fh: - fh.write(TEST_BAD_YAML) - with pytest.raises(HomeAssistantError): - util_yaml.load_yaml(fname, True) + +def test_save_and_load(): + """Test saving and loading back.""" + yaml = YAML(typ='rt') + fname = _path_for("test1") + open(fname, "w+").close() + util_yaml.save_yaml(fname, yaml.load(TEST_YAML_A)) + data = util_yaml.load_yaml(fname, True) + assert data == yaml.load(TEST_YAML_A) + + +def test_overwrite_and_reload(): + """Test that we can overwrite an existing file and read back.""" + yaml = YAML(typ='rt') + fname = _path_for("test2") + open(fname, "w+").close() + util_yaml.save_yaml(fname, yaml.load(TEST_YAML_A)) + util_yaml.save_yaml(fname, yaml.load(TEST_YAML_B)) + data = util_yaml.load_yaml(fname, True) + assert data == yaml.load(TEST_YAML_B) + + +def test_load_bad_data(): + """Test error from trying to load unserialisable data.""" + fname = _path_for("test3") + with open(fname, "w") as fh: + fh.write(TEST_BAD_YAML) + with pytest.raises(HomeAssistantError): + util_yaml.load_yaml(fname, True) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 533ce3c0a15..39d5db1ff83 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -1,5 +1,5 @@ """Test the unit system helper.""" -import unittest +import pytest from homeassistant.util.unit_system import ( UnitSystem, @@ -19,141 +19,138 @@ from homeassistant.const import ( TEMPERATURE, VOLUME ) -import pytest - SYSTEM_NAME = 'TEST' INVALID_UNIT = 'INVALID' -class TestUnitSystem(unittest.TestCase): - """Test the unit system helper.""" +def test_invalid_units(): + """Test errors are raised when invalid units are passed in.""" + with pytest.raises(ValueError): + UnitSystem(SYSTEM_NAME, INVALID_UNIT, LENGTH_METERS, VOLUME_LITERS, + MASS_GRAMS, PRESSURE_PA) - def test_invalid_units(self): - """Test errors are raised when invalid units are passed in.""" - with pytest.raises(ValueError): - UnitSystem(SYSTEM_NAME, INVALID_UNIT, LENGTH_METERS, VOLUME_LITERS, - MASS_GRAMS, PRESSURE_PA) + with pytest.raises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, INVALID_UNIT, VOLUME_LITERS, + MASS_GRAMS, PRESSURE_PA) - with pytest.raises(ValueError): - UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, INVALID_UNIT, VOLUME_LITERS, - MASS_GRAMS, PRESSURE_PA) + with pytest.raises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, INVALID_UNIT, + MASS_GRAMS, PRESSURE_PA) - with pytest.raises(ValueError): - UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, INVALID_UNIT, - MASS_GRAMS, PRESSURE_PA) + with pytest.raises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, + INVALID_UNIT, PRESSURE_PA) - with pytest.raises(ValueError): - UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, - INVALID_UNIT, PRESSURE_PA) + with pytest.raises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, + MASS_GRAMS, INVALID_UNIT) - with pytest.raises(ValueError): - UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, - MASS_GRAMS, INVALID_UNIT) - def test_invalid_value(self): - """Test no conversion happens if value is non-numeric.""" - with pytest.raises(TypeError): - METRIC_SYSTEM.length('25a', LENGTH_KILOMETERS) - with pytest.raises(TypeError): - METRIC_SYSTEM.temperature('50K', TEMP_CELSIUS) - with pytest.raises(TypeError): - METRIC_SYSTEM.volume('50L', VOLUME_LITERS) - with pytest.raises(TypeError): - METRIC_SYSTEM.pressure('50Pa', PRESSURE_PA) +def test_invalid_value(): + """Test no conversion happens if value is non-numeric.""" + with pytest.raises(TypeError): + METRIC_SYSTEM.length('25a', LENGTH_KILOMETERS) + with pytest.raises(TypeError): + METRIC_SYSTEM.temperature('50K', TEMP_CELSIUS) + with pytest.raises(TypeError): + METRIC_SYSTEM.volume('50L', VOLUME_LITERS) + with pytest.raises(TypeError): + METRIC_SYSTEM.pressure('50Pa', PRESSURE_PA) - def test_as_dict(self): - """Test that the as_dict() method returns the expected dictionary.""" - expected = { - LENGTH: LENGTH_KILOMETERS, - TEMPERATURE: TEMP_CELSIUS, - VOLUME: VOLUME_LITERS, - MASS: MASS_GRAMS, - PRESSURE: PRESSURE_PA - } - assert expected == METRIC_SYSTEM.as_dict() +def test_as_dict(): + """Test that the as_dict() method returns the expected dictionary.""" + expected = { + LENGTH: LENGTH_KILOMETERS, + TEMPERATURE: TEMP_CELSIUS, + VOLUME: VOLUME_LITERS, + MASS: MASS_GRAMS, + PRESSURE: PRESSURE_PA + } - def test_temperature_same_unit(self): - """Test no conversion happens if to unit is same as from unit.""" - assert 5 == \ - METRIC_SYSTEM.temperature(5, - METRIC_SYSTEM.temperature_unit) + assert expected == METRIC_SYSTEM.as_dict() - def test_temperature_unknown_unit(self): - """Test no conversion happens if unknown unit.""" - with pytest.raises(ValueError): - METRIC_SYSTEM.temperature(5, 'K') - def test_temperature_to_metric(self): - """Test temperature conversion to metric system.""" - assert 25 == \ - METRIC_SYSTEM.temperature(25, METRIC_SYSTEM.temperature_unit) - assert 26.7 == \ - round(METRIC_SYSTEM.temperature( - 80, IMPERIAL_SYSTEM.temperature_unit), 1) +def test_temperature_same_unit(): + """Test no conversion happens if to unit is same as from unit.""" + assert METRIC_SYSTEM.temperature(5, METRIC_SYSTEM.temperature_unit) == 5 - def test_temperature_to_imperial(self): - """Test temperature conversion to imperial system.""" - assert 77 == \ - IMPERIAL_SYSTEM.temperature(77, IMPERIAL_SYSTEM.temperature_unit) - assert 77 == \ - IMPERIAL_SYSTEM.temperature(25, METRIC_SYSTEM.temperature_unit) - def test_length_unknown_unit(self): - """Test length conversion with unknown from unit.""" - with pytest.raises(ValueError): - METRIC_SYSTEM.length(5, 'fr') +def test_temperature_unknown_unit(): + """Test no conversion happens if unknown unit.""" + with pytest.raises(ValueError): + METRIC_SYSTEM.temperature(5, 'K') - def test_length_to_metric(self): - """Test length conversion to metric system.""" - assert 100 == \ - METRIC_SYSTEM.length(100, METRIC_SYSTEM.length_unit) - assert 8.04672 == \ - METRIC_SYSTEM.length(5, IMPERIAL_SYSTEM.length_unit) - def test_length_to_imperial(self): - """Test length conversion to imperial system.""" - assert 100 == \ - IMPERIAL_SYSTEM.length(100, - IMPERIAL_SYSTEM.length_unit) - assert 3.106855 == \ - IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) +def test_temperature_to_metric(): + """Test temperature conversion to metric system.""" + assert METRIC_SYSTEM.temperature(25, METRIC_SYSTEM.temperature_unit) == 25 + assert round(METRIC_SYSTEM.temperature( + 80, IMPERIAL_SYSTEM.temperature_unit), 1) == 26.7 - def test_pressure_same_unit(self): - """Test no conversion happens if to unit is same as from unit.""" - assert 5 == \ - METRIC_SYSTEM.pressure(5, METRIC_SYSTEM.pressure_unit) - def test_pressure_unknown_unit(self): - """Test no conversion happens if unknown unit.""" - with pytest.raises(ValueError): - METRIC_SYSTEM.pressure(5, 'K') +def test_temperature_to_imperial(): + """Test temperature conversion to imperial system.""" + assert IMPERIAL_SYSTEM.temperature( + 77, IMPERIAL_SYSTEM.temperature_unit) == 77 + assert IMPERIAL_SYSTEM.temperature( + 25, METRIC_SYSTEM.temperature_unit) == 77 - def test_pressure_to_metric(self): - """Test pressure conversion to metric system.""" - assert 25 == \ - METRIC_SYSTEM.pressure(25, METRIC_SYSTEM.pressure_unit) - self.assertAlmostEqual( - METRIC_SYSTEM.pressure(14.7, IMPERIAL_SYSTEM.pressure_unit), - 101352.932, places=1) - def test_pressure_to_imperial(self): - """Test pressure conversion to imperial system.""" - assert 77 == \ - IMPERIAL_SYSTEM.pressure(77, IMPERIAL_SYSTEM.pressure_unit) - self.assertAlmostEqual( - IMPERIAL_SYSTEM.pressure(101352.932, METRIC_SYSTEM.pressure_unit), - 14.7, places=4) +def test_length_unknown_unit(): + """Test length conversion with unknown from unit.""" + with pytest.raises(ValueError): + METRIC_SYSTEM.length(5, 'fr') - def test_properties(self): - """Test the unit properties are returned as expected.""" - assert LENGTH_KILOMETERS == METRIC_SYSTEM.length_unit - assert TEMP_CELSIUS == METRIC_SYSTEM.temperature_unit - assert MASS_GRAMS == METRIC_SYSTEM.mass_unit - assert VOLUME_LITERS == METRIC_SYSTEM.volume_unit - assert PRESSURE_PA == METRIC_SYSTEM.pressure_unit - def test_is_metric(self): - """Test the is metric flag.""" - assert METRIC_SYSTEM.is_metric - assert not IMPERIAL_SYSTEM.is_metric +def test_length_to_metric(): + """Test length conversion to metric system.""" + assert METRIC_SYSTEM.length(100, METRIC_SYSTEM.length_unit) == 100 + assert METRIC_SYSTEM.length(5, IMPERIAL_SYSTEM.length_unit) == 8.04672 + + +def test_length_to_imperial(): + """Test length conversion to imperial system.""" + assert IMPERIAL_SYSTEM.length(100, IMPERIAL_SYSTEM.length_unit) == 100 + assert IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) == 3.106855 + + +def test_pressure_same_unit(): + """Test no conversion happens if to unit is same as from unit.""" + assert METRIC_SYSTEM.pressure(5, METRIC_SYSTEM.pressure_unit) == 5 + + +def test_pressure_unknown_unit(): + """Test no conversion happens if unknown unit.""" + with pytest.raises(ValueError): + METRIC_SYSTEM.pressure(5, 'K') + + +def test_pressure_to_metric(): + """Test pressure conversion to metric system.""" + assert METRIC_SYSTEM.pressure(25, METRIC_SYSTEM.pressure_unit) == 25 + assert METRIC_SYSTEM.pressure(14.7, IMPERIAL_SYSTEM.pressure_unit) == \ + pytest.approx(101352.932, abs=1e-1) + + +def test_pressure_to_imperial(): + """Test pressure conversion to imperial system.""" + assert IMPERIAL_SYSTEM.pressure(77, IMPERIAL_SYSTEM.pressure_unit) == 77 + assert IMPERIAL_SYSTEM.pressure( + 101352.932, METRIC_SYSTEM.pressure_unit) == \ + pytest.approx(14.7, abs=1e-4) + + +def test_properties(): + """Test the unit properties are returned as expected.""" + assert LENGTH_KILOMETERS == METRIC_SYSTEM.length_unit + assert TEMP_CELSIUS == METRIC_SYSTEM.temperature_unit + assert MASS_GRAMS == METRIC_SYSTEM.mass_unit + assert VOLUME_LITERS == METRIC_SYSTEM.volume_unit + assert PRESSURE_PA == METRIC_SYSTEM.pressure_unit + + +def test_is_metric(): + """Test the is metric flag.""" + assert METRIC_SYSTEM.is_metric + assert not IMPERIAL_SYSTEM.is_metric diff --git a/tests/util/test_volume.py b/tests/util/test_volume.py index 26208d37b68..7f8da3910cf 100644 --- a/tests/util/test_volume.py +++ b/tests/util/test_volume.py @@ -1,49 +1,45 @@ """Test homeassistant volume utility functions.""" -import unittest +import pytest + import homeassistant.util.volume as volume_util from homeassistant.const import (VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE) -import pytest - INVALID_SYMBOL = 'bob' VALID_SYMBOL = VOLUME_LITERS -class TestVolumeUtil(unittest.TestCase): - """Test the volume utility functions.""" +def test_convert_same_unit(): + """Test conversion from any unit to same unit.""" + assert volume_util.convert(2, VOLUME_LITERS, VOLUME_LITERS) == 2 + assert volume_util.convert(3, VOLUME_MILLILITERS, VOLUME_MILLILITERS) == 3 + assert volume_util.convert(4, VOLUME_GALLONS, VOLUME_GALLONS) == 4 + assert volume_util.convert(5, VOLUME_FLUID_OUNCE, VOLUME_FLUID_OUNCE) == 5 - def test_convert_same_unit(self): - """Test conversion from any unit to same unit.""" - assert 2 == volume_util.convert(2, VOLUME_LITERS, VOLUME_LITERS) - assert 3 == volume_util.convert(3, VOLUME_MILLILITERS, - VOLUME_MILLILITERS) - assert 4 == volume_util.convert(4, VOLUME_GALLONS, - VOLUME_GALLONS) - assert 5 == volume_util.convert(5, VOLUME_FLUID_OUNCE, - VOLUME_FLUID_OUNCE) - def test_convert_invalid_unit(self): - """Test exception is thrown for invalid units.""" - with pytest.raises(ValueError): - volume_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) +def test_convert_invalid_unit(): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + volume_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) - with pytest.raises(ValueError): - volume_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + with pytest.raises(ValueError): + volume_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) - def test_convert_nonnumeric_value(self): - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - volume_util.convert('a', VOLUME_GALLONS, VOLUME_LITERS) - def test_convert_from_liters(self): - """Test conversion from liters to other units.""" - liters = 5 - assert volume_util.convert(liters, VOLUME_LITERS, - VOLUME_GALLONS) == 1.321 +def test_convert_nonnumeric_value(): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + volume_util.convert('a', VOLUME_GALLONS, VOLUME_LITERS) - def test_convert_from_gallons(self): - """Test conversion from gallons to other units.""" - gallons = 5 - assert volume_util.convert(gallons, VOLUME_GALLONS, - VOLUME_LITERS) == 18.925 + +def test_convert_from_liters(): + """Test conversion from liters to other units.""" + liters = 5 + assert volume_util.convert(liters, VOLUME_LITERS, VOLUME_GALLONS) == 1.321 + + +def test_convert_from_gallons(): + """Test conversion from gallons to other units.""" + gallons = 5 + assert volume_util.convert(gallons, VOLUME_GALLONS, + VOLUME_LITERS) == 18.925 diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 99eee30027c..c7d1be3d58c 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -21,256 +21,269 @@ def mock_credstash(): yield mock_credstash -class TestYaml(unittest.TestCase): - """Test util.yaml loader.""" +def test_simple_list(): + """Test simple list.""" + conf = "config:\n - simple\n - list" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc['config'] == ["simple", "list"] - # pylint: disable=no-self-use, invalid-name - def test_simple_list(self): - """Test simple list.""" - conf = "config:\n - simple\n - list" +def test_simple_dict(): + """Test simple dict.""" + conf = "key: value" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc['key'] == 'value' + + +def test_unhashable_key(): + """Test an unhasable key.""" + files = {YAML_CONFIG_FILE: 'message:\n {{ states.state }}'} + with pytest.raises(HomeAssistantError), \ + patch_yaml_files(files): + load_yaml_config_file(YAML_CONFIG_FILE) + + +def test_no_key(): + """Test item without a key.""" + files = {YAML_CONFIG_FILE: 'a: a\nnokeyhere'} + with pytest.raises(HomeAssistantError), \ + patch_yaml_files(files): + yaml.load_yaml(YAML_CONFIG_FILE) + + +def test_environment_variable(): + """Test config file with environment variable.""" + os.environ["PASSWORD"] = "secret_password" + conf = "password: !env_var PASSWORD" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc['password'] == "secret_password" + del os.environ["PASSWORD"] + + +def test_environment_variable_default(): + """Test config file with default value for environment variable.""" + conf = "password: !env_var PASSWORD secret_password" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc['password'] == "secret_password" + + +def test_invalid_environment_variable(): + """Test config file with no environment variable sat.""" + conf = "password: !env_var PASSWORD" + with pytest.raises(HomeAssistantError): + with io.StringIO(conf) as file: + yaml.yaml.safe_load(file) + + +def test_include_yaml(): + """Test include yaml.""" + with patch_yaml_files({'test.yaml': 'value'}): + conf = 'key: !include test.yaml' with io.StringIO(conf) as file: doc = yaml.yaml.safe_load(file) - assert doc['config'] == ["simple", "list"] + assert doc["key"] == "value" - def test_simple_dict(self): - """Test simple dict.""" - conf = "key: value" + with patch_yaml_files({'test.yaml': None}): + conf = 'key: !include test.yaml' with io.StringIO(conf) as file: doc = yaml.yaml.safe_load(file) - assert doc['key'] == 'value' + assert doc["key"] == {} - def test_unhashable_key(self): - """Test an unhasable key.""" - files = {YAML_CONFIG_FILE: 'message:\n {{ states.state }}'} - with pytest.raises(HomeAssistantError), \ - patch_yaml_files(files): - load_yaml_config_file(YAML_CONFIG_FILE) - def test_no_key(self): - """Test item without a key.""" - files = {YAML_CONFIG_FILE: 'a: a\nnokeyhere'} - with pytest.raises(HomeAssistantError), \ - patch_yaml_files(files): - yaml.load_yaml(YAML_CONFIG_FILE) +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_list(mock_walk): + """Test include dir list yaml.""" + mock_walk.return_value = [ + ['/tmp', [], ['two.yaml', 'one.yaml']], + ] - def test_environment_variable(self): - """Test config file with environment variable.""" - os.environ["PASSWORD"] = "secret_password" - conf = "password: !env_var PASSWORD" - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc['password'] == "secret_password" - del os.environ["PASSWORD"] - - def test_environment_variable_default(self): - """Test config file with default value for environment variable.""" - conf = "password: !env_var PASSWORD secret_password" - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc['password'] == "secret_password" - - def test_invalid_environment_variable(self): - """Test config file with no environment variable sat.""" - conf = "password: !env_var PASSWORD" - with pytest.raises(HomeAssistantError): - with io.StringIO(conf) as file: - yaml.yaml.safe_load(file) - - def test_include_yaml(self): - """Test include yaml.""" - with patch_yaml_files({'test.yaml': 'value'}): - conf = 'key: !include test.yaml' - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc["key"] == "value" - - with patch_yaml_files({'test.yaml': None}): - conf = 'key: !include test.yaml' - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc["key"] == {} - - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_list(self, mock_walk): - """Test include dir list yaml.""" - mock_walk.return_value = [ - ['/tmp', [], ['two.yaml', 'one.yaml']], - ] - - with patch_yaml_files({ + with patch_yaml_files({ '/tmp/one.yaml': 'one', '/tmp/two.yaml': 'two', - }): - conf = "key: !include_dir_list /tmp" - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc["key"] == sorted(["one", "two"]) + }): + conf = "key: !include_dir_list /tmp" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc["key"] == sorted(["one", "two"]) - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_list_recursive(self, mock_walk): - """Test include dir recursive list yaml.""" - mock_walk.return_value = [ - ['/tmp', ['tmp2', '.ignore', 'ignore'], ['zero.yaml']], - ['/tmp/tmp2', [], ['one.yaml', 'two.yaml']], - ['/tmp/ignore', [], ['.ignore.yaml']] - ] - with patch_yaml_files({ +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_list_recursive(mock_walk): + """Test include dir recursive list yaml.""" + mock_walk.return_value = [ + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['zero.yaml']], + ['/tmp/tmp2', [], ['one.yaml', 'two.yaml']], + ['/tmp/ignore', [], ['.ignore.yaml']] + ] + + with patch_yaml_files({ '/tmp/zero.yaml': 'zero', '/tmp/tmp2/one.yaml': 'one', '/tmp/tmp2/two.yaml': 'two' - }): - conf = "key: !include_dir_list /tmp" - with io.StringIO(conf) as file: - assert '.ignore' in mock_walk.return_value[0][1], \ - "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) - assert 'tmp2' in mock_walk.return_value[0][1] - assert '.ignore' not in mock_walk.return_value[0][1] - assert sorted(doc["key"]) == sorted(["zero", "one", "two"]) + }): + conf = "key: !include_dir_list /tmp" + with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" + doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] + assert sorted(doc["key"]) == sorted(["zero", "one", "two"]) - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_named(self, mock_walk): - """Test include dir named yaml.""" - mock_walk.return_value = [ - ['/tmp', [], ['first.yaml', 'second.yaml', 'secrets.yaml']] - ] - with patch_yaml_files({ +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_named(mock_walk): + """Test include dir named yaml.""" + mock_walk.return_value = [ + ['/tmp', [], ['first.yaml', 'second.yaml', 'secrets.yaml']] + ] + + with patch_yaml_files({ '/tmp/first.yaml': 'one', '/tmp/second.yaml': 'two' - }): - conf = "key: !include_dir_named /tmp" - correct = {'first': 'one', 'second': 'two'} - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc["key"] == correct + }): + conf = "key: !include_dir_named /tmp" + correct = {'first': 'one', 'second': 'two'} + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc["key"] == correct - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_named_recursive(self, mock_walk): - """Test include dir named yaml.""" - mock_walk.return_value = [ - ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], - ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], - ['/tmp/ignore', [], ['.ignore.yaml']] - ] - with patch_yaml_files({ +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_named_recursive(mock_walk): + """Test include dir named yaml.""" + mock_walk.return_value = [ + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], + ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], + ['/tmp/ignore', [], ['.ignore.yaml']] + ] + + with patch_yaml_files({ '/tmp/first.yaml': 'one', '/tmp/tmp2/second.yaml': 'two', '/tmp/tmp2/third.yaml': 'three' - }): - conf = "key: !include_dir_named /tmp" - correct = {'first': 'one', 'second': 'two', 'third': 'three'} - with io.StringIO(conf) as file: - assert '.ignore' in mock_walk.return_value[0][1], \ - "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) - assert 'tmp2' in mock_walk.return_value[0][1] - assert '.ignore' not in mock_walk.return_value[0][1] - assert doc["key"] == correct + }): + conf = "key: !include_dir_named /tmp" + correct = {'first': 'one', 'second': 'two', 'third': 'three'} + with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" + doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] + assert doc["key"] == correct - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_merge_list(self, mock_walk): - """Test include dir merge list yaml.""" - mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] - with patch_yaml_files({ +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_merge_list(mock_walk): + """Test include dir merge list yaml.""" + mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] + + with patch_yaml_files({ '/tmp/first.yaml': '- one', '/tmp/second.yaml': '- two\n- three' - }): - conf = "key: !include_dir_merge_list /tmp" - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert sorted(doc["key"]) == sorted(["one", "two", "three"]) + }): + conf = "key: !include_dir_merge_list /tmp" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert sorted(doc["key"]) == sorted(["one", "two", "three"]) - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_merge_list_recursive(self, mock_walk): - """Test include dir merge list yaml.""" - mock_walk.return_value = [ - ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], - ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], - ['/tmp/ignore', [], ['.ignore.yaml']] - ] - with patch_yaml_files({ +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_merge_list_recursive(mock_walk): + """Test include dir merge list yaml.""" + mock_walk.return_value = [ + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], + ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], + ['/tmp/ignore', [], ['.ignore.yaml']] + ] + + with patch_yaml_files({ '/tmp/first.yaml': '- one', '/tmp/tmp2/second.yaml': '- two', '/tmp/tmp2/third.yaml': '- three\n- four' - }): - conf = "key: !include_dir_merge_list /tmp" - with io.StringIO(conf) as file: - assert '.ignore' in mock_walk.return_value[0][1], \ - "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) - assert 'tmp2' in mock_walk.return_value[0][1] - assert '.ignore' not in mock_walk.return_value[0][1] - assert sorted(doc["key"]) == sorted(["one", "two", - "three", "four"]) + }): + conf = "key: !include_dir_merge_list /tmp" + with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" + doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] + assert sorted(doc["key"]) == sorted(["one", "two", + "three", "four"]) - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_merge_named(self, mock_walk): - """Test include dir merge named yaml.""" - mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] - files = { - '/tmp/first.yaml': 'key1: one', - '/tmp/second.yaml': 'key2: two\nkey3: three', - } +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_merge_named(mock_walk): + """Test include dir merge named yaml.""" + mock_walk.return_value = [['/tmp', [], ['first.yaml', 'second.yaml']]] - with patch_yaml_files(files): - conf = "key: !include_dir_merge_named /tmp" - with io.StringIO(conf) as file: - doc = yaml.yaml.safe_load(file) - assert doc["key"] == { - "key1": "one", - "key2": "two", - "key3": "three" - } + files = { + '/tmp/first.yaml': 'key1: one', + '/tmp/second.yaml': 'key2: two\nkey3: three', + } - @patch('homeassistant.util.yaml.os.walk') - def test_include_dir_merge_named_recursive(self, mock_walk): - """Test include dir merge named yaml.""" - mock_walk.return_value = [ - ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], - ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], - ['/tmp/ignore', [], ['.ignore.yaml']] - ] + with patch_yaml_files(files): + conf = "key: !include_dir_merge_named /tmp" + with io.StringIO(conf) as file: + doc = yaml.yaml.safe_load(file) + assert doc["key"] == { + "key1": "one", + "key2": "two", + "key3": "three" + } - with patch_yaml_files({ + +@patch('homeassistant.util.yaml.os.walk') +def test_include_dir_merge_named_recursive(mock_walk): + """Test include dir merge named yaml.""" + mock_walk.return_value = [ + ['/tmp', ['tmp2', '.ignore', 'ignore'], ['first.yaml']], + ['/tmp/tmp2', [], ['second.yaml', 'third.yaml']], + ['/tmp/ignore', [], ['.ignore.yaml']] + ] + + with patch_yaml_files({ '/tmp/first.yaml': 'key1: one', '/tmp/tmp2/second.yaml': 'key2: two', '/tmp/tmp2/third.yaml': 'key3: three\nkey4: four' - }): - conf = "key: !include_dir_merge_named /tmp" - with io.StringIO(conf) as file: - assert '.ignore' in mock_walk.return_value[0][1], \ - "Expecting .ignore in here" - doc = yaml.yaml.safe_load(file) - assert 'tmp2' in mock_walk.return_value[0][1] - assert '.ignore' not in mock_walk.return_value[0][1] - assert doc["key"] == { - "key1": "one", - "key2": "two", - "key3": "three", - "key4": "four" - } + }): + conf = "key: !include_dir_merge_named /tmp" + with io.StringIO(conf) as file: + assert '.ignore' in mock_walk.return_value[0][1], \ + "Expecting .ignore in here" + doc = yaml.yaml.safe_load(file) + assert 'tmp2' in mock_walk.return_value[0][1] + assert '.ignore' not in mock_walk.return_value[0][1] + assert doc["key"] == { + "key1": "one", + "key2": "two", + "key3": "three", + "key4": "four" + } - @patch('homeassistant.util.yaml.open', create=True) - def test_load_yaml_encoding_error(self, mock_open): - """Test raising a UnicodeDecodeError.""" - mock_open.side_effect = UnicodeDecodeError('', b'', 1, 0, '') - with pytest.raises(HomeAssistantError): - yaml.load_yaml('test') - def test_dump(self): - """The that the dump method returns empty None values.""" - assert yaml.dump({'a': None, 'b': 'b'}) == 'a:\nb: b\n' +@patch('homeassistant.util.yaml.open', create=True) +def test_load_yaml_encoding_error(mock_open): + """Test raising a UnicodeDecodeError.""" + mock_open.side_effect = UnicodeDecodeError('', b'', 1, 0, '') + with pytest.raises(HomeAssistantError): + yaml.load_yaml('test') - def test_dump_unicode(self): - """The that the dump method returns empty None values.""" - assert yaml.dump({'a': None, 'b': 'привет'}) == 'a:\nb: привет\n' + +def test_dump(): + """The that the dump method returns empty None values.""" + assert yaml.dump({'a': None, 'b': 'b'}) == 'a:\nb: b\n' + + +def test_dump_unicode(): + """The that the dump method returns empty None values.""" + assert yaml.dump({'a': None, 'b': 'привет'}) == 'a:\nb: привет\n' FILES = {} @@ -415,9 +428,9 @@ class TestSecrets(unittest.TestCase): def test_secrets_are_not_dict(self): """Did secrets handle non-dict file.""" FILES[self._secret_path] = ( - '- http_pw: pwhttp\n' - ' comp1_un: un1\n' - ' comp1_pw: pw1\n') + '- http_pw: pwhttp\n' + ' comp1_un: un1\n' + ' comp1_pw: pw1\n') yaml.clear_secret_cache() with pytest.raises(HomeAssistantError): load_yaml(self._yaml_path, From 41f0066e76113c09b4349037afcd3706b5cbc4e6 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 30 Apr 2019 12:57:07 -0400 Subject: [PATCH 188/346] bump zha-quirks version (#23568) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c8bc0479f30..e107a980aa6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.7.2", - "zha-quirks==0.0.8", + "zha-quirks==0.0.9", "zigpy-deconz==0.1.4", "zigpy-homeassistant==0.3.2", "zigpy-xbee-homeassistant==0.2.0" diff --git a/requirements_all.txt b/requirements_all.txt index 48cf6409dc0..61f9a88b68e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1839,7 +1839,7 @@ zengge==0.2 zeroconf==0.21.3 # homeassistant.components.zha -zha-quirks==0.0.8 +zha-quirks==0.0.9 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 From cfaaae661acd54bd770a77a08a4aaa2263be9af2 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Tue, 30 Apr 2019 12:04:37 -0500 Subject: [PATCH 189/346] Add core APIs to migrate device identifiers and entity unique_id (#23481) * Add device identifiers migration * Add entity unique_id migration * Update per arch issue * Move to existing update methods --- homeassistant/helpers/device_registry.py | 10 +++++++-- homeassistant/helpers/entity_registry.py | 18 +++++++++++++--- tests/helpers/test_device_registry.py | 16 +++++++++++---- tests/helpers/test_entity_registry.py | 26 ++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 25c9933fd11..596bc84b6f9 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -134,16 +134,19 @@ class DeviceRegistry: @callback def async_update_device( - self, device_id, *, area_id=_UNDEF, name_by_user=_UNDEF): + self, device_id, *, area_id=_UNDEF, name_by_user=_UNDEF, + new_identifiers=_UNDEF): """Update properties of a device.""" return self._async_update_device( - device_id, area_id=area_id, name_by_user=name_by_user) + device_id, area_id=area_id, name_by_user=name_by_user, + new_identifiers=new_identifiers) @callback def _async_update_device(self, device_id, *, add_config_entry_id=_UNDEF, remove_config_entry_id=_UNDEF, merge_connections=_UNDEF, merge_identifiers=_UNDEF, + new_identifiers=_UNDEF, manufacturer=_UNDEF, model=_UNDEF, name=_UNDEF, @@ -178,6 +181,9 @@ class DeviceRegistry: if value is not _UNDEF and not value.issubset(old_value): changes[attr_name] = old_value | value + if new_identifiers is not _UNDEF: + changes['identifiers'] = new_identifiers + for attr_name, value in ( ('manufacturer', manufacturer), ('model', model), diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index be50d11d17d..64064ffde7b 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -160,18 +160,19 @@ class EntityRegistry: @callback def async_update_entity(self, entity_id, *, name=_UNDEF, - new_entity_id=_UNDEF): + new_entity_id=_UNDEF, new_unique_id=_UNDEF): """Update properties of an entity.""" return self._async_update_entity( entity_id, name=name, - new_entity_id=new_entity_id + new_entity_id=new_entity_id, + new_unique_id=new_unique_id ) @callback def _async_update_entity(self, entity_id, *, name=_UNDEF, config_entry_id=_UNDEF, new_entity_id=_UNDEF, - device_id=_UNDEF): + device_id=_UNDEF, new_unique_id=_UNDEF): """Private facing update properties method.""" old = self.entities[entity_id] @@ -201,6 +202,17 @@ class EntityRegistry: self.entities.pop(entity_id) entity_id = changes['entity_id'] = new_entity_id + if new_unique_id is not _UNDEF: + conflict = next((entity for entity in self.entities.values() + if entity.unique_id == new_unique_id + and entity.domain == old.domain + and entity.platform == old.platform), None) + if conflict: + raise ValueError( + "Unique id '{}' is already in use by '{}'".format( + new_unique_id, conflict.entity_id)) + changes['unique_id'] = new_unique_id + if not changes: return old diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index adfa05a021b..8c874a9837b 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -361,17 +361,25 @@ async def test_update(registry): config_entry_id='1234', connections={ (device_registry.CONNECTION_NETWORK_MAC, '12:34:56:AB:CD:EF') - }) - + }, + identifiers={('hue', '456'), ('bla', '123')}) + new_identifiers = { + ('hue', '654'), + ('bla', '321') + } assert not entry.area_id assert not entry.name_by_user - updated_entry = registry.async_update_device( - entry.id, area_id='12345A', name_by_user='Test Friendly Name') + with patch.object(registry, 'async_schedule_save') as mock_save: + updated_entry = registry.async_update_device( + entry.id, area_id='12345A', name_by_user='Test Friendly Name', + new_identifiers=new_identifiers) + assert mock_save.call_count == 1 assert updated_entry != entry assert updated_entry.area_id == '12345A' assert updated_entry.name_by_user == 'Test Friendly Name' + assert updated_entry.identifiers == new_identifiers async def test_loading_race_condition(hass): diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 3fb79f693bd..529b03160ca 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -271,3 +271,29 @@ async def test_loading_race_condition(hass): mock_load.assert_called_once_with() assert results[0] == results[1] + + +async def test_update_entity_unique_id(registry): + """Test entity's unique_id is updated.""" + entry = registry.async_get_or_create( + 'light', 'hue', '5678', config_entry_id='mock-id-1') + new_unique_id = '1234' + with patch.object(registry, 'async_schedule_save') as mock_schedule_save: + updated_entry = registry.async_update_entity( + entry.entity_id, new_unique_id=new_unique_id) + assert updated_entry != entry + assert updated_entry.unique_id == new_unique_id + assert mock_schedule_save.call_count == 1 + + +async def test_update_entity_unique_id_conflict(registry): + """Test migration raises when unique_id already in use.""" + entry = registry.async_get_or_create( + 'light', 'hue', '5678', config_entry_id='mock-id-1') + entry2 = registry.async_get_or_create( + 'light', 'hue', '1234', config_entry_id='mock-id-1') + with patch.object(registry, 'async_schedule_save') as mock_schedule_save, \ + pytest.raises(ValueError): + registry.async_update_entity( + entry.entity_id, new_unique_id=entry2.unique_id) + assert mock_schedule_save.call_count == 0 From 603e2cd961e1ca7c3aae46f14e862419b223b564 Mon Sep 17 00:00:00 2001 From: Sylvia van Os Date: Tue, 30 Apr 2019 21:02:16 +0200 Subject: [PATCH 190/346] Essent sensor (#23513) * Initial commit for Essent * Cleanup Essent component * Update CODEOWNERS * Move stuff to PyEssent * Update requirements_all * Fix PyEssent * Move meter list to PyEssent library * Update requirements_all * Only check for updates once an hour * Use PyEssent 0.10 * Fixing up Essent component * Fix crash * Don't add unused meter/tariff combos * Fix lint * Get tariffs per meter * Don't hammer Essent API * Fix linting errors * Fix old description * Fix old call * Cleanup Essent component --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/essent/__init__.py | 1 + homeassistant/components/essent/manifest.json | 8 ++ homeassistant/components/essent/sensor.py | 112 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 126 insertions(+) create mode 100644 homeassistant/components/essent/__init__.py create mode 100644 homeassistant/components/essent/manifest.json create mode 100644 homeassistant/components/essent/sensor.py diff --git a/.coveragerc b/.coveragerc index 3aeb2b5c187..a574c2ead43 100644 --- a/.coveragerc +++ b/.coveragerc @@ -173,6 +173,7 @@ omit = homeassistant/components/esphome/light.py homeassistant/components/esphome/sensor.py homeassistant/components/esphome/switch.py + homeassistant/components/essent/sensor.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* homeassistant/components/everlights/light.py diff --git a/CODEOWNERS b/CODEOWNERS index 979805a65dd..9f5b83bf941 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -71,6 +71,7 @@ homeassistant/components/ephember/* @ttroy50 homeassistant/components/epsonworkforce/* @ThaStealth homeassistant/components/eq3btsmart/* @rytilahti homeassistant/components/esphome/* @OttoWinter +homeassistant/components/essent/* @TheLastProject homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes homeassistant/components/fitbit/* @robbiet480 diff --git a/homeassistant/components/essent/__init__.py b/homeassistant/components/essent/__init__.py new file mode 100644 index 00000000000..42e867c6d21 --- /dev/null +++ b/homeassistant/components/essent/__init__.py @@ -0,0 +1 @@ +"""The Essent component.""" diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json new file mode 100644 index 00000000000..49189f6bacb --- /dev/null +++ b/homeassistant/components/essent/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "essent", + "name": "Essent", + "documentation": "https://www.home-assistant.io/components/essent", + "requirements": ["PyEssent==0.10"], + "dependencies": [], + "codeowners": ["@TheLastProject"] +} diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py new file mode 100644 index 00000000000..545ed3d5baf --- /dev/null +++ b/homeassistant/components/essent/sensor.py @@ -0,0 +1,112 @@ +"""Support for Essent API.""" +from datetime import timedelta + +from pyessent import PyEssent +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +SCAN_INTERVAL = timedelta(hours=1) + +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 Essent platform.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + essent = EssentBase(username, password) + meters = [] + for meter in essent.retrieve_meters(): + data = essent.retrieve_meter_data(meter) + for tariff in data['values']['LVR'].keys(): + meters.append(EssentMeter( + essent, + meter, + data['type'], + tariff, + data['values']['LVR'][tariff]['unit'])) + + add_devices(meters, True) + + +class EssentBase(): + """Essent Base.""" + + def __init__(self, username, password): + """Initialize the Essent API.""" + self._username = username + self._password = password + self._meters = [] + self._meter_data = {} + + self.update() + + def retrieve_meters(self): + """Retrieve the list of meters.""" + return self._meters + + def retrieve_meter_data(self, meter): + """Retrieve the data for this meter.""" + return self._meter_data[meter] + + @Throttle(timedelta(minutes=30)) + def update(self): + """Retrieve the latest meter data from Essent.""" + essent = PyEssent(self._username, self._password) + self._meters = essent.get_EANs() + for meter in self._meters: + self._meter_data[meter] = essent.read_meter( + meter, only_last_meter_reading=True) + + +class EssentMeter(Entity): + """Representation of Essent measurements.""" + + def __init__(self, essent_base, meter, meter_type, tariff, unit): + """Initialize the sensor.""" + self._state = None + self._essent_base = essent_base + self._meter = meter + self._type = meter_type + self._tariff = tariff + self._unit = unit + + @property + def name(self): + """Return the name of the sensor.""" + return "Essent {} ({})".format(self._type, self._tariff) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self._unit.lower() == 'kwh': + return ENERGY_KILO_WATT_HOUR + + return self._unit + + def update(self): + """Fetch the energy usage.""" + # Ensure our data isn't too old + self._essent_base.update() + + # Retrieve our meter + data = self._essent_base.retrieve_meter_data(self._meter) + + # Set our value + self._state = next( + iter(data['values']['LVR'][self._tariff]['records'].values())) diff --git a/requirements_all.txt b/requirements_all.txt index 61f9a88b68e..22d1edaec2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -41,6 +41,9 @@ Mastodon.py==1.3.1 # homeassistant.components.orangepi_gpio OPi.GPIO==0.3.6 +# homeassistant.components.essent +PyEssent==0.10 + # homeassistant.components.github PyGithub==1.43.5 From 75a2c057f283d99afcb8ac1da822d98974775d4e Mon Sep 17 00:00:00 2001 From: Cyro Date: Tue, 30 Apr 2019 22:12:39 +0200 Subject: [PATCH 191/346] Upgrade discord.py to v1.0.1 (#23523) --- .../components/discord/manifest.json | 2 +- homeassistant/components/discord/notify.py | 41 +++++++++++++++---- requirements_all.txt | 2 +- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 155e2b6806f..05b2a3c8e06 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,7 +3,7 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/components/discord", "requirements": [ - "discord.py==0.16.12" + "discord.py==1.0.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index faf79d14e33..42f54086145 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -39,30 +39,53 @@ class DiscordNotificationService(BaseNotificationService): discord.VoiceClient.warn_nacl = False discord_bot = discord.Client(loop=self.hass.loop) + images = None if ATTR_TARGET not in kwargs: _LOGGER.error("No target specified") return None + if ATTR_DATA in kwargs: + data = kwargs.get(ATTR_DATA) + + if ATTR_IMAGES in data: + import os.path + images = list() + + for image in data.get(ATTR_IMAGES): + if os.path.isfile(image): + images.append(image) + else: + _LOGGER.warning("Image not found: %s", image) + # pylint: disable=unused-variable @discord_bot.event async def on_ready(): """Send the messages when the bot is ready.""" try: - data = kwargs.get(ATTR_DATA) - images = None - if data: - images = data.get(ATTR_IMAGES) for channelid in kwargs[ATTR_TARGET]: - channel = discord.Object(id=channelid) - await discord_bot.send_message(channel, message) + channelid = int(channelid) + channel = discord_bot.get_channel(channelid) + + if channel is None: + _LOGGER.warning( + "Channel not found for id: %s", + channelid) + continue + + # Must create new instances of File for each channel. + files = None if images: - for anum, f_name in enumerate(images): - await discord_bot.send_file(channel, f_name) + files = list() + for image in images: + files.append(discord.File(image)) + + await channel.send(message, files=files) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) await discord_bot.logout() await discord_bot.close() - await discord_bot.start(self.token) + # Using reconnect=False prevents multiple ready events to be fired. + await discord_bot.start(self.token, reconnect=False) diff --git a/requirements_all.txt b/requirements_all.txt index 22d1edaec2d..35bf6eac815 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -344,7 +344,7 @@ directpy==0.5 discogs_client==2.2.1 # homeassistant.components.discord -discord.py==0.16.12 +discord.py==1.0.1 # homeassistant.components.updater distro==1.4.0 From 09012e7baa0c80fbc882795e15b139486dad5675 Mon Sep 17 00:00:00 2001 From: Mike Miller Date: Tue, 30 Apr 2019 23:43:21 +0300 Subject: [PATCH 192/346] Always pad mac addresses from nmap tracker with leading zeros (#23492) --- homeassistant/components/nmap_tracker/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 3537f01b2b8..1b528b0af7e 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -47,7 +47,7 @@ def _arp(ip_address): out, _ = arp.communicate() match = re.search(r'(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})', str(out)) if match: - return match.group(0) + return ':'.join([i.zfill(2) for i in match.group(0).split(':')]) _LOGGER.info('No MAC address found for %s', ip_address) return None From 5ded0dd3fac99830f89e5d4d7926a8f3a941074f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 30 Apr 2019 23:06:44 +0200 Subject: [PATCH 193/346] Update mysensors sensor icons (#23491) * Add some icons. * Sort sensor types according to mysensors serial api numbering. --- homeassistant/components/mysensors/sensor.py | 21 +++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 9acd47b6238..d9154847ca0 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -8,21 +8,28 @@ SENSORS = { 'V_TEMP': [None, 'mdi:thermometer'], 'V_HUM': ['%', 'mdi:water-percent'], 'V_DIMMER': ['%', 'mdi:percent'], - 'V_LIGHT_LEVEL': ['%', 'white-balance-sunny'], + 'V_PERCENTAGE': ['%', 'mdi:percent'], + 'V_PRESSURE': [None, 'mdi:gauge'], + 'V_FORECAST': [None, 'mdi:weather-partlycloudy'], + 'V_RAIN': [None, 'mdi:weather-rainy'], + 'V_RAINRATE': [None, 'mdi:weather-rainy'], + 'V_WIND': [None, 'mdi:weather-windy'], + 'V_GUST': [None, 'mdi:weather-windy'], 'V_DIRECTION': ['°', 'mdi:compass'], 'V_WEIGHT': ['kg', 'mdi:weight-kilogram'], 'V_DISTANCE': ['m', 'mdi:ruler'], 'V_IMPEDANCE': ['ohm', None], 'V_WATT': [POWER_WATT, None], 'V_KWH': [ENERGY_KILO_WATT_HOUR, None], - 'V_FLOW': ['m', None], + 'V_LIGHT_LEVEL': ['%', 'white-balance-sunny'], + 'V_FLOW': ['m', 'mdi:gauge'], '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': ['lx', 'white-balance-sunny']}, + 'V_VOLTAGE': ['V', 'mdi:flash'], + 'V_CURRENT': ['A', 'mdi:flash-auto'], + 'V_PH': ['pH', None], 'V_ORP': ['mV', None], 'V_EC': ['μS/cm', None], 'V_VAR': ['var', None], @@ -65,8 +72,8 @@ class MySensorsSensor(mysensors.device.MySensorsEntity): 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): + 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 From 24060e0fb55facede6c040c70b59281d40b5fe0b Mon Sep 17 00:00:00 2001 From: Jonas Pedersen Date: Tue, 30 Apr 2019 23:07:49 +0200 Subject: [PATCH 194/346] Add bypass and automatic bypass switch for Danfor Air. (#23572) Add bypass and automatic bypass switch for Danfoss Air --- homeassistant/components/danfoss_air/__init__.py | 4 ++++ homeassistant/components/danfoss_air/manifest.json | 2 +- homeassistant/components/danfoss_air/switch.py | 10 +++++++++- requirements_all.txt | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/danfoss_air/__init__.py b/homeassistant/components/danfoss_air/__init__.py index a340b94e9a4..6e86b16c02d 100644 --- a/homeassistant/components/danfoss_air/__init__.py +++ b/homeassistant/components/danfoss_air/__init__.py @@ -85,5 +85,9 @@ class DanfossAir: = self._client.command(ReadCommand.boost) self._data[ReadCommand.battery_percent] \ = self._client.command(ReadCommand.battery_percent) + self._data[ReadCommand.bypass] \ + = self._client.command(ReadCommand.bypass) + self._data[ReadCommand.automatic_bypass] \ + = self._client.command(ReadCommand.automatic_bypass) _LOGGER.debug("Done fetching data from Danfoss Air CCM module") diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index 8af1707de65..a210b5a78d1 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -3,7 +3,7 @@ "name": "Danfoss air", "documentation": "https://www.home-assistant.io/components/danfoss_air", "requirements": [ - "pydanfossair==0.0.7" + "pydanfossair==0.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index f5a7fd47f69..4e7fce28dc7 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -19,6 +19,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ReadCommand.boost, UpdateCommand.boost_activate, UpdateCommand.boost_deactivate], + ["Danfoss Air Bypass", + ReadCommand.bypass, + UpdateCommand.bypass_activate, + UpdateCommand.bypass_deactivate], + ["Danfoss Air Automatic Bypass", + ReadCommand.automatic_bypass, + UpdateCommand.bypass_activate, + UpdateCommand.bypass_deactivate], ] dev = [] @@ -59,7 +67,7 @@ class DanfossAir(SwitchDevice): def turn_off(self, **kwargs): """Turn the switch off.""" - _LOGGER.debug("Turning of switch with command %s", self._off_command) + _LOGGER.debug("Turning off switch with command %s", self._off_command) self._data.update_state(self._off_command, self._state_command) def update(self): diff --git a/requirements_all.txt b/requirements_all.txt index 35bf6eac815..b4f78ab24da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1009,7 +1009,7 @@ pycsspeechtts==1.0.2 pydaikin==1.4.0 # homeassistant.components.danfoss_air -pydanfossair==0.0.7 +pydanfossair==0.1.0 # homeassistant.components.deconz pydeconz==55 From b0843f4a38d6418e0edcf12ff339fa56c7ef7a7c Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 1 May 2019 06:07:34 +0800 Subject: [PATCH 195/346] Ptvsd debugger component. (#23336) * ptvsd debugger component. * Add test case * ptvsd as test dependency * Fix for 3.5 * Fixed bootstrap test * Use dict direct lookup. * Don't need to load dependencies. * Get the test working. * 3.5 fix * Set mock return value * Put tests back, but skip them * Change log level --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/bootstrap.py | 12 +++- homeassistant/components/ptvsd/__init__.py | 63 ++++++++++++++++++++ homeassistant/components/ptvsd/manifest.json | 10 ++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/ptvsd/__init__py | 1 + tests/components/ptvsd/test_ptvsd.py | 56 +++++++++++++++++ 10 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/ptvsd/__init__.py create mode 100644 homeassistant/components/ptvsd/manifest.json create mode 100644 tests/components/ptvsd/__init__py create mode 100644 tests/components/ptvsd/test_ptvsd.py diff --git a/.coveragerc b/.coveragerc index a574c2ead43..de778888097 100644 --- a/.coveragerc +++ b/.coveragerc @@ -451,6 +451,7 @@ omit = homeassistant/components/proxy/camera.py homeassistant/components/ps4/__init__.py homeassistant/components/ps4/media_player.py + homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9f5b83bf941..6d27c8563fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -175,6 +175,7 @@ homeassistant/components/pi_hole/* @fabaff homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 +homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/qnap/* @colinodell diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 3959eb88035..d63caf9e76f 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -26,6 +26,7 @@ ERROR_LOG_FILENAME = 'home-assistant.log' # hass.data key for logging information. DATA_LOGGING = 'logging' +DEBUGGER_INTEGRATIONS = {'ptvsd', } CORE_INTEGRATIONS = ('homeassistant', 'persistent_notification') LOGGING_INTEGRATIONS = {'logger', 'system_log'} STAGE_1_INTEGRATIONS = { @@ -306,6 +307,15 @@ async def _async_set_up_integrations( """Set up all the integrations.""" domains = _get_domains(hass, config) + # Start up debuggers. Start these first in case they want to wait. + debuggers = domains & DEBUGGER_INTEGRATIONS + if debuggers: + _LOGGER.debug("Starting up debuggers %s", debuggers) + await asyncio.gather(*[ + async_setup_component(hass, domain, config) + for domain in debuggers]) + domains -= DEBUGGER_INTEGRATIONS + # Resolve all dependencies of all components so we can find the logging # and integrations that need faster initialization. resolved_domains_task = asyncio.gather(*[ @@ -339,7 +349,7 @@ async def _async_set_up_integrations( stage_2_domains = domains - logging_domains - stage_1_domains if logging_domains: - _LOGGER.debug("Setting up %s", logging_domains) + _LOGGER.info("Setting up %s", logging_domains) await asyncio.gather(*[ async_setup_component(hass, domain, config) diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py new file mode 100644 index 00000000000..2a86e15ddd2 --- /dev/null +++ b/homeassistant/components/ptvsd/__init__.py @@ -0,0 +1,63 @@ +""" +Enable ptvsd debugger to attach to HA. + +Attach ptvsd debugger by default to port 5678. +""" + +import logging +from threading import Thread +from asyncio import Event + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PORT) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +DOMAIN = 'ptvsd' + +CONF_WAIT = 'wait' + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Optional( + CONF_HOST, default='0.0.0.0' + ): cv.string, + vol.Optional( + CONF_PORT, default=5678 + ): cv.port, + vol.Optional( + CONF_WAIT, default=False + ): cv.boolean, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up ptvsd debugger.""" + import ptvsd + + conf = config[DOMAIN] + host = conf[CONF_HOST] + port = conf[CONF_PORT] + + ptvsd.enable_attach((host, port)) + + wait = conf[CONF_WAIT] + if wait: + _LOGGER.warning("Waiting for ptvsd connection on %s:%s", host, port) + ready = Event() + + def waitfor(): + ptvsd.wait_for_attach() + hass.loop.call_soon_threadsafe(ready.set) + Thread(target=waitfor).start() + + await ready.wait() + else: + _LOGGER.warning("Listening for ptvsd connection on %s:%s", host, port) + + return True diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json new file mode 100644 index 00000000000..8bd46c3dc32 --- /dev/null +++ b/homeassistant/components/ptvsd/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "ptvsd", + "name": "ptvsd", + "documentation": "https://www.home-assistant.io/components/ptvsd", + "requirements": [ + "ptvsd==4.2.8" + ], + "dependencies": [], + "codeowners": ["@swamp-ig"] +} diff --git a/requirements_all.txt b/requirements_all.txt index b4f78ab24da..a95ce69ae2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -884,6 +884,9 @@ protobuf==3.6.1 # homeassistant.components.systemmonitor psutil==5.6.1 +# homeassistant.components.ptvsd +ptvsd==4.2.8 + # homeassistant.components.wink pubnubsub-handler==1.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 402647172e3..b22a3be31b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -204,6 +204,9 @@ pmsensor==0.4 # homeassistant.components.prometheus prometheus_client==0.2.0 +# homeassistant.components.ptvsd +ptvsd==4.2.8 + # homeassistant.components.pushbullet pushbullet.py==0.11.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dc8ed652d11..ad0a833436e 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -94,6 +94,7 @@ TEST_REQUIREMENTS = ( 'pilight', 'pmsensor', 'prometheus_client', + 'ptvsd', 'pushbullet.py', 'py-canary', 'pyblackbird', diff --git a/tests/components/ptvsd/__init__py b/tests/components/ptvsd/__init__py new file mode 100644 index 00000000000..e2a1a9ba0a6 --- /dev/null +++ b/tests/components/ptvsd/__init__py @@ -0,0 +1 @@ +"""Tests for PTVSD Debugger""" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py new file mode 100644 index 00000000000..169ab8fb97a --- /dev/null +++ b/tests/components/ptvsd/test_ptvsd.py @@ -0,0 +1,56 @@ +"""Tests for PTVSD Debugger.""" + +from unittest.mock import patch +from asynctest import CoroutineMock +from pytest import mark + +import homeassistant.components.ptvsd as ptvsd_component +from homeassistant.setup import async_setup_component +from homeassistant.bootstrap import _async_set_up_integrations + + +@mark.skip('causes code cover to fail') +async def test_ptvsd(hass): + """Test loading ptvsd component.""" + with patch('ptvsd.enable_attach') as attach: + with patch('ptvsd.wait_for_attach') as wait: + assert await async_setup_component( + hass, ptvsd_component.DOMAIN, { + ptvsd_component.DOMAIN: {} + }) + + attach.assert_called_once_with(('0.0.0.0', 5678)) + assert wait.call_count == 0 + + +@mark.skip('causes code cover to fail') +async def test_ptvsd_wait(hass): + """Test loading ptvsd component with wait.""" + with patch('ptvsd.enable_attach') as attach: + with patch('ptvsd.wait_for_attach') as wait: + assert await async_setup_component( + hass, ptvsd_component.DOMAIN, { + ptvsd_component.DOMAIN: { + ptvsd_component.CONF_WAIT: True + } + }) + + attach.assert_called_once_with(('0.0.0.0', 5678)) + assert wait.call_count == 1 + + +async def test_ptvsd_bootstrap(hass): + """Test loading ptvsd component with wait.""" + config = { + ptvsd_component.DOMAIN: { + ptvsd_component.CONF_WAIT: True + } + } + + with patch( + 'homeassistant.components.ptvsd.async_setup', + CoroutineMock()) as setup_mock: + setup_mock.return_value = True + await _async_set_up_integrations(hass, config) + + assert setup_mock.call_count == 1 From 1c4367e5a946eb05d1db69a491f62d6ddae3843f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 30 Apr 2019 23:08:31 +0100 Subject: [PATCH 196/346] Fix min/max temp and humidity for homekit_controller climate (#23421) * Fix min/max temp and humidity for homekit_controller climate. * Fix typo --- .../components/homekit_controller/climate.py | 45 ++++++++++++++++++- tests/components/homekit_controller/common.py | 6 +++ .../specific_devices/test_ecobee3.py | 7 +++ .../homekit_controller/test_climate.py | 2 + 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 2cbd8f6d700..54d11b3b09e 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -4,7 +4,8 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( STATE_COOL, STATE_HEAT, STATE_IDLE, SUPPORT_OPERATION_MODE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY) + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW) from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity @@ -43,6 +44,10 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): self._target_temp = None self._current_humidity = None self._target_humidity = None + self._min_target_temp = None + self._max_target_temp = None + self._min_target_humidity = None + self._max_target_humidity = None super().__init__(*args) def get_characteristic_types(self): @@ -86,9 +91,23 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): def _setup_temperature_target(self, characteristic): self._features |= SUPPORT_TARGET_TEMPERATURE + if 'minValue' in characteristic: + self._min_target_temp = characteristic['minValue'] + + if 'maxValue' in characteristic: + self._max_target_temp = characteristic['maxValue'] + def _setup_relative_humidity_target(self, characteristic): self._features |= SUPPORT_TARGET_HUMIDITY + if 'minValue' in characteristic: + self._min_target_humidity = characteristic['minValue'] + self._features |= SUPPORT_TARGET_HUMIDITY_LOW + + if 'maxValue' in characteristic: + self._max_target_humidity = characteristic['maxValue'] + self._features |= SUPPORT_TARGET_HUMIDITY_HIGH + def _update_heating_cooling_current(self, value): self._state = MODE_HOMEKIT_TO_HASS.get(value) @@ -152,6 +171,20 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp + @property + def min_temp(self): + """Return the minimum target temp.""" + if self._max_target_temp: + return self._min_target_temp + return super().min_temp + + @property + def max_temp(self): + """Return the maximum target temp.""" + if self._max_target_temp: + return self._max_target_temp + return super().max_temp + @property def current_humidity(self): """Return the current humidity.""" @@ -162,6 +195,16 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): """Return the humidity we try to reach.""" return self._target_humidity + @property + def min_humidity(self): + """Return the minimum humidity.""" + return self._min_target_humidity + + @property + def max_humidity(self): + """Return the maximum humidity.""" + return self._max_target_humidity + @property def current_operation(self): """Return current operation ie. heat, cool, idle.""" diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 43003251218..84abce0a1fe 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -180,6 +180,12 @@ async def setup_accessories_from_file(hass, path): char.description = char_data['description'] if 'value' in char_data: char.value = char_data['value'] + if 'minValue' in char_data: + char.minValue = char_data['minValue'] + if 'maxValue' in char_data: + char.maxValue = char_data['maxValue'] + if 'valid-values' in char_data: + char.valid_values = char_data['valid-values'] service.characteristics.append(char) accessory.services.append(service) diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index a7e449ddbe4..780904588c6 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -10,6 +10,7 @@ from homekit import AccessoryDisconnectedError from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, SUPPORT_OPERATION_MODE) from tests.components.homekit_controller.common import ( FakePairing, device_config_changed, setup_accessories_from_file, @@ -32,9 +33,15 @@ async def test_ecobee3_setup(hass): assert climate_state.attributes['friendly_name'] == 'HomeW' assert climate_state.attributes['supported_features'] == ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY | + SUPPORT_TARGET_HUMIDITY_HIGH | SUPPORT_TARGET_HUMIDITY_LOW | SUPPORT_OPERATION_MODE ) + assert climate_state.attributes['min_temp'] == 7.2 + assert climate_state.attributes['max_temp'] == 33.3 + assert climate_state.attributes['min_humidity'] == 20 + assert climate_state.attributes['max_humidity'] == 50 + occ1 = entity_registry.async_get('binary_sensor.kitchen') assert occ1.unique_id == 'homekit-AB1C-56' diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 4c0a5debb65..e444a25dca6 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -138,6 +138,8 @@ async def test_climate_read_thermostat_state(hass, utcnow): assert state.state == 'heat' assert state.attributes['current_temperature'] == 19 assert state.attributes['current_humidity'] == 50 + assert state.attributes['min_temp'] == 7 + assert state.attributes['max_temp'] == 35 # Simulate that cooling is on helper.characteristics[TEMPERATURE_CURRENT].value = 21 From 581b16e9fa95e4f3650ed351efd5bf250f51d4db Mon Sep 17 00:00:00 2001 From: etheralm <8655564+etheralm@users.noreply.github.com> Date: Wed, 1 May 2019 01:47:40 +0200 Subject: [PATCH 197/346] Fix failing state update tests (#23575) --- tests/components/dyson/test_air_quality.py | 7 +++++-- tests/components/dyson/test_fan.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index ab068823d64..1c7947fd621 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -86,8 +86,11 @@ async def test_purecool_aiq_update_state(devices, login, hass): device.environmental_state = \ DysonEnvironmentalSensorV2State(json.dumps(event)) - callback = device.add_message_listener.call_args_list[2][0][0] - callback(device.environmental_state) + for call in device.add_message_listener.call_args_list: + callback = call[0][0] + if type(callback.__self__) == dyson.DysonAirSensor: + callback(device.environmental_state) + await hass.async_block_till_done() fan_state = hass.states.get("air_quality.living_room") attributes = fan_state.attributes diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 09622e4d36d..07c919c6e90 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -680,8 +680,11 @@ async def test_purecool_update_state(devices, login, hass): "osau": "0095", "ancp": "CUST"}} device.state = DysonPureCoolV2State(json.dumps(event)) - callback = device.add_message_listener.call_args_list[3][0][0] - callback(device.state) + for call in device.add_message_listener.call_args_list: + callback = call[0][0] + if type(callback.__self__) == dyson.DysonPureCoolDevice: + callback(device.state) + await hass.async_block_till_done() fan_state = hass.states.get("fan.living_room") attributes = fan_state.attributes From 5b9d01139d016ef0b19f63bd44b40b7b0b801d99 Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Wed, 1 May 2019 10:54:25 +0800 Subject: [PATCH 198/346] render_with_collect method for template (#23283) * Make entity_filter be a modifiable builder * Add render_with_collect method * Use sync render_with_collect and non-class based test case * Refactor: Template renders to RenderInfo * Freeze with exception too * Finish merging test changes * Removed unused sync interface * Final bits of the diff --- homeassistant/helpers/config_validation.py | 3 +- homeassistant/helpers/template.py | 203 ++++++++++-- tests/helpers/test_template.py | 358 +++++++++++++++++---- 3 files changed, 474 insertions(+), 90 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 1f139704e5f..9282770de1a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -23,7 +23,6 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, __version__) from homeassistant.core import valid_entity_id, split_entity_id from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template as template_helper from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify @@ -445,6 +444,8 @@ unit_system = vol.All(vol.Lower, vol.Any(CONF_UNIT_SYSTEM_METRIC, def template(value): """Validate a jinja2 template.""" + from homeassistant.helpers import template as template_helper + if value is None: raise vol.Invalid('template value is None') if isinstance(value, (list, dict, template_helper.Template)): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 24275c87061..203e460aaa5 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,21 +1,21 @@ """Template helper methods for rendering strings with Home Assistant data.""" -from datetime import datetime +import base64 import json import logging import math import random -import base64 import re +from datetime import datetime import jinja2 from jinja2 import contextfilter from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace -from homeassistant.const import ( - ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, MATCH_ALL, - STATE_UNKNOWN) -from homeassistant.core import State, valid_entity_id +from homeassistant.const import (ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, + ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN) +from homeassistant.core import ( + State, callback, valid_entity_id, split_entity_id) from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.helpers.typing import TemplateVarsType @@ -29,6 +29,8 @@ _LOGGER = logging.getLogger(__name__) _SENTINEL = object() DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S" +_RENDER_INFO = 'template.render_info' + _RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M) _RE_GET_ENTITIES = re.compile( r"(?:(?:states\.|(?:is_state|is_state_attr|state_attr|states)" @@ -89,6 +91,54 @@ def extract_entities(template, variables=None): return MATCH_ALL +def _true(arg) -> bool: + return True + + +class RenderInfo: + """Holds information about a template render.""" + + def __init__(self, template): + """Initialise.""" + self.template = template + # Will be set sensibly once frozen. + self.filter_lifecycle = _true + self._result = None + self._exception = None + self._all_states = False + self._domains = [] + self._entities = [] + + def filter(self, entity_id: str) -> bool: + """Template should re-render if the state changes.""" + return entity_id in self._entities + + def _filter_lifecycle(self, entity_id: str) -> bool: + """Template should re-render if the state changes.""" + return ( + split_entity_id(entity_id)[0] in self._domains + or entity_id in self._entities) + + @property + def result(self) -> str: + """Results of the template computation.""" + if self._exception is not None: + raise self._exception # pylint: disable=raising-bad-type + return self._result + + def _freeze(self) -> None: + self._entities = frozenset(self._entities) + if self._all_states: + # Leave lifecycle_filter as True + del self._domains + elif not self._domains: + del self._domains + self.filter_lifecycle = self.filter + else: + self._domains = frozenset(self._domains) + self.filter_lifecycle = self._filter_lifecycle + + class Template: """Class to hold a template and manage caching and rendering.""" @@ -124,6 +174,7 @@ class Template: return run_callback_threadsafe( self.hass.loop, self.async_render, kwargs).result() + @callback def async_render(self, variables: TemplateVarsType = None, **kwargs) -> str: """Render given template. @@ -141,6 +192,23 @@ class Template: except jinja2.TemplateError as err: raise TemplateError(err) + @callback + def async_render_to_info( + self, variables: TemplateVarsType = None, + **kwargs) -> RenderInfo: + """Render the template and collect an entity filter.""" + assert self.hass and _RENDER_INFO not in self.hass.data + render_info = self.hass.data[_RENDER_INFO] = RenderInfo(self) + # pylint: disable=protected-access + try: + render_info._result = self.async_render(variables, **kwargs) + except TemplateError as ex: + render_info._exception = ex + finally: + del self.hass.data[_RENDER_INFO] + render_info._freeze() + return render_info + def render_with_possible_json_value(self, value, error_value=_SENTINEL): """Render template with value exposed. @@ -150,6 +218,7 @@ class Template: self.hass.loop, self.async_render_with_possible_json_value, value, error_value).result() + @callback def async_render_with_possible_json_value(self, value, error_value=_SENTINEL, variables=None): @@ -190,7 +259,7 @@ class Template: global_vars = ENV.make_globals({ 'closest': template_methods.closest, 'distance': template_methods.distance, - 'is_state': self.hass.states.is_state, + 'is_state': template_methods.is_state, 'is_state_attr': template_methods.is_state_attr, 'state_attr': template_methods.state_attr, 'states': AllStates(self.hass), @@ -207,6 +276,14 @@ class Template: self.template == other.template and self.hass == other.hass) + def __hash__(self): + """Hash code for template.""" + return hash(self.template) + + def __repr__(self): + """Representation of Template.""" + return 'Template(\"' + self.template + '\")' + class AllStates: """Class to expose all HA states as attributes.""" @@ -217,24 +294,42 @@ class AllStates: def __getattr__(self, name): """Return the domain state.""" + if '.' in name: + if not valid_entity_id(name): + raise TemplateError("Invalid entity ID '{}'".format(name)) + return _get_state(self._hass, name) + if not valid_entity_id(name + '.entity'): + raise TemplateError("Invalid domain name '{}'".format(name)) return DomainStates(self._hass, name) + def _collect_all(self): + render_info = self._hass.data.get(_RENDER_INFO) + if render_info is not None: + # pylint: disable=protected-access + render_info._all_states = True + def __iter__(self): """Return all states.""" + self._collect_all() return iter( - _wrap_state(state) for state in + _wrap_state(self._hass, state) for state in sorted(self._hass.states.async_all(), key=lambda state: state.entity_id)) def __len__(self): """Return number of states.""" + self._collect_all() return len(self._hass.states.async_entity_ids()) def __call__(self, entity_id): """Return the states.""" - state = self._hass.states.get(entity_id) + state = _get_state(self._hass, entity_id) return STATE_UNKNOWN if state is None else state.state + def __repr__(self): + """Representation of All States.""" + return '