From 6bb4199824179be72de35df79e92e59b543ee10c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 18 Apr 2019 03:16:20 +0100 Subject: [PATCH 001/139] 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 30269be90514bc..68720c2821b439 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 e724f680b609a5..3e447f08f4b7ea 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 002/139] 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 ecc39f8db8f672..ed2ea83afb08c7 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -51,11 +51,11 @@ _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 @@ def resolve_legacy(cls, hass: 'HomeAssistant', domain: str) \ 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 99afd4fdb95b71..2467dae04b963b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -487,7 +487,7 @@ def __init__(self, domain=None, dependencies=None, setup=None, 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 003/139] 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 00000000000000..d60750721ac351 --- /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 9e99d219316aee..25db031e6bf6ec 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -69,6 +69,10 @@ async def async_setup(self, tries=0): 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 @@ async def async_reset(self): # 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 00000000000000..555c16a0be7d32 --- /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 00000000000000..1d6fa2d34b4795 --- /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 855a12e26208fb..5b383afc53dbe5 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 00000000000000..99829c59666d2e --- /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 004/139] 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 1cfbf9e3c5fdb3..8c576f58c14c3b 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 005/139] 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 fe13172d7fe5f0..6ab7d911d472b2 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -14,7 +14,8 @@ 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 477e67ab75a03d..67c767c080bb2f 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_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 @@ 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 00000000000000..2225bb58242658 --- /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 8afa55acc5c6b7..982b840393e151 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 @@ def __init__(self, config, user_id, request_id): 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 ab2907cf661fa3..9edde36f09d716 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 a79dfdd3dca01a..5bec683ccc744e 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -38,7 +38,7 @@ 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 6c50a158cad30b..c147f8492d7439 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 24c7059d5c541e..30a398fccc38a4 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 07db4c47296be5..12731978f57785 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -49,6 +49,7 @@ 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 006/139] 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 aabee41694ce5b..17b5f60cf44c68 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -7,7 +7,7 @@ # 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 @@ 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 @@ def __init__(self, light, data): 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 @@ def is_on(self): """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 @@ async def async_update(self): 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 c1c095f989af11..8cb58a7b74c81c 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 b861e8e2a49416..c4411eb1789029 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 007/139] 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 ab7ada618fed80..53aa21c91c6d41 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,7 +3,9 @@ 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 @@ ATTR_INITIAL = 'initial' ATTR_STEP = 'step' +ATTR_MINIMUM = 'minimum' +ATTR_MAXIMUM = 'maximum' CONF_INITIAL = 'initial' CONF_RESTORE = 'restore' @@ -26,11 +30,19 @@ 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 @@ 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 @@ def state(self): @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 @@ async def async_added_to_hass(self): 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 ef76f9b9eacbf1..fc3f0ad36cb5a4 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 97a39cdeb73b46..4ed303474d52e4 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 008/139] 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 86819ef51a39b8..cb0c50f72fe993 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 68720c2821b439..aa4b5547d2c18c 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 00000000000000..072a05e0dd7748 --- /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 00000000000000..1c5a447b101f8c --- /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 00000000000000..422660f1f64fc9 --- /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 00000000000000..65fd0f7de50836 --- /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 c4411eb1789029..316a21da33a1b6 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 009/139] 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 ed86d52584f87a..6371be2802102d 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -31,6 +31,6 @@ def template_listener(entity_id, from_s, to_s): '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 010/139] 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 90e04db011111f..aa57af55852be4 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 @@ 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 011/139] 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 d60750721ac351..3286f185ea4b1e 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 012/139] Add storage for cacheable homekit entity maps. (#23191) --- .../components/homekit_controller/__init__.py | 28 +++-- .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/connection.py | 98 ++++++++++++--- .../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, 372 insertions(+), 28 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 11026d7e9ac92b..3fa4ade519e402 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -8,9 +8,10 @@ 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 @@ def setup(self): # 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 @@ def setup(self): 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 @@ def get_characteristic_types(self): 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 discovery_dispatch(service, discovery_info): 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 1cd66896fe2985..a6c5ac8b36d56c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -152,7 +152,7 @@ async def async_step_discovery(self, discovery_info): "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 2ca568b547fd3e..2b82370d187fb2 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -4,11 +4,10 @@ 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 @@ def __init__(self, hass, host, port, model, hkid, config_num, config): 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 @@ def __init__(self, hass, host, port, model, hkid, config_num, config): 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() + 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'] - def accessory_setup(self): + # 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.model.services import ServicesTypes from homekit.exceptions import AccessoryDisconnectedError - self.pairing.pairing_data['AccessoryIP'] = self.host - self.pairing.pairing_data['AccessoryPort'] = self.port - 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 @@ def accessory_setup(self): 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 @@ def device_config_callback(self, callback_data): 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 @@ async def put_characteristics(self, characteristics): 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 de9663f1202522..f112737ca24140 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -3,6 +3,7 @@ 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 00000000000000..4a7c0a8057bc6e --- /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 5ad197f8294d45..5d85fba6ae3a79 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 0831cd5b780c2e..a7e449ddbe4bd4 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 @@ 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 62c741b4eaab36..da4176e1edc598 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 00000000000000..43b8cba885afcb --- /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 013/139] 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 1437685692b699..a56671c9dcd3a6 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -6,7 +6,6 @@ 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 014/139] 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 10370c42c6645e..1845ae8e999203 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -167,6 +167,11 @@ async def async_configure(self): 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 061541d4dae4e5..b5509b1d559ca1 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -7,6 +7,7 @@ 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 @@ def cluster_command(self, tsn, command_id, args): 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 435ab25acc60df..74e3c7bcc46aca 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -200,10 +200,50 @@ def add_cluster_channel(self, cluster_channel): 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 @@ async def async_configure(self): 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 @@ async def async_initialize(self, from_cache=False): 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 17c7c6f878f193..4a16bfe5004350 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -259,17 +259,25 @@ async def async_device_initialized(self, device, is_new_join): """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 @@ async def async_device_initialized(self, device, is_new_join): 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 015/139] 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 62336ebf126264..00000000000000 --- 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 016/139] 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 24e6f4f390d952..4f655e692f74dc 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 017/139] 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 aa4b5547d2c18c..e5d009cc7a5361 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 22c85723cb87e2..bad39a1cb97ff5 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 d8e4e5c4da6142..3d2c3a5e9119cb 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 018/139] 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 00000000000000..c96ea3fd09feea --- /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 019/139] 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 00000000000000..df056f94e85fa0 --- /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 020/139] 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 00000000000000..835f6402481435 --- /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 00000000000000..736b0ec71c3f8e --- /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 021/139] 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 00000000000000..f4c31420f9a89f --- /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 022/139] 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 1460181ddc22f0..c0dd231ef95161 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 a39bbc01ea1fda..4d77101cf0dbf1 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 7436bbd6ea4043..938e9c8e3249f8 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 bb0f5f932aeaad..f2e5f0b837a402 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 c168185c9b3c38..2545d47c82532d 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 7c926a5a879a54..83e6ea2533b180 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 2514db6314d81b..b555f98d883b2e 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -3,12 +3,13 @@ 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 c2a72ebd5090ed..059231cf95465e 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -61,26 +61,22 @@ def domain(self) -> str: """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 00000000000000..9765eff1d36eb0 --- /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 023/139] 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 0c3b6b313f1f0f..3709bc476f5bc5 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -55,13 +55,13 @@ def __init__(self, config): 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 024/139] 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 411d529b33f29b..0527904a0836cd 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -3,7 +3,6 @@ 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 @@ def device_info(self): '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 025/139] 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 | 157 ++++++++++++------ requirements_all.txt | 2 +- 4 files changed, 109 insertions(+), 55 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e5d009cc7a5361..a6dd61e4ffbab2 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 ac589de841ac24..c65204d78e8c71 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 7b47a388325845..68374ed59b9baf 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 @@ DEFAULT_NAME = 'Vizio SmartCast' DEFAULT_VOLUME_STEP = 1 +DEFAULT_DEVICE_CLASS = 'tv' DEVICE_ID = 'pyvizio' DEVICE_NAME = 'Python Vizio' @@ -30,36 +43,71 @@ 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 - -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)), -}) +COMMON_SUPPORTED_COMMANDS = ( + SUPPORT_SELECT_SOURCE | + SUPPORT_TURN_ON | + SUPPORT_TURN_OFF | + SUPPORT_VOLUME_MUTE | + SUPPORT_VOLUME_SET | + SUPPORT_VOLUME_STEP +) + +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 @@ def update(self): 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 @@ def update(self): @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 @@ def select_source(self, source): 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 316a21da33a1b6..2fb55c9ddd57ab 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 026/139] 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 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/alexa/services.yaml b/homeassistant/components/alexa/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/api/services.yaml b/homeassistant/components/api/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/apns/services.yaml b/homeassistant/components/apns/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/arlo/services.yaml b/homeassistant/components/arlo/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/blackbird/services.yaml b/homeassistant/components/blackbird/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/bluesound/services.yaml b/homeassistant/components/bluesound/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/bluetooth_tracker/services.yaml b/homeassistant/components/bluetooth_tracker/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/channels/services.yaml b/homeassistant/components/channels/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/cloudflare/services.yaml b/homeassistant/components/cloudflare/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/config/services.yaml b/homeassistant/components/config/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/configurator/services.yaml b/homeassistant/components/configurator/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/demo/services.yaml b/homeassistant/components/demo/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 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 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/dominos/services.yaml b/homeassistant/components/dominos/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/duckdns/services.yaml b/homeassistant/components/duckdns/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/ecobee/services.yaml b/homeassistant/components/ecobee/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/econet/services.yaml b/homeassistant/components/econet/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/emulated_hue/services.yaml b/homeassistant/components/emulated_hue/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/epson/services.yaml b/homeassistant/components/epson/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/facebox/services.yaml b/homeassistant/components/facebox/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/flux/services.yaml b/homeassistant/components/flux/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/generic_thermostat/services.yaml b/homeassistant/components/generic_thermostat/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/harmony/services.yaml b/homeassistant/components/harmony/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/html5/services.yaml b/homeassistant/components/html5/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/hue/services.yaml b/homeassistant/components/hue/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/ifttt/services.yaml b/homeassistant/components/ifttt/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/joaoapps_join/services.yaml b/homeassistant/components/joaoapps_join/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/kodi/services.yaml b/homeassistant/components/kodi/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/lifx/services.yaml b/homeassistant/components/lifx/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/local_file/services.yaml b/homeassistant/components/local_file/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/mill/services.yaml b/homeassistant/components/mill/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/mobile_app/services.yaml b/homeassistant/components/mobile_app/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/monoprice/services.yaml b/homeassistant/components/monoprice/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/mysensors/services.yaml b/homeassistant/components/mysensors/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/neato/services.yaml b/homeassistant/components/neato/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/ness_alarm/services.yaml b/homeassistant/components/ness_alarm/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/nuheat/services.yaml b/homeassistant/components/nuheat/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/nuimo_controller/services.yaml b/homeassistant/components/nuimo_controller/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/nuki/services.yaml b/homeassistant/components/nuki/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/onkyo/services.yaml b/homeassistant/components/onkyo/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/onvif/services.yaml b/homeassistant/components/onvif/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/pilight/services.yaml b/homeassistant/components/pilight/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/rest_command/services.yaml b/homeassistant/components/rest_command/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/snapcast/services.yaml b/homeassistant/components/snapcast/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/songpal/services.yaml b/homeassistant/components/songpal/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/telegram/services.yaml b/homeassistant/components/telegram/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/universal/services.yaml b/homeassistant/components/universal/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/websocket_api/services.yaml b/homeassistant/components/websocket_api/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 9765eff1d36eb0..bb04d2fc13f025 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 027/139] 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 aacb0a711a53f1..0604c2fada4535 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 028/139] 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 e2b2c54fd93155..9fd0629fcb269e 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 2fb55c9ddd57ab..57f8d813af11af 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 029/139] 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 bb04d2fc13f025..4be366b3d55781 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 030/139] 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 e226e966b09667..3de53145cfce55 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -69,6 +69,7 @@ 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 @@ def embedded_broker_deprecated(value): 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 @@ async def _attributes_subscribe_topics(self): """(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 @@ def attributes_message_received(msg: Message) -> None: _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 027135e8a7a877..45267484211889 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 031/139] 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 9ad9083506dec7..be739b61809839 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 032/139] 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 99aa68d19756aa..8b116210a10134 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -9,7 +9,7 @@ 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_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 @@ 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 @@ def _setup_from_config(self, config): 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 @@ async def _subscribe_topics(self): 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 @@ def state_received(msg): 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 @@ def speed_received(msg): 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 @@ async def async_turn_on(self, speed: str = None, **kwargs) -> None: """ 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 @@ async def async_turn_off(self, **kwargs) -> None: """ 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 @@ async def async_set_speed(self, speed: str) -> None: 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 @@ async def async_oscillate(self, oscillating: bool) -> None: 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 b7f8b8338a0d57..c00de8522b958c 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -4,12 +4,14 @@ 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 033/139] 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 34faffd202821d..af0a28aa34a8b6 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -20,6 +20,8 @@ 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 @@ async def async_added_to_hass(self): 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 2a4d0fef09de6b..03ad27e604886e 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 034/139] 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 ae4b3322b8e278..5895d52e9dce97 100644 --- a/homeassistant/components/mqtt/vacuum.py +++ b/homeassistant/components/mqtt/vacuum.py @@ -313,7 +313,7 @@ def message_received(msg): 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 4140177a929dcf..78ca45a792fcd8 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 035/139] 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 f24b943f188548..d256f56e7fe0ef 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 @@ async def async_stop(event): 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 036/139] 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 f9bae50698be79..b8df1bbaaf1eb3 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -33,6 +33,8 @@ 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 @@ def state(self): @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 @@ async def async_update(self): # 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 @@ async def async_update(self): 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 @@ async def _async_update(self): 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 037/139] 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 2fdb859c320934..c8c0f6c9f19b0c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -16,11 +16,12 @@ 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_handle_core_service(call): 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 00000000000000..d19ca23799ae2d --- /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 05c183ccd608d1..85ae6473562f61 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 @@ @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 e4132562c31e39..9656346cd2c061 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_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 09a98edc14804b..90953d634c315f 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,9 +5,9 @@ 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 @@ @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 @@ async def async_discovery_start_handler(event): 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 7eddc639690a02..aae1f31d486490 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -81,6 +81,14 @@ def get_addon_info(self, addon): 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 0ba83f1ca1bd23..824dee86fadaa0 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -20,7 +20,7 @@ @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 23095064d558aa..24782e457993fe 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 00000000000000..0591521865944c --- /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 3e7b9e95d92432..372d567c021ddd 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 f1f148f8495c1f..7b8fad3ec09066 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 038/139] 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 072a05e0dd7748..79ebf01ed613da 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -6,9 +6,9 @@ _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 1c5a447b101f8c..10eddb1e0419a2 100644 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -6,7 +6,7 @@ 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 @@ 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 422660f1f64fc9..373df656b256c4 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -3,7 +3,7 @@ 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 @@ 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 039/139] 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 2467dae04b963b..46e30187d45511 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 5d85fba6ae3a79..430032512180c6 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -15,7 +15,7 @@ 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 040/139] 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 3fa4ade519e402..1b1c7b96b5835b 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 041/139] 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 ca73c6d56bb5e3..496ab9199c3487 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 042/139] 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 2a883e33da68d7..9e31887cebb81a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -175,8 +175,9 @@ async def async_update(self): 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 043/139] 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 1600227bfe2acc..39d1c2dd370105 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -25,6 +25,7 @@ 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 @@ async def _async_update(self, now=None): 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 57bd1383363b63..5c859fefb71605 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 57f8d813af11af..c1e381e760bd14 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 044/139] 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 2b82370d187fb2..af438c68164169 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -101,25 +101,27 @@ def setup(self): 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 @@ def refresh_entity_map(self, config_num): 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 045/139] 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 3286f185ea4b1e..b9921a9a01fbf5 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -5,7 +5,7 @@ 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 99829c59666d2e..38eb3d8c55b807 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 046/139] Drop unnecessary block_till_done, improve tests (#23246) --- .../mqtt/test_alarm_control_panel.py | 684 +++++++++--------- 1 file changed, 337 insertions(+), 347 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 6efaedd270bab7..882f748fe4c7a8 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 @@ 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] - entity_id = 'alarm_control_panel.test' - assert STATE_UNKNOWN == \ - self.hass.states.get(entity_id).state - - 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 - - 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, { +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', - 'name': 'test', - 'state_topic': 'alarm/state', - 'command_topic': 'alarm/command', + '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', + } + }) - fire_mqtt_message(self.hass, 'alarm/state', 'unsupported state') - self.hass.block_till_done() - assert STATE_UNKNOWN == self.hass.states.get(entity_id).state + entity_id = 'alarm_control_panel.test' - 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', - } - }) + assert STATE_UNKNOWN == \ + hass.states.get(entity_id).state - 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) + 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 - def test_arm_home_not_publishes_mqtt_with_invalid_code_when_req(self): - """Test not publishing of MQTT messages with invalid. - 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_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', + } + }) - 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 + entity_id = 'alarm_control_panel.test' - def test_arm_home_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages. + assert STATE_UNKNOWN == \ + hass.states.get(entity_id).state - 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_fire_mqtt_message(hass, 'alarm/state', 'unsupported state') + assert STATE_UNKNOWN == hass.states.get(entity_id).state - 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) - 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', - } - }) +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', + } + }) - 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) + 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) - 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_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid. - 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 = 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 + } + }) - def test_arm_away_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages. + 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 - 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_home_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages. - 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', - } - }) + 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 + } + }) - 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) + 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) - def test_arm_night_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_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', + } + }) - 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 + 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) - 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_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid code. - 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 = 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 + } + }) - 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', - } - }) + 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 - 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_away_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages. - 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 }}\"}', - } - }) + 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 + } + }) - 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) + 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) - def test_disarm_publishes_mqtt_when_code_not_req(self): - """Test publishing of MQTT messages while disarmed. - 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 - } - }) +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', + } + }) - common.alarm_disarm(self.hass) - self.hass.block_till_done() - self.mock_publish.async_publish.assert_called_once_with( - 'alarm/command', 'DISARM', 0, False) + 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) - 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_not_publishes_mqtt_with_invalid_code_when_req( + hass, mqtt_mock): + """Test not publishing of MQTT messages with invalid code. - 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 = 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 + } + }) - 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' - } - }) + 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 - 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_arm_night_publishes_mqtt_when_code_not_req(hass, mqtt_mock): + """Test publishing of MQTT messages. - state = self.hass.states.get('alarm_control_panel.test') - assert STATE_UNAVAILABLE != state.state + 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 + } + }) - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() + 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 - 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' - } - }) +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) + + +async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): + """Test publishing of MQTT messages while disarmed. + + 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 }}\"}', + } + }) + + 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) + + +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) - fire_mqtt_message(self.hass, 'availability-topic', 'good') + +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 047/139] Drop unnecessary block_till_done, improve tests (#23247) --- tests/components/mqtt/test_binary_sensor.py | 542 +++++++++----------- 1 file changed, 254 insertions(+), 288 deletions(-) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 3e6e36cd05047d..2c8faf665495cc 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) - - -class TestSensorMQTT(unittest.TestCase): - """Test the MQTT sensor.""" - - 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) - - def tearDown(self): # pylint: disable=invalid-name - """Stop everything that was started.""" - self.hass.stop() - - 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', - } - }) - - 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 - - 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 - - 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%}' - } - }) - - state = self.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 - - 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 = 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', - } - }) - - state = self.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 - - 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 = 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() - - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE != state.state - - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - - state = self.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' - } - }) - - state = self.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 - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('binary_sensor.test') - assert STATE_UNAVAILABLE == state.state - - 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' - } - }) - - events = [] - - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) - - 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) - - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.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, binary_sensor.DOMAIN, { - binary_sensor.DOMAIN: { - 'platform': 'mqtt', - 'name': 'test', - 'state_topic': 'test-topic', - 'payload_on': 'ON', - 'payload_off': 'OFF', - 'force_update': True - } - }) - - events = [] - - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) - - 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) - - fire_mqtt_message(self.hass, 'test-topic', 'ON') - self.hass.block_till_done() - assert 2 == len(events) - - 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 = [] - - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) - - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) - - 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) - - 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) + MockConfigEntry, async_fire_mqtt_message, async_fire_time_changed, + async_mock_mqtt_component, mock_registry) + + +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', + } + }) + + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == state.state + + async_fire_mqtt_message(hass, 'test-topic', 'ON') + state = hass.states.get('binary_sensor.test') + assert STATE_ON == state.state + + async_fire_mqtt_message(hass, 'test-topic', 'OFF') + state = hass.states.get('binary_sensor.test') + assert STATE_OFF == 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%}' + } + }) + + state = 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_ON == state.state + + async_fire_mqtt_message(hass, 'test-topic', '') + state = 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', + } + }) + + state = hass.states.get('binary_sensor.test') + assert 'motion' == 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, binary_sensor.DOMAIN, { + binary_sensor.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'device_class': 'abc123', + 'state_topic': 'test-topic', + } + }) + + state = hass.states.get('binary_sensor.test') + assert state is None + + +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', + } + }) + + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE != state.state + + +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 = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'online') + + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + + state = 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' + } + }) + + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + + state = hass.states.get('binary_sensor.test') + assert STATE_UNAVAILABLE == state.state + + +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' + } + }) + + 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() + assert 1 == len(events) + + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + assert 1 == 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 + } + }) + + 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() + assert 1 == len(events) + + async_fire_mqtt_message(hass, 'test-topic', 'ON') + await hass.async_block_till_done() + assert 2 == 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 048/139] 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 5726a64ba11a8f..9774ba81b51ce4 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 @@ 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 049/139] 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 a8e1ae6111ecbe..15321301997983 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -4,9 +4,6 @@ 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 @@ 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 @@ }} -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 050/139] 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 cfb0d75d1c7210..7b0157aeb7e2a9 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 1e325cec5abf93..e4f2a3b7ef85bf 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 2db2bd06aa28ac..658357b80633ea 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 051/139] 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 | 198 +++++++++++++----- 15 files changed, 289 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 9e24b619460f39..aedd71bd9ac1d7 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -102,7 +102,7 @@ def should_expose(entity): 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 1286832c0c7abf..5002286edb9376 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -5,7 +5,7 @@ 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 6ab7d911d472b2..bf9b78335274ef 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -19,7 +19,7 @@ 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 @@ }) -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 b0244f6b1fb162..0e2abae15b0b73 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -3,7 +3,7 @@ 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 @@ async def async_initialize(self): 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 @@ async def async_initialize(self): 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 @@ def google_enabled(self): 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 2d3a19afa1302a..c8078b7d9d2285 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -20,7 +20,7 @@ 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 @@ 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 67c767c080bb2f..07506611109e59 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -28,13 +28,13 @@ 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 @@ 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_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 @@ (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 2225bb58242658..3aef1e9408d5e9 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 @@ def __init__(self, code, msg): """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 982b840393e151..71cce9de500752 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -19,12 +19,12 @@ 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 @@ def query_serialize(self): 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 11d8a3841650d7..d385d742c7d180 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -10,12 +10,12 @@ 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 is_exposed(entity) -> bool: 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 9edde36f09d716..37f35edf64528f 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 5bec683ccc744e..bad186a4edb087 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -19,6 +19,7 @@ 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 @@ 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 @@ def can_execute(self, command, params): """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 @@ def query_attributes(self): 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 @@ def query_attributes(self): """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 @@ def query_attributes(self): """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 @@ def query_attributes(self): 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 @@ def query_attributes(self): """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 @@ def query_attributes(self): """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 @@ def query_attributes(self): '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 @@ def query_attributes(self): 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 @@ def query_attributes(self): """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 @@ def query_attributes(self): 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 @@ def query_attributes(self): 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 @@ def query_attributes(self): 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 @@ async def execute(self, command, data, params): 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 3a07e52724f2a8..08ab5324b970e1 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 c147f8492d7439..4aebc5679a0fd8 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -9,7 +9,8 @@ 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 30a398fccc38a4..8ea6f26553de7a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -22,7 +22,6 @@ 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 12731978f57785..8b7f0788f34411 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -19,7 +19,8 @@ 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 @@ BASIC_CONFIG = helpers.Config( should_expose=lambda state: True, - allow_unlock=False ) REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf' @@ -41,9 +41,15 @@ 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,7 +922,7 @@ async def test_lock_unlock_unlock(hass): trt = trait.LockUnlockTrait(hass, State('lock.front_door', lock.STATE_LOCKED), - BASIC_CONFIG) + PIN_CONFIG) assert trt.sync_attributes() == {} @@ -902,22 +930,29 @@ async def test_lock_unlock_unlock(hass): '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) + assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False}) - assert trt.sync_attributes() == {} + calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK) - assert trt.query_attributes() == { - 'isLocked': True - } + # 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 - assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False}) + # 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 - calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK) - await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}) + 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 052/139] 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 cc629b2165deeb..56152870cc6ec6 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 053/139] Drop unnecessary block_till_done, improve tests (#23252) --- tests/components/mqtt/test_sensor.py | 704 +++++++++++++-------------- 1 file changed, 331 insertions(+), 373 deletions(-) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 45267484211889..db8f7620864597 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 @@ 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) - - -class TestSensorMQTT(unittest.TestCase): - """Test the MQTT sensor.""" - - 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) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - 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') - - assert '100' == state.state - assert 'fav unit' == \ - state.attributes.get('unit_of_measurement') - - @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 - } - }) - - state = self.hass.states.get('sensor.test') - assert 'unknown' == 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() - - # Value was set correctly. - state = self.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() - - # Value is not yet expired - state = self.hass.states.get('sensor.test') - assert '100' == state.state - - # Next message resets timer - mock_utcnow.return_value = now - fire_mqtt_message(self.hass, 'test-topic', '101') - self.hass.block_till_done() - - # Value was updated correctly. - state = self.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() - - # Value is not yet expired - state = self.hass.states.get('sensor.test') - assert '101' == 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 - - 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 }}' - } - }) - - 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 - - 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' - } - }) - - events = [] - - @ha.callback - def callback(event): - events.append(event) - - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) - - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 1 == len(events) - - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.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 = [] - - @ha.callback - def callback(event): - events.append(event) - - self.hass.bus.listen(EVENT_STATE_CHANGED, callback) - - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 1 == len(events) - - fire_mqtt_message(self.hass, 'test-topic', '100') - self.hass.block_till_done() - assert 2 == 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' - } - }) - - 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() - - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE != state.state - - fire_mqtt_message(self.hass, 'availability-topic', 'offline') - self.hass.block_till_done() - - state = self.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' - } - }) - - state = self.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 - - fire_mqtt_message(self.hass, 'availability-topic', 'nogood') - self.hass.block_till_done() - - state = self.hass.states.get('sensor.test') - assert STATE_UNAVAILABLE == state.state - - 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_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' - } - }) - - 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') - - @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' - } - }) - - fire_mqtt_message(self.hass, 'test-topic', '[ "list", "of", "things"]') - self.hass.block_till_done() - state = self.hass.states.get('sensor.test') - - assert state.attributes.get('val') is None - assert mock_logger.warning.called - - @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() - - state = self.hass.states.get('sensor.test') - assert state.attributes.get('val') is None - assert mock_logger.warning.called - assert mock_logger.debug.called - - 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' - } - }) - - 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 - - def test_invalid_device_class(self): - """Test device_class option with invalid value.""" - with assert_setup_component(0): - assert setup_component(self.hass, 'sensor', { - 'sensor': { - 'platform': 'mqtt', - 'name': 'Test 1', - 'state_topic': 'test-topic', - 'device_class': 'foobarnotreal' - } - }) - - def test_valid_device_class(self): - """Test device_class option with valid values.""" - assert setup_component(self.hass, 'sensor', { - 'sensor': [{ - 'platform': 'mqtt', - 'name': 'Test 1', - 'state_topic': 'test-topic', - 'device_class': 'temperature' - }, { - 'platform': 'mqtt', - 'name': 'Test 2', - 'state_topic': 'test-topic', - }] - }) - self.hass.block_till_done() - - state = self.hass.states.get('sensor.test_1') - assert state.attributes['device_class'] == 'temperature' - state = self.hass.states.get('sensor.test_2') - assert 'device_class' not in state.attributes + MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, + async_fire_time_changed, mock_registry) + + +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' + } + }) + + 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') + + +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 + } + }) + + state = hass.states.get('sensor.test') + assert 'unknown' == state.state + + 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() + + # Value was set correctly. + state = hass.states.get('sensor.test') + assert '100' == state.state + + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is not yet expired + state = hass.states.get('sensor.test') + assert '100' == state.state + + # 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 was updated correctly. + state = hass.states.get('sensor.test') + assert '101' == state.state + + # Time jump +3s + now = now + timedelta(seconds=3) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is not yet expired + state = hass.states.get('sensor.test') + assert '101' == state.state + + # Time jump +2s + now = now + timedelta(seconds=2) + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Value is expired now + state = 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 }}' + } + }) + + async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') + state = hass.states.get('sensor.test') + + assert '100' == state.state + + +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 = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_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) + + +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 + } + }) + + events = [] + + @ha.callback + def callback(event): + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() + assert 1 == len(events) + + async_fire_mqtt_message(hass, 'test-topic', '100') + await hass.async_block_till_done() + assert 2 == len(events) + + +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 = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'online') + + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'offline') + + state = 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' + } + }) + + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'good') + + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE != state.state + + async_fire_mqtt_message(hass, 'availability-topic', 'nogood') + + state = hass.states.get('sensor.test') + assert STATE_UNAVAILABLE == state.state + + +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' + } + }) + + async_fire_mqtt_message(hass, 'test-topic', '{ "val": "100" }') + state = hass.states.get('sensor.test') + + assert '100' == \ + state.attributes.get('val') + + +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' + } + }) + + async_fire_mqtt_message(hass, 'test-topic', '[ "list", "of", "things"]') + state = hass.states.get('sensor.test') + + assert state.attributes.get('val') is None + assert 'JSON result was not a dictionary' in caplog.text + + +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' + } + }) + + async_fire_mqtt_message(hass, 'test-topic', 'This is not JSON') + + state = hass.states.get('sensor.test') + assert state.attributes.get('val') is None + assert 'Erroneous JSON: This is not JSON' in caplog.text + + +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 054/139] 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 e69de29bb2d1d6..68eaf6ac377b2f 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 d4985258368b8a..ef944d75efc9d3 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 055/139] 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 cb0c50f72fe993..ac674b9fada424 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 a6dd61e4ffbab2..c2cd1f4553a067 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 00000000000000..43ca0abc2a0514 --- /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 00000000000000..140caf51936b76 --- /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 00000000000000..c66c6b52e0c3d4 --- /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 91b49283be8a23..e9a8d0749b0a46 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -7,6 +7,7 @@ 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 c1e381e760bd14..5fa04616a8ef13 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 2e5012d76e0509..383aec75958861 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 f71b8944d7cb24..63b0ef737e23c4 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -48,6 +48,7 @@ '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 00000000000000..46fbe073ab0a39 --- /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 00000000000000..d0398d448e9c76 --- /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 00000000000000..47efe8d03c9e9b --- /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 00000000000000..0defb1137470de --- /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 056/139] 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 c00de8522b958c..bd19ec526a3a11 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 057/139] 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 ba72db52a8f770..42513a2e9007d2 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 cd274079e01453..180b7af5bef632 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -33,15 +33,12 @@ def record_calls2(*args): '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 @@ def record_calls2(*args): 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 @@ def record_calls2(*args): '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 @@ def record_calls2(*args): 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 @@ def record_calls2(*args): 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 058/139] Drop unnecessary block_till_done, improve tests for MQTT Cover tests (#23255) --- tests/components/mqtt/test_cover.py | 2105 +++++++++++++-------------- 1 file changed, 1048 insertions(+), 1057 deletions(-) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 47681e0de101b6..5ca8a1aa649c19 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 @@ 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.""" - - 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) - - def tearDown(self): # pylint: disable=invalid-name - """Stop down everything that was started.""" - self.hass.stop() - - 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 = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - fire_mqtt_message(self.hass, 'state-topic', STATE_CLOSED) - self.hass.block_till_done() - - 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() - - state = self.hass.states.get('cover.test') - assert STATE_OPEN == state.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' - } - })) - - state = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - fire_mqtt_message(self.hass, 'get-position-topic', '0') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - assert STATE_CLOSED == 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 - - 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 = self.hass.states.get('cover.test') - assert STATE_UNKNOWN == state.state - - fire_mqtt_message(self.hass, 'state-topic', '10000') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - assert STATE_OPEN == state.state - - fire_mqtt_message(self.hass, 'state-topic', '99') - self.hass.block_till_done() - - 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 }}' - } - })) - - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_UNKNOWN, state.state) - - fire_mqtt_message(self.hass, 'get-position-topic', '10000') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_OPEN, state.state) - - fire_mqtt_message(self.hass, 'get-position-topic', '5000') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual(STATE_OPEN, state.state) - - fire_mqtt_message(self.hass, 'get-position-topic', '99') - self.hass.block_till_done() - - state = self.hass.states.get('cover.test') - self.assertEqual(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) - - self.hass.services.call( +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' + } + }) + + state = 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_CLOSED) + + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state + + async_fire_mqtt_message(hass, 'state-topic', STATE_OPEN) + + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state + + +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 = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + 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 + + async_fire_mqtt_message(hass, 'get-position-topic', '100') + + state = 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 %}' + } + }) + + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + + async_fire_mqtt_message(hass, 'state-topic', '10000') + + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state + + async_fire_mqtt_message(hass, 'state-topic', '99') + + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.state + + +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 = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + + async_fire_mqtt_message(hass, 'get-position-topic', '10000') + + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state + + async_fire_mqtt_message(hass, 'get-position-topic', '5000') + + state = hass.states.get('cover.test') + assert STATE_OPEN == state.state + + async_fire_mqtt_message(hass, 'get-position-topic', '99') + + state = hass.states.get('cover.test') + assert STATE_CLOSED == state.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, + } + }) + + 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() - - 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 - - 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 - - self.hass.services.call( + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'CLOSE', 0, False) + state = hass.states.get('cover.test') + assert STATE_CLOSED == 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 + } + }) + + 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() - - 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 - - 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 - - self.hass.services.call( + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'OPEN', 2, False) + state = 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 + } + }) + + 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() - - 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 - - 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 - - self.hass.services.call( + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'CLOSE', 2, False) + state = 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 + } + }) + + 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() - - 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 - - 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) - - 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 - - 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 - - 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 - - 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 - - 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' - } - }) - - 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 - - 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 - - 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 - - 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 - - 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 - - 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' - } - }) - - 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 - - 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' - } - }) - - self.hass.services.call( + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'command-topic', 'STOP', 2, False) + state = hass.states.get('cover.test') + assert STATE_UNKNOWN == state.state + + +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' + } + }) + + 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) + + 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 + + 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 + + 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 + + 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 + + +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' + } + }) + + 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) + + 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 + + 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 + + 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 + + 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 + + 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 + + +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' + } + }) + + 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() - - self.mock_publish.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( + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 100})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'set-position-topic', '38', 0, False) + + +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() - - self.mock_publish.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'] - - 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 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' - } - }) - - state_attributes_dict = self.hass.states.get( - 'cover.test').attributes - assert 'current_tilt_position' in state_attributes_dict - - 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' - } - }) - - self.hass.services.call( + {ATTR_ENTITY_ID: 'cover.test', ATTR_POSITION: 62})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'position-topic', 62, 0, False) + + +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' + } + }) + + assert 240 == hass.states.get( + 'cover.test').attributes['supported_features'] + + +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' + } + }) + + assert 251 == hass.states.get( + 'cover.test').attributes['supported_features'] + + +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' + } + }) + + 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() - - self.mock_publish.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( + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 0, 0, False) + + +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() - - self.mock_publish.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() - - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 0 == current_cover_tilt_position - - fire_mqtt_message(self.hass, 'tilt-status-topic', '50') - self.hass.block_till_done() - - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 50 == current_cover_tilt_position - - 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 - } - }) - - 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 - - fire_mqtt_message(self.hass, 'tilt-status-topic', '50') - self.hass.block_till_done() - - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 100 == current_cover_tilt_position - - fire_mqtt_message(self.hass, 'tilt-status-topic', '25') - self.hass.block_till_done() - - current_cover_tilt_position = self.hass.states.get( - 'cover.test').attributes['current_tilt_position'] - assert 50 == 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 - } - }) - - self.hass.services.call( + {ATTR_ENTITY_ID: 'cover.test'})) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 125, 0, False) + + +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 + } + }) + + async_fire_mqtt_message(hass, 'tilt-status-topic', '0') + + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 0 == current_cover_tilt_position + + 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 + + +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 + } + }) + + async_fire_mqtt_message(hass, 'tilt-status-topic', '0') + + current_cover_tilt_position = hass.states.get( + 'cover.test').attributes['current_tilt_position'] + assert 0 == current_cover_tilt_position + + 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 + + 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 + + +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() - - self.mock_publish.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( + blocking=True)) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 50, 0, False) + + +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() - - self.mock_publish.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') - - 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 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) - - assert 56 == mqtt_cover.find_percentage_in_range(44) - assert 56 == mqtt_cover.find_percentage_in_range(44, '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') - - 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 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) - - assert 120 == mqtt_cover.find_in_range_from_percent(40) - assert 120 == mqtt_cover.find_in_range_from_percent(40, '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') - - 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 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' - } - }) - - state = self.hass.states.get('cover.test') - assert STATE_UNAVAILABLE != state.state - - 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 - - fire_mqtt_message(self.hass, 'availability-topic', 'online') - self.hass.block_till_done() - - 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() - - state = self.hass.states.get('cover.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, 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 - - fire_mqtt_message(self.hass, 'availability-topic', 'good') - self.hass.block_till_done() - - 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() - - state = self.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', - } - }) - - state = self.hass.states.get('cover.test') - assert 'garage' == state.attributes.get('device_class') - - 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', - } - }) - - state = self.hass.states.get('cover.test') - assert state is None + blocking=True)) + await hass.async_block_till_done() + + mqtt_mock.async_publish.assert_called_once_with( + 'tilt-command-topic', 25, 0, False) + + +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) + + 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_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 40 == mqtt_cover.find_percentage_in_range(120) + assert 40 == 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) + + assert 56 == mqtt_cover.find_percentage_in_range(44) + assert 56 == mqtt_cover.find_percentage_in_range(44, 'cover') + + +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 60 == mqtt_cover.find_percentage_in_range(120) + assert 60 == mqtt_cover.find_percentage_in_range(120, '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) + + assert 44 == mqtt_cover.find_in_range_from_percent(44) + assert 44 == mqtt_cover.find_in_range_from_percent(44, 'cover') + + +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) + + assert 120 == mqtt_cover.find_in_range_from_percent(40) + assert 120 == mqtt_cover.find_in_range_from_percent(40, 'cover') + + +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) + + 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_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) + + assert 120 == mqtt_cover.find_in_range_from_percent(60) + assert 120 == mqtt_cover.find_in_range_from_percent(60, 'cover') + + +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' + } + }) + + state = hass.states.get('cover.test') + assert STATE_UNAVAILABLE != state.state + + +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 = hass.states.get('cover.test') + assert STATE_UNAVAILABLE == state.state + + 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 + + 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 + + +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 059/139] 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 7917803aa07b16..dfd05424ca7e84 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 060/139] 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 9ff00ed1c23a51..4cb4204f274e0b 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -672,7 +672,7 @@ def make(self): 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 @@ def supported_features(self): # 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 @@ def supported_features(self): 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 061/139] 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 4821c39ff32878..ae91178e4c457f 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 5fa04616a8ef13..640daf76cfd7f5 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 383aec75958861..2b2448fb447981 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 062/139] 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 | 226 +++++++++++++----- 6 files changed, 201 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 2977345f97d0ef..97b5393561452c 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 0da9db31bb28f6..8821591df207e8 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 @@ 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 @@ async def async_added_to_hass(self): 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 640daf76cfd7f5..b5510c421b0ae9 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 2b2448fb447981..6c58aa863f6985 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 211153b1cc7db6..496f143d51f1b7 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 dd36c2c013de02..0870f82b3ff00a 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 set_signal(): 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] - - 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 - - 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 - - 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 - - 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) - - 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) + # 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 + + +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 + + +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 + + +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 + + +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 + + +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 063/139] 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 9fd0629fcb269e..fb30c09d26b2e0 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 b5510c421b0ae9..bfc2073159892a 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 064/139] 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 fb30c09d26b2e0..c8bc0479f30c73 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 bfc2073159892a..0926f79d7635b7 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 6c58aa863f6985..a093ccdc1adf4e 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 065/139] Add ctags file to .gitignore (#23279) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b486032c741429..75ab19881ac752 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 066/139] 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 9016502b5d3bb0..7731f845005ded 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 1d13936f592c1c..d2f9e90c41a9dd 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 cf0439345dc360..9cc50405bad9c0 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 ec12f6cdac4417..88daadd35aa1c6 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 1d67ddbd5817cc..4d1a55eaa09596 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 0926f79d7635b7..ec9d2347fd1453 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 067/139] 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 310abff94842ba..396a0fb8d3f2ee 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -18,7 +18,7 @@ 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 dc51152f565c79..bb07d9e479f26c 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -12,7 +12,7 @@ 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 3eb24e0f1c608e..cea246af328d26 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 ec9d2347fd1453..7bb24ef8bfbbab 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 a093ccdc1adf4e..e0051c8edafaba 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 068/139] 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 201a5be6c51547..316bf1f4cd8238 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 @@ def device_class(self): @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 @@ def device_class(self): @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 @@ def unit_of_measurement(self): 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 069/139] 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 882f748fe4c7a8..4514e5285aa8f6 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 2c8faf665495cc..70394a62f061c8 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 @@ def callback(event): 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 @@ def callback(event): 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 @@ def callback(event): 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 15321301997983..11e2984cbb36b6 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 5ca8a1aa649c19..8bf136c6f0fff3 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 bd19ec526a3a11..31aebecc23699e 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 dc9299e4a359ac..b0d1de36efea0b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -82,11 +82,11 @@ def test_publish_calls_service(self): 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 @@ def test_service_call_with_template_payload_renders_template(self): 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 @@ def test_all_subscriptions_run_when_decode_fails(self): 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 @@ def test_subscribe_topic(self): 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 @@ def test_subscribe_topic_not_match(self): 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 @@ def test_subscribe_topic_level_wildcard(self): 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 @@ def test_subscribe_topic_level_wildcard_no_subtree_match(self): 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 @@ def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match(self): 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 @@ def test_subscribe_topic_subtree_wildcard_subtree_topic(self): 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 @@ def test_subscribe_topic_subtree_wildcard_root_topic(self): 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 @@ def test_subscribe_topic_subtree_wildcard_no_match(self): 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 @@ def test_subscribe_topic_level_wildcard_and_wildcard_root_topic(self): 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 @@ def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic(self): 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 @@ def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match(self): 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 @@ def test_subscribe_topic_level_wildcard_and_wildcard_no_match(self): 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 @@ def test_subscribe_topic_sys_root(self): 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 @@ def test_subscribe_topic_sys_root_and_wildcard_topic(self): 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 @@ def test_subscribe_topic_sys_root_and_wildcard_subtree_topic(self): '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 @@ def test_subscribe_special_characters(self): 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 @@ def test_mqtt_disconnect_tries_reconnect(self, mock_sleep): 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 @@ def test_restore_all_active_subscriptions_on_reconnect(self): 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 7b0157aeb7e2a9..75fd92dddc0e36 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 e4f2a3b7ef85bf..018f706a1a07c9 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 658357b80633ea..eef91675110188 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 56152870cc6ec6..6328d2b7c1a102 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 db8f7620864597..bcd70b82a2493f 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 @@ def callback(event): 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 @@ def callback(event): 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 180b7af5bef632..95074e95eb30b5 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -33,24 +33,24 @@ def record_calls2(*args): '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 @@ def record_calls2(*args): '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 @@ def record_calls2(*args): 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 dfd05424ca7e84..df6706b01cf2a7 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 070/139] 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 6a22a698b4ca2b..843fd3d13b0b9f 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 7bb24ef8bfbbab..0d0d821e6654bb 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 071/139] 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 8821591df207e8..56e9647df50e0a 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -6,10 +6,10 @@ 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 @@ 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 @@ async def async_mute_volume(self, mute): """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 0870f82b3ff00a..4cf871f5ed0f48 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 @@ 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 072/139] 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 121e210a0a0aa2..e5feedd84215a0 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -149,7 +149,8 @@ async def async_save_to_store(self) -> None: 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 734544b49c7803..9d25ec6d034da6 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 0d0d821e6654bb..b1212d3c63a5fa 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 073/139] 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 18ddcf1f5ffd84..16a3dbd119d3f4 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 859ad26a3ddd6c..0b0b1de4275af4 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -7,40 +7,48 @@ 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 @@ def __init__(self, tv, name, on_script): """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 @@ def name(self): @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 b1212d3c63a5fa..33ed716b3363a5 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 074/139] 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} | 347 +++++---- tests/components/mqtt/test_state_vacuum.py | 685 ++++++++++++++++++ 5 files changed, 1370 insertions(+), 195 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 00000000000000..f69e41985d6196 --- /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 5895d52e9dce97..6321d98fcd7c7b 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 @@ 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 @@ 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 @@ def strings_to_services(strings): 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 @@ def strings_to_services(strings): 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) - - -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) + mqtt.MQTT_JSON_ATTRS_SCHEMA.schema).extend(MQTT_VACUUM_SCHEMA.schema) -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 @@ def _setup_from_config(self, config): 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 @@ def _setup_from_config(self, config): 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 @@ def unique_id(self): 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 @@ def status(self): 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 @@ async def async_turn_on(self, **kwargs): 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 @@ async def async_turn_off(self, **kwargs): 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 @@ async def async_stop(self, **kwargs): 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 @@ async def async_clean_spot(self, **kwargs): 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 @@ async def async_locate(self, **kwargs): 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 @@ async def async_start_pause(self, **kwargs): 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 @@ async def async_return_to_base(self, **kwargs): 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 00000000000000..2e0921ad19dd1e --- /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 78ca45a792fcd8..5a7bf6c2d8b494 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 @@ 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 @@ } -@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_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, + }) + + 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, mock_publish): +async def test_status(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 = """{ @@ -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 00000000000000..0c871fdcfd0a1a --- /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 075/139] 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 5491fffe96989f..0d349f8756e380 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -35,11 +35,18 @@ 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 @@ }, 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 8f61e7a44b5e74..4ba3afb07b42d8 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 076/139] 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 555c16a0be7d32..30a439f92e9634 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 38eb3d8c55b807..6259921dcfbc5d 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -48,7 +48,7 @@ } LIGHT_LEVEL_SENSOR_1 = { "state": { - "lightlevel": 0, + "lightlevel": 1, "dark": True, "daylight": True, "lastupdated": "2019-01-01T01:00:00" @@ -141,7 +141,7 @@ } LIGHT_LEVEL_SENSOR_2 = { "state": { - "lightlevel": 100, + "lightlevel": 10001, "dark": True, "daylight": True, "lastupdated": "2019-01-01T01:00:00" @@ -234,7 +234,7 @@ } 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 077/139] 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 00000000000000..9534ad3f696862 --- /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 078/139] 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 a6c5ac8b36d56c..310f187556d73e 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -130,6 +130,11 @@ async def async_step_discovery(self, discovery_info): 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 b1601a1f33e8a6..075bf6ca6cd557 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 da4176e1edc598..2dd42737477877 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -12,9 +12,17 @@ ) +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 079/139] 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 d19ca23799ae2d..7291a87e9544fd 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 24782e457993fe..23095064d558aa 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 080/139] 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 ed2ea83afb08c7..fb2c1bae894106 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 081/139] 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 5684a3c64d1426..16d11e913f7c15 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 33ed716b3363a5..fa5ee0cc221725 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 082/139] 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 b555f98d883b2e..bca419126db909 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 25553be1124970..f0f14ad21a44f6 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 4be366b3d55781..8750f9a69826c9 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 083/139] 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 63e76dd2496906..0079f11447b88a 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -11,6 +11,7 @@ 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 @@ def _entity_func(self) -> Callable[[str, str], bool]: """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 @@ def __init__(self, policy: PolicyType, 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 d2d259fb32ee7d..0d334c4a3ba892 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -3,6 +3,7 @@ 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 test_value(object_id: str, key: str) -> Optional[bool]: 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 d9834758c80215..84178beef8b96d 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 00000000000000..1a339208f4dbcf --- /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 084/139] 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 39d1c2dd370105..b3e2b2833c28df 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -109,26 +109,38 @@ def __init__(self, api, hass, async_see, scan_interval, 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 5c859fefb71605..0f9aa6e8464de7 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 fa5ee0cc221725..10e8e52dceda37 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 085/139] 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 3bef086d70a148..25d6c587277d4b 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 10e8e52dceda37..24371f3d0f6865 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 6f67f93d3e2e16..4f1e3a6eb71332 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,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', From b252d8e2cd89285a0dcb82a77c05894bc7b29fbc Mon Sep 17 00:00:00 2001 From: dreed47 Date: Tue, 23 Apr 2019 14:44:13 -0400 Subject: [PATCH 086/139] 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 e66aad701b7972..0a1f14324f648f 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -113,12 +113,16 @@ def update(self): 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 087/139] 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 aedd71bd9ac1d7..5bbd7bb48fab30 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -106,6 +106,10 @@ def should_expose(entity): 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 088/139] 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 c2cd1f4553a067..b96aae298a5daa 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 22c21fcffbed51..16c09d7ce2d4e0 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 1b184a3774fc35..ff028682739724 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -7,6 +7,7 @@ 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 @@ 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 @@ def __init__(self): 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 @@ async def async_step_user(self, user_input=None): 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 @@ async def async_step_link(self, user_input=None): """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 @@ async def async_step_link(self, user_input=None): 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 @@ async def async_step_link(self, user_input=None): }, ) + # 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 605dd3f530ce23..fcfcad95c127ba 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 3382cd6fe43ba4..a53110b6f0e7da 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -9,6 +9,7 @@ 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 @@ def __init__(self): 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 @@ def update(self): 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 @@ def state_unknown(self): """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 @@ def reset_title(self): 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 @@ def add_games(self, title_id, app_name): 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 @@ def get_device_info(self, status): '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 @@ def media_content_id(self): @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 @@ def media_stop(self): 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 ea69d8c7a8c873..77443b1ee9a013 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 24371f3d0f6865..e53402d49a39b1 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 e0051c8edafaba..4f8050390418da 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 06fe1ef65da8f7..5db3fc2dd818bf 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -7,6 +7,7 @@ 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_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 089/139] 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 cb35833c231a42..1326e46d7d3532 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 @@ def __init__(self, home, device): @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 48e9520a952621..1396493a527a2c 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 @@ def device_class(self): @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 @@ def device_class(self): @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 @@ def device_state_attributes(self): 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 @@ def is_on(self): 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 @@ def device_state_attributes(self): 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 @@ def device_state_attributes(self): 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 5055858e9c78d1..8a2ad8738dffac 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 e572e3d97546ad..381bcf1980e35b 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 0b815d0ec7e4d5..f6da8b27cf751e 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 @@ def __init__(self, home, device, post=None): @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 64721c0a96c5b2..99e98b5a1d2bea 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 @@ async def async_setup(self): 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 @@ async def async_checkbutton(self): 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 @@ async def async_register(self): 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 @@ async def get_state(self): 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 @@ def update_all(self): 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 @@ async def async_reset(self): 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 b67e4114db20d3..e783214a447608 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 @@ def __init__(self, home, device): @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 @@ async def async_turn_off(self, **kwargs): 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 @@ def __init__(self, home, device, channel_index): } @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 @@ def device_state_attributes(self): """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 @@ async def async_turn_on(self, **kwargs): 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 316bf1f4cd8238..4816eacd08fe00 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 @@ def __init__(self, home, device): @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 @@ def icon(self): @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 @@ def unit_of_measurement(self): 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 b96e0c4cf4d60c..9a0d48ac2531ea 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 74b302b18fc33a..9c7d843b4484d0 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 090/139] 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 1ed2bb6db5903d..a7267441cdb570 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 091/139] 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 07506611109e59..815b2bd1bd2aca 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -34,6 +34,7 @@ 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 092/139] 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 418b6ffa89df0c..7e7fb1430cc502 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 ec37d3e5128ffc..a59494023bb68b 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 @@ def __init__(self, config, address_connection): 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 @@ def __init__(self, config, address_connection): 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 @@ def __init__(self, config, address_connection): 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 7123f2d5d0a579..d07fa09c189a05 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 @@ def __init__(self, config, address_connection): 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 653873ba78a373..49cdff5de492cc 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 @@ def __init__(self, config, address_connection): 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 @@ def __init__(self, config, address_connection): 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 48ac8c7266c40d..38b17c80793641 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 @@ def __init__(self, config, address_connection): 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 @@ def __init__(self, config, address_connection): 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 48ae579fbcd7c3..e5a8484e27170b 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 @@ def __init__(self, config, address_connection): 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 @@ def __init__(self, config, address_connection): 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 093/139] 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 0143855db37c8e..21ac6ad833e974 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 b8a2a905dcb00c..c29dfea6737068 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -46,7 +46,7 @@ async def async_added_to_hass(self): 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 e53402d49a39b1..eb6e3cd7beefc6 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 094/139] 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 f33933a2772440..e7e15d2303c220 100755 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -35,6 +35,8 @@ (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 095/139] 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 815b2bd1bd2aca..1bab27bdd1243e 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -50,6 +50,7 @@ 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 @@ 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 8ea6f26553de7a..375f647da22dd3 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.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 096/139] 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 431e711951a9a1..9007cb5c7bed6c 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 eb6e3cd7beefc6..7c9d471894c156 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 097/139] 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 e783214a447608..f4f73104f7c089 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 098/139] 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 bad186a4edb087..ac2f65af058cf7 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -60,6 +60,7 @@ 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_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 @@ def supported(domain, features, device_class): """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 @@ def query_attributes(self): 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 @@ async def execute(self, command, data, params, challenge): 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 @@ async def execute(self, command, data, params, challenge): '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 d75b51df65b680..f3732c12213716 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -143,7 +143,7 @@ }, '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 @@ }, '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 @@ '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 19e1858d4f5cf2..4e2c04e5cf46b2 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 8b7f0788f34411..96ca8d82f5e06b 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 099/139] 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 25d6c587277d4b..a4a08af1236d76 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 7c9d471894c156..a90e15a661643d 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 4f1e3a6eb71332..4de6fa2f042250 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ '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 100/139] 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 cf64363ba503f9..f56ffbfffd23d3 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -58,8 +58,8 @@ 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 101/139] 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 3d8e575f686753..e98307468d1f31 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 102/139] 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 40c8c7b06290fa..00000000000000 --- 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 f6c3438025d097..00000000000000 --- 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 fcfc8cfb2976bb..00000000000000 --- 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 a90e15a661643d..70c547e26e038d 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 103/139] 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 1bab27bdd1243e..b6f57546ceca20 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -100,7 +100,7 @@ (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 375f647da22dd3..ce750b74e2335e 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 104/139] 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 ae91178e4c457f..608687610e42f6 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 70c547e26e038d..188c7075d10086 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 4f8050390418da..c09da4b3bb8568 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 105/139] 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 3cbcec0214e750..e9ca2a6c8065a3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -318,7 +318,8 @@ def test_process_config_upgrade(self): 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 @@ def test_process_config_upgrade(self): 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 106/139] 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 | 120 +++++++++++------- requirements_all.txt | 2 +- 4 files changed, 78 insertions(+), 49 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b96aae298a5daa..20fd91b75d3524 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 1aadeb549096bd..8fc3b2476cb4a8 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 33f57fa0371eaa..fe95d7c7e20e5d 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -5,6 +5,7 @@ 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 @@ _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 @@ }) -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 + 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: - printer = SyncThru(host) - except TypeError: - # if an exception is thrown, printer cannot be set up - return + # 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 @@ def device_state_attributes(self): 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 188c7075d10086..e768fa1f071c48 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 107/139] 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 d36768fb1b0328..4495cb3c2f9048 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 e768fa1f071c48..2007491a903f05 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 108/139] 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 | 111 ++++------- .../components/enocean/binary_sensor.py | 70 ++++--- homeassistant/components/enocean/light.py | 35 ++-- .../components/enocean/manifest.json | 4 +- homeassistant/components/enocean/sensor.py | 187 +++++++++++++++--- homeassistant/components/enocean/switch.py | 46 +++-- requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + 10 files changed, 306 insertions(+), 154 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 20fd91b75d3524..c31262058100c1 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 2dcf6a3a0ac514..9d51821082a251 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -4,13 +4,13 @@ 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 @@ }), }, 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 @@ def __init__(self, hass, ser): self.__communicator = SerialCommunicator( port=ser, callback=self.callback) self.__communicator.start() - self.__devices = [] - - def register_device(self, dev): - """Register another device.""" - self.__devices.append(dev) + self.hass = hass + self.hass.helpers.dispatcher.dispatcher_connect( + SIGNAL_SEND_MESSAGE, self._send_message_callback) - 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) - - -class EnOceanDevice(): + if isinstance(packet, RadioPacket): + _LOGGER.debug("Received radio packet: %s", packet) + self.hass.helpers.dispatcher.dispatcher_send( + SIGNAL_RECEIVE_MESSAGE, packet) + + +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 649bec024e3db3..5e0a3b31817c6b 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -3,16 +3,17 @@ 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 @@ 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. + + 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, devname, device_class): + 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 9ec3f4ab27bd33..d40b2c01df6557 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -4,10 +4,10 @@ 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 @@ 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 @@ def turn_off(self, **kwargs): 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 7c4d7c0b8d9b28..e6f1c5d78262c9 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 530738e1f88a7c..62d0277946fe7e 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -3,58 +3,201 @@ import voluptuous as vol +from homeassistant.components import enocean from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_ID, POWER_WATT) -from homeassistant.helpers.entity import Entity +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 -from homeassistant.components import enocean _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) + + 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)]) - add_entities([EnOceanSensor(dev_id, devname)]) + 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 f0b132c9d1c2a8..48d53949a47726 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -3,16 +3,16 @@ 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 @@ 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 @@ def is_on(self): @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 @@ def turn_off(self, **kwargs): 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 2007491a903f05..b328c0f361d299 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 c09da4b3bb8568..7299a3cfdc5018 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 63b0ef737e23c4..9586dc179479a3 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -58,6 +58,7 @@ '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 109/139] 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 ac674b9fada424..3aeb2b5c1874f5 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 c31262058100c1..700d68b9449205 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 00000000000000..5806d7ea48744d --- /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 00000000000000..cd2d85a25a4fb1 --- /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 00000000000000..6c2365767d0135 --- /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 00000000000000..1a139c51bf0adc --- /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 566297ecb144f9..00000000000000 --- 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 2edf83a0d1f3f5..00000000000000 --- 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 132155c7f65220..00000000000000 --- 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 b328c0f361d299..bc48a6538a3d64 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 7299a3cfdc5018..cd20177bcdd959 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 110/139] 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 0a1f14324f648f..036422d6800f5f 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 111/139] 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 8e610a4f51c808..b81a2320b5e87a 100644 --- a/homeassistant/components/ubee/device_tracker.py +++ b/homeassistant/components/ubee/device_tracker.py @@ -18,7 +18,13 @@ 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 524dcb1d77b7a9..f9f17e41546a4a 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 bc48a6538a3d64..23d0c4800744f9 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 112/139] 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 d0c4336f544314..003861d2107e42 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 113/139] 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 97b5393561452c..5b0a8e6789387a 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 56e9647df50e0a..ae1b1c3200318e 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 @@ async def wrapper(*args, **kwargs): 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 @@ async def _controller_event(self, event): 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 23d0c4800744f9..902e921b74efec 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 cd20177bcdd959..dd44ba61575067 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 4cf871f5ed0f48..e3e0211025878e 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 114/139] 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 4b2972b0c002eb..0a1406adceec4a 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1046,6 +1046,11 @@ def media_image_url(self): 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 7dcfdac52179f8..478f59d2817ae8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -324,6 +324,11 @@ def media_image_url(self): """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 @@ def entity_picture(self): 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 @@ async def get(self, request, entity_id): 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 808c6e4f50fa77..23deffa972a2ed 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -8,8 +8,8 @@ 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 115/139] 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 4cb4204f274e0b..4a65808e0494b9 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -172,12 +172,15 @@ def update_devices(): # 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 116/139] 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 3a0a983fceb90f..6de31caa90e325 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -5,16 +5,30 @@ 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 @@ 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 @@ '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_sensor_values(sensors): + if SENSOR_MOTION_DETECTOR in sensors: + _LOGGER.warning( + "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 _deprecated_sensors(value): - if SENSOR_MOTION_DETECTOR in value: +def _deprecated_switches(config): + if CONF_SWITCHES in config: _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 '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(value): - names = [camera[CONF_NAME] for camera in value] +def _has_unique_names(devices): + names = [device[CONF_NAME] for device in devices] vol.Schema(vol.Unique())(names) - return value - - -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)]), -}) + return devices + + +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 b591616a88d585..0eb9e42e707dd7 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -5,38 +5,39 @@ 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 @@ def update(self): _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 07f5d403ba8742..e646c11f2e9d27 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 @@ async def async_camera_image(self): 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 @@ async def async_camera_image(self): 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 @@ async def handle_async_mjpeg_stream(self, request): # 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 @@ def name(self): """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 @@ def brand(self): """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 @@ def model(self): @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 @@ def is_on(self): # 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 @@ def update(self): _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 @@ def turn_on(self): """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 @@ def _enable_video_stream(self, enable): 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 00000000000000..a0230937e95b81 --- /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 00000000000000..270c969a6cc9fa --- /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 56cb021052e319..4d2cd88c5ae4a7 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -5,11 +5,19 @@ 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 @@ def icon(self): @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 00000000000000..d6e7a02a4f96b8 --- /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 90f750d17978d4..5989d4daf1e38a 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 117/139] 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 829c05fef3105e..6aba3973a0d8f7 100644 --- a/tests/components/alarm_control_panel/common.py +++ b/tests/components/alarm_control_panel/common.py @@ -9,12 +9,9 @@ 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 b5b6137a0a8d75..21bc4536a9b699 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -12,13 +12,10 @@ 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 5a819b0c5daa2e..79093f5ff02660 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 8711acaa318b01..5013e316ea2bde 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 634c56ffbadfc6..d4356ace48cf0c 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 f3873dd9fe068e..4df0d5c376007d 100644 --- a/tests/components/fan/common.py +++ b/tests/components/fan/common.py @@ -8,13 +8,10 @@ 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 60d2250a13d5bf..71472dc844369c 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -4,6 +4,8 @@ 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 51580e503bd58c..7c28a72a883e1b 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 906e0458dba6b2..81922e7123452b 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -9,7 +9,6 @@ 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 c5a71a3eb96a42..4a91204948e685 100644 --- a/tests/components/lock/common.py +++ b/tests/components/lock/common.py @@ -6,7 +6,6 @@ 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 a6e59af64d5bd1..f0f1072085363a 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 4514e5285aa8f6..28348b99fde9c4 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 11e2984cbb36b6..d6a49fd2002192 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 31aebecc23699e..5644aaa8912971 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 5a7bf6c2d8b494..8beceb7d6606cb 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 75fd92dddc0e36..ea2b535b0fa7e6 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 018f706a1a07c9..a3958669369e3e 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 6328d2b7c1a102..2ab75b584d2c5d 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 0c871fdcfd0a1a..ecd63a1dcdc942 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 df6706b01cf2a7..f469cc8a1398a5 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 8db8e425ddb023..2da42c8bcc8511 100644 --- a/tests/components/switch/common.py +++ b/tests/components/switch/common.py @@ -6,7 +6,6 @@ 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 5e6bebb56efa50..efe96efb5a89cd 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 85e63025bbc327..02eec391c4d9ad 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 62a0e429c0a241..7dfdd043237fde 100644 --- a/tests/components/vacuum/common.py +++ b/tests/components/vacuum/common.py @@ -10,7 +10,6 @@ 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 118/139] 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 c2039161ceba4b..3959eb880351e4 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 119/139] 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 a79f29604972e1..8620b1dc34c46d 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 902e921b74efec..c82fe3741b8c78 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 120/139] 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 a7267441cdb570..44008214535163 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 121/139] 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 cb3f3b5b46a60f..5a97b43af86f75 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 @@ def __init__(self, name): 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 @@ def sound_mode_list(self): """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 b6f57546ceca20..0f15d10f181d33 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -51,6 +51,9 @@ 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 @@ 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 @@ (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 478f59d2817ae8..b23d95ab625ece 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -66,6 +66,16 @@ 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 f3732c12213716..76ccc79a378898 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -147,7 +147,7 @@ 'action.devices.traits.Modes' ], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.MEDIA', 'willReportState': False }, { @@ -162,7 +162,7 @@ 'action.devices.traits.Modes' ], 'type': - 'action.devices.types.SWITCH', + 'action.devices.types.MEDIA', 'willReportState': False }, { @@ -171,7 +171,7 @@ '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 @@ '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 ce750b74e2335e..ea5291f28f7c86 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.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 122/139] 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 9007cb5c7bed6c..0320bc3b0b41d8 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 c82fe3741b8c78..a0c9397f2b3c26 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 123/139] 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 60c2f9a231b991..374e22d77ddb2c 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -187,5 +187,5 @@ def update(self): 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 124/139] 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 1396493a527a2c..dee43e3f367d51 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -78,7 +78,7 @@ def device_class(self): @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 @@ def device_class(self): @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 125/139] 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 4a24120be95592..550ba43950b7e7 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -4,9 +4,12 @@ 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 @@ }, 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 1326e46d7d3532..c2ad23700f3e62 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 dee43e3f367d51..19d35c47cdb426 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -7,9 +7,12 @@ 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 @@ def is_on(self): 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 @@ def is_on(self): 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 @@ def is_on(self): 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 @@ def device_state_attributes(self): 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 @@ def device_state_attributes(self): 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 @@ def device_state_attributes(self): 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 8a2ad8738dffac..3170fc149d53e6 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 458186bcce1cc6..696425df5b5ac2 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 @@ @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 381bcf1980e35b..fc75d78119d55d 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 @@ async def async_set_cover_position(self, **kwargs): 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 f6da8b27cf751e..6bbbb8b4fab4a8 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 @@ 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 @@ def _device_changed(self, *args, **kwargs): 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 @@ def name(self): 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 99e98b5a1d2bea..b3731bc9f1af5a 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -6,7 +6,8 @@ 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 @@ async def get_auth(self, hass, hapid, pin): 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 @@ def __init__(self, hass, config_entry): 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 @@ async def async_reset(self): 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 f4f73104f7c089..7cfbae95a33b6e 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 @@ def device_state_attributes(self): 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 @@ async def async_turn_off(self, **kwargs): 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 @@ def __init__(self, home, device, channel): } @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 @@ def device_state_attributes(self): 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 4816eacd08fe00..3d91b25c2bdcf5 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -10,11 +10,14 @@ 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 @@ def device_info(self): } @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 @@ def unit_of_measurement(self): 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 @@ def icon(self): 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 @@ def unit_of_measurement(self): 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 @@ def unit_of_measurement(self): 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 @@ def state(self): 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 @@ def device_state_attributes(self): 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 @@ def state(self): 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 @@ def unit_of_measurement(self): 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 @@ def unit_of_measurement(self): 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 @@ def device_state_attributes(self): 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 9a0d48ac2531ea..7b87f6c740e2bb 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -6,8 +6,11 @@ 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 @@ async def async_turn_off(self, **kwargs): 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 @@ def today_energy_kwh(self): 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 9c7d843b4484d0..b97948b2d9fa88 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -4,9 +4,12 @@ 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 126/139] 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 09baaa5ba0b3e8..8d68a4c33b753c 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -11,7 +11,7 @@ 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 @@ def supported_features(self): """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 6efd5065ba6baa..a66c68a694c3c6 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -9,7 +9,8 @@ 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 @@ def state(self): """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 127/139] 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 3404bdef99b2c6..a1cc0a0caa3ce3 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 @@ 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 5dca559cb0e52e..44ae3d7612a6c4 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -4,10 +4,9 @@ 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 128/139] 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 c68da4b566cb01..588bd23410cf83 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 a0c9397f2b3c26..1f13b1edf792d7 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 dd44ba61575067..99861b3ce881a4 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 129/139] 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 b5509b1d559ca1..031eb2464da932 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -219,3 +219,5 @@ async def async_read_state(self, from_cache): '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 130/139] 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 b23d95ab625ece..ccfa968fa9a15b 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -45,7 +45,8 @@ 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 @@ async def async_volume_up(self): 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 @@ async def async_volume_down(self): 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 8c576f58c14c3b..7eb72a66c8b027 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 1c4a2fa84a2670..aa3d1eff209522 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -29,6 +29,13 @@ def volume_level(self): """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 @@ def volume_level(self): """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 647ca981da3f47..81cdd097855328 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 131/139] 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 a2a6c315edae51..74c614c03a6cc5 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -5,11 +5,12 @@ 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 @@ 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 87f41b11d95169..432229a482c6d1 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -372,7 +372,7 @@ def test_media_commands(self, mocked_soundtouch_device): 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 132/139] 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 b661fa26fe7711..d68e87914ecda8 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 7c2e5fec843075..2e7d09be334fd8 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -10,20 +10,21 @@ 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 @@ 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 @@ 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 @@ def __init__(self, hass): 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 @@ async def async_service_handle(service): 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 service_handle(service): 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 00000000000000..95bc66fe3171a2 --- /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 a09fa7d2615c4f..3cdeeb08f0279f 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 4cb4a291b16874..a06a6160400754 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 +"""Tests for the Sonos Media Player platform.""" +from homeassistant.components.sonos import media_player, DOMAIN +from homeassistant.setup import async_setup_component -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 +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() -ENTITY_ID = 'media_player.kitchen' +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 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')} - - -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': '' - } - - -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 133/139] 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 f56ffbfffd23d3..8e556e4b6c938c 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -12,6 +12,8 @@ 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 @@ 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 _service_addwebhook(service): 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 _service_addwebhook(service): 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 _service_dropwebhook(service): 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 f282faf82c87aa..432820d6dbd7ec 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,7 +8,8 @@ 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 b74dce4b26209b..976e0794938806 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -9,7 +9,8 @@ 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 00c08c654ef0f3..33ad34b25ff3af 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -14,7 +14,7 @@ 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 00000000000000..ea547aaa52bbfb --- /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 c9c1101c2a2b1e..161177c9c7632c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -12,7 +12,7 @@ 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 @@ 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 134/139] 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 d4d67edbd353be..9bf3ba09ce7135 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 135/139] 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 | 155 ++++++++++----------- homeassistant/components/iqvia/const.py | 31 ++--- homeassistant/components/iqvia/sensor.py | 55 +++++--- 3 files changed, 117 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 5806d7ea48744d..b6bd85fca53922 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 @@ _LOGGER = logging.getLogger(__name__) + CONF_ZIP_CODE = 'zip_code' DATA_CONFIG = 'config' @@ -31,8 +36,18 @@ 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 @@ 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 refresh(event_time): 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 @@ def __init__(self, client, sensor_types): 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) - - 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 = {} + # 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 isinstance(result, IQVIAError): + _LOGGER.error('Unable to get %s data: %s', key, result) + self.data[key] = {} + continue + + _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 @@ def state(self): @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 cd2d85a25a4fb1..0ba9d7a0f1ea25 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -22,24 +22,15 @@ 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 1a139c51bf0adc..252007de21e1a2 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 @@ async def async_update(self): 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 @@ async def async_update(self): 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 @@ async def async_update(self): '{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 136/139] 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 fc15ebea772704..edc447fe721436 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 137/139] 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 c0b2f7acd0fcd0..2ed83fe1d9b06b 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -88,7 +88,7 @@ def token_saver(token): 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 138/139] 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 05e3307299a16e..ee362ad130f582 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -151,9 +151,12 @@ def log_error(msg: str, link: bool = True) -> None: 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 @@ def log_error(msg: str, link: bool = True) -> None: 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 @@ def log_error(msg: str, link: bool = True) -> None: 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 139/139] 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 4546be8078b5df..78efeca7311934 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 1f13b1edf792d7..d841baa54f953e 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