From f3411f8db29ad8b07afe4696e4baf9eadd5a450c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 5 May 2018 11:42:32 -0400 Subject: [PATCH 001/144] Version bump to 0.70.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 30c73546cf7..37e0c32ca03 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 69 +MINOR_VERSION = 70 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 2bb1a95098f7fef0e9276c0ec4d4c6fa4a2bea19 Mon Sep 17 00:00:00 2001 From: thepotoo <31549428+thepotoo@users.noreply.github.com> Date: Sun, 6 May 2018 02:21:02 -0400 Subject: [PATCH 002/144] Add unique_id to MQTT switch (#13719) --- homeassistant/components/switch/mqtt.py | 13 ++++++++++++- tests/components/switch/test_mqtt.py | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 69f12536c5f..1075888e199 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -5,6 +5,7 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.mqtt/ """ import logging +from typing import Optional import voluptuous as vol @@ -29,12 +30,14 @@ DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False +CONF_UNIQUE_ID = 'unique_id' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) @@ -62,6 +65,7 @@ async def async_setup_platform(hass, config, async_add_devices, config.get(CONF_OPTIMISTIC), config.get(CONF_PAYLOAD_AVAILABLE), config.get(CONF_PAYLOAD_NOT_AVAILABLE), + config.get(CONF_UNIQUE_ID), value_template, )]) @@ -72,7 +76,8 @@ class MqttSwitch(MqttAvailability, SwitchDevice): def __init__(self, name, icon, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, - payload_available, payload_not_available, value_template): + payload_available, payload_not_available, + unique_id: Optional[str], value_template): """Initialize the MQTT switch.""" super().__init__(availability_topic, qos, payload_available, payload_not_available) @@ -87,6 +92,7 @@ class MqttSwitch(MqttAvailability, SwitchDevice): self._payload_off = payload_off self._optimistic = optimistic self._template = value_template + self._unique_id = unique_id async def async_added_to_hass(self): """Subscribe to MQTT events.""" @@ -139,6 +145,11 @@ class MqttSwitch(MqttAvailability, SwitchDevice): """Return true if we do optimistic updates.""" return self._optimistic + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the icon.""" diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index b5e2a0b0395..24db0540012 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -248,3 +248,26 @@ class TestSwitchMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) + + def test_unique_id(self): + """Test unique id option only creates one switch per unique_id.""" + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: [{ + 'platform': 'mqtt', + 'name': 'Test 1', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }, { + 'platform': 'mqtt', + 'name': 'Test 2', + 'state_topic': 'test-topic', + 'command_topic': 'command-topic', + 'unique_id': 'TOTALLY_UNIQUE' + }] + }) + + fire_mqtt_message(self.hass, 'test-topic', 'payload') + self.hass.block_till_done() + assert len(self.hass.states.async_entity_ids()) == 2 + # all switches group is 1, unique id created is 1 From 63cc179ea23a8e60091a0064388843f805db9642 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Sun, 6 May 2018 02:17:05 -0700 Subject: [PATCH 003/144] zha: Bump to zigpy 0.1.0 (#14305) --- homeassistant/components/zha/__init__.py | 6 +++--- requirements_all.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 9b66c4c6ded..9d7556fc334 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -16,9 +16,9 @@ from homeassistant.helpers import discovery, entity from homeassistant.util import slugify REQUIREMENTS = [ - 'bellows==0.5.2', - 'zigpy==0.0.3', - 'zigpy-xbee==0.0.2', + 'bellows==0.6.0', + 'zigpy==0.1.0', + 'zigpy-xbee==0.1.0', ] DOMAIN = 'zha' diff --git a/requirements_all.txt b/requirements_all.txt index e74070cb98c..5162feb6bd2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -146,7 +146,7 @@ batinfo==0.4.2 beautifulsoup4==4.6.0 # homeassistant.components.zha -bellows==0.5.2 +bellows==0.6.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.5.0 @@ -1382,7 +1382,7 @@ zeroconf==0.20.0 ziggo-mediabox-xl==1.0.0 # homeassistant.components.zha -zigpy-xbee==0.0.2 +zigpy-xbee==0.1.0 # homeassistant.components.zha -zigpy==0.0.3 +zigpy==0.1.0 From 107769ab8198bd8e63e821222ec77ece78f38a74 Mon Sep 17 00:00:00 2001 From: Justin Loutsenhizer Date: Sun, 6 May 2018 13:18:26 -0400 Subject: [PATCH 004/144] Add missing 'sensor' to ABODE_PLATFORMS (#14313) This fixes missing light, humidity, temperature sensors from abode component. --- homeassistant/components/abode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/abode.py b/homeassistant/components/abode.py index 2f56bb7c2b5..6d5feb87dc2 100644 --- a/homeassistant/components/abode.py +++ b/homeassistant/components/abode.py @@ -81,7 +81,7 @@ TRIGGER_SCHEMA = vol.Schema({ ABODE_PLATFORMS = [ 'alarm_control_panel', 'binary_sensor', 'lock', 'switch', 'cover', - 'camera', 'light' + 'camera', 'light', 'sensor' ] From 34727be5ac23d6d0a2a4585c76ea6a5b56726531 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 6 May 2018 20:54:56 -0400 Subject: [PATCH 005/144] Fix module names for custom components (#14317) * Fix module names for custom components * Also set __package__ correctly * bla * Remove print --- homeassistant/loader.py | 23 +++++++++++++++---- tests/test_loader.py | 18 +++++++++++++-- .../test_package/__init__.py | 2 +- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 322870952f2..b6dabb1d883 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -73,13 +73,15 @@ def get_component(hass, comp_or_platform): # Try custom component module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - comp_or_platform) + PATH_CUSTOM_COMPONENTS, comp_or_platform) if module is None: try: module = importlib.import_module( '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) + _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) except ImportError: + _LOGGER.warning('Unable to find %s', comp_or_platform) module = None cache = hass.data.get(DATA_KEY) @@ -102,18 +104,20 @@ def _find_spec(path, name): return None -def _load_module(path, name): +def _load_module(path, base_module, name): """Load a module based on a folder and a name.""" + mod_name = "{}.{}".format(base_module, name) spec = _find_spec([path], name) # Special handling if loading platforms and the folder is a namespace # (namespace is a folder without __init__.py) if spec is None and '.' in name: - parent_spec = _find_spec([path], name.split('.')[0]) + mod_parent_name = name.split('.')[0] + parent_spec = _find_spec([path], mod_parent_name) if (parent_spec is None or parent_spec.submodule_search_locations is None): return None - spec = _find_spec(parent_spec.submodule_search_locations, name) + spec = _find_spec(parent_spec.submodule_search_locations, mod_name) # Not found if spec is None: @@ -123,8 +127,19 @@ def _load_module(path, name): if spec.loader is None: return None + _LOGGER.debug('Loaded %s (%s)', name, base_module) + module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) + # A hack, I know. Don't currently know how to work around it. + if not module.__name__.startswith(base_module): + module.__name__ = "{}.{}".format(base_module, name) + + if not module.__package__: + module.__package__ = base_module + elif not module.__package__.startswith(base_module): + module.__package__ = "{}.{}".format(base_module, name) + return module diff --git a/tests/test_loader.py b/tests/test_loader.py index 646526e94ea..e8a79c6501f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -30,8 +30,7 @@ class TestLoader(unittest.TestCase): comp = object() loader.set_component(self.hass, 'switch.test_set', comp) - self.assertEqual(comp, - loader.get_component(self.hass, 'switch.test_set')) + assert loader.get_component(self.hass, 'switch.test_set') is comp def test_get_component(self): """Test if get_component works.""" @@ -106,3 +105,18 @@ def test_helpers_wrapper(hass): yield from hass.async_block_till_done() assert result == ['hello'] + + +async def test_custom_component_name(hass): + """Test the name attribte of custom components.""" + comp = loader.get_component(hass, 'test_standalone') + assert comp.__name__ == 'custom_components.test_standalone' + assert comp.__package__ == 'custom_components' + + comp = loader.get_component(hass, 'test_package') + assert comp.__name__ == 'custom_components.test_package' + assert comp.__package__ == 'custom_components.test_package' + + comp = loader.get_component(hass, 'light.test') + assert comp.__name__ == 'custom_components.light.test' + assert comp.__package__ == 'custom_components.light' diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index 528f056948b..ee669c6c9b5 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -2,6 +2,6 @@ DOMAIN = 'test_package' -def setup(hass, config): +async def async_setup(hass, config): """Mock a successful setup.""" return True From 91fe6e4e5611f5b385f2bb5373400cc42817515c Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 7 May 2018 02:55:38 +0200 Subject: [PATCH 006/144] Add debounce to move_cover (#14314) * Add debounce to move_cover * Fix spelling mistake --- .../components/homekit/type_covers.py | 4 ++- .../homekit/type_security_systems.py | 2 +- tests/components/homekit/test_type_covers.py | 34 ++++++++++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index b30109f711d..3de87cf63e8 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES) from . import TYPES -from .accessories import HomeAccessory +from .accessories import HomeAccessory, debounce from .const import ( SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, CHAR_TARGET_POSITION, CHAR_POSITION_STATE, @@ -80,6 +80,7 @@ class WindowCovering(HomeAccessory): self.char_target_position = serv_cover.configure_char( CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) @@ -122,6 +123,7 @@ class WindowCoveringBasic(HomeAccessory): self.char_position_state = serv_cover.configure_char( CHAR_POSITION_STATE, value=2) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" _LOGGER.debug('%s: Set position to %d', self.entity_id, value) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index e32860d1fef..ab16f921e99 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -67,7 +67,7 @@ class SecuritySystem(HomeAccessory): _LOGGER.debug('%s: Updated current state to %s (%d)', self.entity_id, hass_state, current_security_state) - # SecuritySystemTargetSTate does not support triggered + # SecuritySystemTargetState does not support triggered if not self.flag_target_state and \ hass_state != STATE_ALARM_TRIGGERED: self.char_target_state.set_value(current_security_state) diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 2dcb48a4d4c..313d58e78fd 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,19 +4,35 @@ import unittest from homeassistant.core import callback from homeassistant.components.cover import ( ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) -from homeassistant.components.homekit.type_covers import ( - GarageDoorOpener, WindowCovering, WindowCoveringBasic) from homeassistant.const import ( STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, ATTR_SUPPORTED_FEATURES) from tests.common import get_test_home_assistant +from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitSensors(unittest.TestCase): +class TestHomekitCovers(unittest.TestCase): """Test class for all accessory types regarding covers.""" + @classmethod + def setUpClass(cls): + """Setup Light class import and debounce patcher.""" + cls.patcher = patch_debounce() + cls.patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + cls.garage_cls = _import.GarageDoorOpener + cls.window_cls = _import.WindowCovering + cls.window_basic_cls = _import.WindowCoveringBasic + + @classmethod + def tearDownClass(cls): + """Stop debounce patcher.""" + cls.patcher.stop() + def setUp(self): """Setup things to be run when tests are started.""" self.hass = get_test_home_assistant() @@ -37,7 +53,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" garage_door = 'cover.garage_door' - acc = GarageDoorOpener(self.hass, 'Cover', garage_door, 2, config=None) + acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -95,7 +111,7 @@ class TestHomekitSensors(unittest.TestCase): """Test if accessory and HA are updated accordingly.""" window_cover = 'cover.window' - acc = WindowCovering(self.hass, 'Cover', window_cover, 2, config=None) + acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -146,8 +162,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() self.assertEqual(acc.aid, 2) @@ -214,8 +230,8 @@ class TestHomekitSensors(unittest.TestCase): self.hass.states.set(window_cover, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = WindowCoveringBasic(self.hass, 'Cover', window_cover, 2, - config=None) + acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, + config=None) acc.run() # Set from HomeKit From e60d0665141e94683bb3ba788d5cfaebb0b719d7 Mon Sep 17 00:00:00 2001 From: Jerad Meisner Date: Mon, 7 May 2018 00:35:55 -0700 Subject: [PATCH 007/144] Converted SABnzbd to a component (#12915) * Converted SABnzbd to a component * fixed async issues * Made sabnzbd scan interval static. More async fixes. * Sabnzbd component code cleanup * Skip sensor platform setup if discovery_info is None --- .coveragerc | 4 +- homeassistant/components/discovery.py | 3 +- homeassistant/components/sabnzbd.py | 254 +++++++++++++++++++++ homeassistant/components/sensor/sabnzbd.py | 213 +++-------------- requirements_all.txt | 2 +- 5 files changed, 296 insertions(+), 180 deletions(-) create mode 100644 homeassistant/components/sabnzbd.py diff --git a/.coveragerc b/.coveragerc index d2192ca2e46..9030cc9a097 100644 --- a/.coveragerc +++ b/.coveragerc @@ -226,6 +226,9 @@ omit = homeassistant/components/rpi_pfio.py homeassistant/components/*/rpi_pfio.py + homeassistant/components/sabnzbd.py + homeassistant/components/*/sabnzbd.py + homeassistant/components/satel_integra.py homeassistant/components/*/satel_integra.py @@ -650,7 +653,6 @@ omit = homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/ripple.py - homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 07eb5aaab82..46ac58d43b1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -39,6 +39,7 @@ SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' @@ -59,6 +60,7 @@ SERVICE_HANDLERS = { SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), @@ -74,7 +76,6 @@ SERVICE_HANDLERS = { 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), 'harmony': ('remote', 'harmony'), - 'sabnzbd': ('sensor', 'sabnzbd'), 'bose_soundtouch': ('media_player', 'soundtouch'), 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py new file mode 100644 index 00000000000..a7b33b4c697 --- /dev/null +++ b/homeassistant/components/sabnzbd.py @@ -0,0 +1,254 @@ +""" +Support for monitoring an SABnzbd NZB client. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sabnzbd/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.discovery import SERVICE_SABNZBD +from homeassistant.const import ( + CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.json import load_json, save_json + +REQUIREMENTS = ['pysabnzbd==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'sabnzbd' +DATA_SABNZBD = 'sabznbd' + +_CONFIGURING = {} + +ATTR_SPEED = 'speed' +BASE_URL_FORMAT = '{}://{}:{}/' +CONFIG_FILE = 'sabnzbd.conf' +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'SABnzbd' +DEFAULT_PORT = 8080 +DEFAULT_SPEED_LIMIT = '100' +DEFAULT_SSL = False + +UPDATE_INTERVAL = timedelta(seconds=30) + +SERVICE_PAUSE = 'pause' +SERVICE_RESUME = 'resume' +SERVICE_SET_SPEED = 'set_speed' + +SIGNAL_SABNZBD_UPDATED = 'sabnzbd_updated' + +SENSOR_TYPES = { + 'current_status': ['Status', None, 'status'], + 'speed': ['Speed', 'MB/s', 'kbpersec'], + 'queue_size': ['Queue', 'MB', 'mb'], + 'queue_remaining': ['Left', 'MB', 'mbleft'], + 'disk_size': ['Disk', 'GB', 'diskspacetotal1'], + 'disk_free': ['Disk Free', 'GB', 'diskspace1'], + 'queue_count': ['Queue Count', None, 'noofslots_total'], + 'day_size': ['Daily Total', 'GB', 'day_size'], + 'week_size': ['Weekly Total', 'GB', 'week_size'], + 'month_size': ['Monthly Total', 'GB', 'month_size'], + 'total_size': ['Total', 'GB', 'total_size'], +} + +SPEED_LIMIT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_check_sabnzbd(sab_api): + """Check if we can reach SABnzbd.""" + from pysabnzbd import SabnzbdApiException + + try: + await sab_api.check_available() + return True + except SabnzbdApiException: + _LOGGER.error("Connection to SABnzbd API failed") + return False + + +async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, + api_key=None): + """Try to configure Sabnzbd and request api key if configuration fails.""" + from pysabnzbd import SabnzbdApi + + host = config[CONF_HOST] + port = config[CONF_PORT] + uri_scheme = 'https' if use_ssl else 'http' + base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) + if api_key is None: + conf = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) + api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') + + sab_api = SabnzbdApi(base_url, api_key) + if await async_check_sabnzbd(sab_api): + async_setup_sabnzbd(hass, sab_api, config, name) + else: + async_request_configuration(hass, config, base_url) + + +async def async_setup(hass, config): + """Setup the SABnzbd component.""" + async def sabnzbd_discovered(service, info): + """Handle service discovery.""" + ssl = info.get('properties', {}).get('https', '0') == '1' + await async_configure_sabnzbd(hass, info, ssl) + + discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) + + conf = config.get(DOMAIN) + if conf is not None: + use_ssl = conf.get(CONF_SSL) + name = conf.get(CONF_NAME) + api_key = conf.get(CONF_API_KEY) + await async_configure_sabnzbd(hass, conf, use_ssl, name, api_key) + return True + + +@callback +def async_setup_sabnzbd(hass, sab_api, config, name): + """Setup SABnzbd sensors and services.""" + sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {})) + + if config.get(CONF_SENSORS): + hass.data[DATA_SABNZBD] = sab_api_data + hass.async_add_job( + discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) + + async def async_service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + await sab_api_data.async_pause_queue() + elif service.service == SERVICE_RESUME: + await sab_api_data.async_resume_queue() + elif service.service == SERVICE_SET_SPEED: + speed = service.data.get(ATTR_SPEED) + await sab_api_data.async_set_queue_speed(speed) + + hass.services.async_register(DOMAIN, SERVICE_PAUSE, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_RESUME, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_SET_SPEED, + async_service_handler, + schema=SPEED_LIMIT_SCHEMA) + + async def async_update_sabnzbd(now): + """Refresh SABnzbd queue data.""" + from pysabnzbd import SabnzbdApiException + try: + await sab_api.refresh_data() + async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) + except SabnzbdApiException as err: + _LOGGER.error(err) + + async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + + +@callback +def async_request_configuration(hass, config, host): + """Request configuration steps from the user.""" + from pysabnzbd import SabnzbdApi + + configurator = hass.components.configurator + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.async_notify_errors( + _CONFIGURING[host], + 'Failed to register, please try again.') + + return + + async def async_configuration_callback(data): + """Handle configuration changes.""" + api_key = data.get(CONF_API_KEY) + sab_api = SabnzbdApi(host, api_key) + if not await async_check_sabnzbd(sab_api): + return + + def success(): + """Setup was successful.""" + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {CONF_API_KEY: api_key} + save_json(hass.config.path(CONFIG_FILE), conf) + req_config = _CONFIGURING.pop(host) + configurator.request_done(req_config) + + hass.async_add_job(success) + async_setup_sabnzbd(hass, sab_api, config, + config.get(CONF_NAME, DEFAULT_NAME)) + + _CONFIGURING[host] = configurator.async_request_config( + DEFAULT_NAME, + async_configuration_callback, + description='Enter the API Key', + submit_caption='Confirm', + fields=[{'id': CONF_API_KEY, 'name': 'API Key', 'type': ''}] + ) + + +class SabnzbdApiData: + """Class for storing/refreshing sabnzbd api queue data.""" + + def __init__(self, sab_api, name, sensors): + """Initialize component.""" + self.sab_api = sab_api + self.name = name + self.sensors = sensors + + async def async_pause_queue(self): + """Pause Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.pause_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_resume_queue(self): + """Resume Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.resume_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_set_queue_speed(self, limit): + """Set speed limit for the Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.set_speed_limit(limit) + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + def get_queue_field(self, field): + """Return the value for the given field from the Sabnzbd queue.""" + return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 194ff71222a..185f83c9405 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -4,216 +4,75 @@ Support for monitoring an SABnzbd NZB client. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sabnzbd/ """ -import asyncio import logging -from datetime import timedelta -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, - CONF_SSL) +from homeassistant.components.sabnzbd import DATA_SABNZBD, \ + SIGNAL_SABNZBD_UPDATED, SENSOR_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util.json import load_json, save_json -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysabnzbd==1.0.1'] +DEPENDENCIES = ['sabnzbd'] -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -CONFIG_FILE = 'sabnzbd.conf' -DEFAULT_NAME = 'SABnzbd' -DEFAULT_PORT = 8080 -DEFAULT_SSL = False - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) - -SENSOR_TYPES = { - 'current_status': ['Status', None], - 'speed': ['Speed', 'MB/s'], - 'queue_size': ['Queue', 'MB'], - 'queue_remaining': ['Left', 'MB'], - 'disk_size': ['Disk', 'GB'], - 'disk_free': ['Disk Free', 'GB'], - 'queue_count': ['Queue Count', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=['current_status']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, -}) - - -@asyncio.coroutine -def async_check_sabnzbd(sab_api, base_url, api_key): - """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException - sab_api = sab_api(base_url, api_key) - - try: - yield from sab_api.check_available() - except SabnzbdApiException: - _LOGGER.error("Connection to SABnzbd API failed") - return False - return True - - -def setup_sabnzbd(base_url, apikey, name, config, - async_add_devices, sab_api): - """Set up polling from SABnzbd and sensors.""" - sab_api = sab_api(base_url, apikey) - monitored = config.get(CONF_MONITORED_VARIABLES) - async_add_devices([SabnzbdSensor(variable, sab_api, name) - for variable in monitored]) - - -@Throttle(MIN_TIME_BETWEEN_UPDATES) -async def async_update_queue(sab_api): - """ - Throttled function to update SABnzbd queue. - - This ensures that the queue info only gets updated once for all sensors - """ - await sab_api.refresh_data() - - -def request_configuration(host, name, hass, config, async_add_devices, - sab_api): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors(_CONFIGURING[host], - 'Failed to register, please try again.') +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the SABnzbd sensors.""" + if discovery_info is None: return - @asyncio.coroutine - def async_configuration_callback(data): - """Handle configuration changes.""" - api_key = data.get('api_key') - if (yield from async_check_sabnzbd(sab_api, host, api_key)): - setup_sabnzbd(host, api_key, name, config, - async_add_devices, sab_api) - - def success(): - """Set up was successful.""" - conf = load_json(hass.config.path(CONFIG_FILE)) - conf[host] = {'api_key': api_key} - save_json(hass.config.path(CONFIG_FILE), conf) - req_config = _CONFIGURING.pop(host) - configurator.async_request_done(req_config) - - hass.async_add_job(success) - - _CONFIGURING[host] = configurator.async_request_config( - DEFAULT_NAME, - async_configuration_callback, - description='Enter the API Key', - submit_caption='Confirm', - fields=[{'id': 'api_key', 'name': 'API Key', 'type': ''}] - ) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the SABnzbd platform.""" - from pysabnzbd import SabnzbdApi - - if discovery_info is not None: - host = discovery_info.get(CONF_HOST) - port = discovery_info.get(CONF_PORT) - name = DEFAULT_NAME - use_ssl = discovery_info.get('properties', {}).get('https', '0') == '1' - else: - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME, DEFAULT_NAME) - use_ssl = config.get(CONF_SSL) - - api_key = config.get(CONF_API_KEY) - - uri_scheme = 'https://' if use_ssl else 'http://' - base_url = "{}{}:{}/".format(uri_scheme, host, port) - - if not api_key: - conf = load_json(hass.config.path(CONFIG_FILE)) - if conf.get(base_url, {}).get('api_key'): - api_key = conf[base_url]['api_key'] - - if not (yield from async_check_sabnzbd(SabnzbdApi, base_url, api_key)): - request_configuration(base_url, name, hass, config, - async_add_devices, SabnzbdApi) - return - - setup_sabnzbd(base_url, api_key, name, config, - async_add_devices, SabnzbdApi) + sab_api_data = hass.data[DATA_SABNZBD] + sensors = sab_api_data.sensors + client_name = sab_api_data.name + async_add_devices([SabnzbdSensor(sensor, sab_api_data, client_name) + for sensor in sensors]) class SabnzbdSensor(Entity): """Representation of an SABnzbd sensor.""" - def __init__(self, sensor_type, sabnzbd_api, client_name): + def __init__(self, sensor_type, sabnzbd_api_data, client_name): """Initialize the sensor.""" + self._client_name = client_name + self._field_name = SENSOR_TYPES[sensor_type][2] self._name = SENSOR_TYPES[sensor_type][0] - self.sabnzbd_api = sabnzbd_api - self.type = sensor_type - self.client_name = client_name + self._sabnzbd_api = sabnzbd_api_data self._state = None + self._type = sensor_type self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + async_dispatcher_connect(self.hass, SIGNAL_SABNZBD_UPDATED, + self.update_state) + @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return '{} {}'.format(self._client_name, self._name) @property def state(self): """Return the state of the sensor.""" return self._state + def should_poll(self): + """Don't poll. Will be updated by dispatcher signal.""" + return False + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @asyncio.coroutine - def async_refresh_sabnzbd_data(self): - """Call the throttled SABnzbd refresh method.""" - from pysabnzbd import SabnzbdApiException - try: - yield from async_update_queue(self.sabnzbd_api) - except SabnzbdApiException: - _LOGGER.exception("Connection to SABnzbd API failed") - - @asyncio.coroutine - def async_update(self): + def update_state(self, args): """Get the latest data and updates the states.""" - yield from self.async_refresh_sabnzbd_data() + self._state = self._sabnzbd_api.get_queue_field(self._field_name) - if self.sabnzbd_api.queue: - if self.type == 'current_status': - self._state = self.sabnzbd_api.queue.get('status') - elif self.type == 'speed': - mb_spd = float(self.sabnzbd_api.queue.get('kbpersec')) / 1024 - self._state = round(mb_spd, 1) - elif self.type == 'queue_size': - self._state = self.sabnzbd_api.queue.get('mb') - elif self.type == 'queue_remaining': - self._state = self.sabnzbd_api.queue.get('mbleft') - elif self.type == 'disk_size': - self._state = self.sabnzbd_api.queue.get('diskspacetotal1') - elif self.type == 'disk_free': - self._state = self.sabnzbd_api.queue.get('diskspace1') - elif self.type == 'queue_count': - self._state = self.sabnzbd_api.queue.get('noofslots_total') - else: - self._state = 'Unknown' + if self._type == 'speed': + self._state = round(float(self._state) / 1024, 1) + elif 'size' in self._type: + self._state = round(float(self._state), 2) + + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 5162feb6bd2..79c7e38efbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -909,7 +909,7 @@ pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 -# homeassistant.components.sensor.sabnzbd +# homeassistant.components.sabnzbd pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo From 5c95c53c6c33e22eacc742e7b9761cc1b0ed7d03 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 05:25:48 -0400 Subject: [PATCH 008/144] Revert custom component loading logic (#14327) * Revert custom component loading logic * Lint * Fix tests * Guard for infinite inserts into sys.path --- homeassistant/loader.py | 114 +++++++----------- tests/components/notify/test_file.py | 46 ++++--- tests/test_loader.py | 4 + .../image_processing/test.py | 6 +- .../custom_components/light/test.py | 5 +- .../custom_components/switch/test.py | 5 +- .../test_package/__init__.py | 3 + .../custom_components/test_package/const.py | 2 + 8 files changed, 84 insertions(+), 101 deletions(-) create mode 100644 tests/testing_config/custom_components/test_package/const.py diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b6dabb1d883..e94fb2d6833 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -31,12 +31,6 @@ PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) -# List of available components -AVAILABLE_COMPONENTS = [] # type: List[str] - -# Dict of loaded components mapped name => module -_COMPONENT_CACHE = {} # type: Dict[str, ModuleType] - _LOGGER = logging.getLogger(__name__) @@ -64,85 +58,63 @@ def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]: return get_component(hass, PLATFORM_FORMAT.format(domain, platform)) -def get_component(hass, comp_or_platform): - """Load a module from either custom component or built-in.""" +def get_component(hass, comp_or_platform) -> Optional[ModuleType]: + """Try to load specified component. + + Looks in config dir first, then built-in components. + Only returns it if also found to be valid. + Async friendly. + """ try: return hass.data[DATA_KEY][comp_or_platform] except KeyError: pass - # Try custom component - module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS), - PATH_CUSTOM_COMPONENTS, comp_or_platform) - - if module is None: - try: - module = importlib.import_module( - '{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform)) - _LOGGER.debug('Loaded %s (built-in)', comp_or_platform) - except ImportError: - _LOGGER.warning('Unable to find %s', comp_or_platform) - module = None - cache = hass.data.get(DATA_KEY) if cache is None: + # Only insert if it's not there (happens during tests) + if sys.path[0] != hass.config.config_dir: + sys.path.insert(0, hass.config.config_dir) cache = hass.data[DATA_KEY] = {} - cache[comp_or_platform] = module - return module + # First check custom, then built-in + potential_paths = ['custom_components.{}'.format(comp_or_platform), + 'homeassistant.components.{}'.format(comp_or_platform)] - -def _find_spec(path, name): - for finder in sys.meta_path: + for path in potential_paths: try: - spec = finder.find_spec(name, path=path) - if spec is not None: - return spec - except AttributeError: - # Not all finders have the find_spec method - pass + module = importlib.import_module(path) + + # In Python 3 you can import files from directories that do not + # contain the file __init__.py. A directory is a valid module if + # it contains a file with the .py extension. In this case Python + # will succeed in importing the directory as a module and call it + # a namespace. We do not care about namespaces. + # This prevents that when only + # custom_components/switch/some_platform.py exists, + # the import custom_components.switch would succeed. + if module.__spec__.origin == 'namespace': + continue + + _LOGGER.info("Loaded %s from %s", comp_or_platform, path) + + cache[comp_or_platform] = module + + return module + + except ImportError as err: + # This error happens if for example custom_components/switch + # exists and we try to load switch.demo. + if str(err) != "No module named '{}'".format(path): + _LOGGER.exception( + ("Error loading %s. Make sure all " + "dependencies are installed"), path) + + _LOGGER.error("Unable to find component %s", comp_or_platform) + return None -def _load_module(path, base_module, name): - """Load a module based on a folder and a name.""" - mod_name = "{}.{}".format(base_module, name) - spec = _find_spec([path], name) - - # Special handling if loading platforms and the folder is a namespace - # (namespace is a folder without __init__.py) - if spec is None and '.' in name: - mod_parent_name = name.split('.')[0] - parent_spec = _find_spec([path], mod_parent_name) - if (parent_spec is None or - parent_spec.submodule_search_locations is None): - return None - spec = _find_spec(parent_spec.submodule_search_locations, mod_name) - - # Not found - if spec is None: - return None - - # This is a namespace - if spec.loader is None: - return None - - _LOGGER.debug('Loaded %s (%s)', name, base_module) - - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - # A hack, I know. Don't currently know how to work around it. - if not module.__name__.startswith(base_module): - module.__name__ = "{}.{}".format(base_module, name) - - if not module.__package__: - module.__package__ = base_module - elif not module.__package__.startswith(base_module): - module.__package__ = "{}.{}".format(base_module, name) - - return module - - class Components: """Helper to load components.""" diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index 42b9eb9d82d..c5064fca851 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -35,28 +35,30 @@ class TestNotifyFile(unittest.TestCase): assert setup_component(self.hass, notify.DOMAIN, config) assert not handle_config[notify.DOMAIN] - def _test_notify_file(self, timestamp, mock_utcnow, mock_stat): + def _test_notify_file(self, timestamp): """Test the notify file output.""" - mock_utcnow.return_value = dt_util.as_utc(dt_util.now()) - mock_stat.return_value.st_size = 0 + filename = 'mock_file' + message = 'one, two, testing, testing' + with assert_setup_component(1) as handle_config: + self.assertTrue(setup_component(self.hass, notify.DOMAIN, { + 'notify': { + 'name': 'test', + 'platform': 'file', + 'filename': filename, + 'timestamp': timestamp, + } + })) + assert handle_config[notify.DOMAIN] m_open = mock_open() with patch( 'homeassistant.components.notify.file.open', m_open, create=True - ): - filename = 'mock_file' - message = 'one, two, testing, testing' - with assert_setup_component(1) as handle_config: - self.assertTrue(setup_component(self.hass, notify.DOMAIN, { - 'notify': { - 'name': 'test', - 'platform': 'file', - 'filename': filename, - 'timestamp': timestamp, - } - })) - assert handle_config[notify.DOMAIN] + ), patch('homeassistant.components.notify.file.os.stat') as mock_st, \ + patch('homeassistant.util.dt.utcnow', + return_value=dt_util.utcnow()): + + mock_st.return_value.st_size = 0 title = '{} notifications (Log started: {})\n{}\n'.format( ATTR_TITLE_DEFAULT, dt_util.utcnow().isoformat(), @@ -82,14 +84,10 @@ class TestNotifyFile(unittest.TestCase): dt_util.utcnow().isoformat(), message))] ) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file(self, mock_utcnow, mock_stat): + def test_notify_file(self): """Test the notify file output without timestamp.""" - self._test_notify_file(False, mock_utcnow, mock_stat) + self._test_notify_file(False) - @patch('homeassistant.components.notify.file.os.stat') - @patch('homeassistant.util.dt.utcnow') - def test_notify_file_timestamp(self, mock_utcnow, mock_stat): + def test_notify_file_timestamp(self): """Test the notify file output with timestamp.""" - self._test_notify_file(True, mock_utcnow, mock_stat) + self._test_notify_file(True) diff --git a/tests/test_loader.py b/tests/test_loader.py index e8a79c6501f..c97e94a7ce1 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -120,3 +120,7 @@ async def test_custom_component_name(hass): comp = loader.get_component(hass, 'light.test') assert comp.__name__ == 'custom_components.light.test' assert comp.__package__ == 'custom_components.light' + + # Test custom components is mounted + from custom_components.test_package import TEST + assert TEST == 5 diff --git a/tests/testing_config/custom_components/image_processing/test.py b/tests/testing_config/custom_components/image_processing/test.py index 29d362699f5..b50050ed68e 100644 --- a/tests/testing_config/custom_components/image_processing/test.py +++ b/tests/testing_config/custom_components/image_processing/test.py @@ -3,9 +3,11 @@ from homeassistant.components.image_processing import ImageProcessingEntity -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Set up the test image_processing platform.""" - add_devices([TestImageProcessing('camera.demo_camera', "Test")]) + async_add_devices_callback([ + TestImageProcessing('camera.demo_camera', "Test")]) class TestImageProcessing(ImageProcessingEntity): diff --git a/tests/testing_config/custom_components/light/test.py b/tests/testing_config/custom_components/light/test.py index 71625dfdf93..fbf79f9e770 100644 --- a/tests/testing_config/custom_components/light/test.py +++ b/tests/testing_config/custom_components/light/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Return mock devices.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/switch/test.py b/tests/testing_config/custom_components/switch/test.py index 2819f2f2951..79126b7b52a 100644 --- a/tests/testing_config/custom_components/switch/test.py +++ b/tests/testing_config/custom_components/switch/test.py @@ -21,6 +21,7 @@ def init(empty=False): ] -def setup_platform(hass, config, add_devices_callback, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices_callback, + discovery_info=None): """Find and return test switches.""" - add_devices_callback(DEVICES) + async_add_devices_callback(DEVICES) diff --git a/tests/testing_config/custom_components/test_package/__init__.py b/tests/testing_config/custom_components/test_package/__init__.py index ee669c6c9b5..85e78a7f9d6 100644 --- a/tests/testing_config/custom_components/test_package/__init__.py +++ b/tests/testing_config/custom_components/test_package/__init__.py @@ -1,4 +1,7 @@ """Provide a mock package component.""" +from .const import TEST # noqa + + DOMAIN = 'test_package' diff --git a/tests/testing_config/custom_components/test_package/const.py b/tests/testing_config/custom_components/test_package/const.py new file mode 100644 index 00000000000..7e13e04cb47 --- /dev/null +++ b/tests/testing_config/custom_components/test_package/const.py @@ -0,0 +1,2 @@ +"""Constants for test_package custom component.""" +TEST = 5 From a2b8ad50f25fdfe21814556452c8e7b1d1262ec1 Mon Sep 17 00:00:00 2001 From: Javier Gonel Date: Mon, 7 May 2018 16:52:33 +0300 Subject: [PATCH 009/144] fix(hbmqtt): partial packets breaking hbmqtt (#14329) This issue was fixed in hbmqtt/issues#95 that was released in hbmqtt 0.9.2 --- homeassistant/components/mqtt/server.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index db251ab4180..8a012928792 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['hbmqtt==0.9.1'] +REQUIREMENTS = ['hbmqtt==0.9.2'] DEPENDENCIES = ['http'] # None allows custom config to be created through generate_config diff --git a/requirements_all.txt b/requirements_all.txt index 79c7e38efbb..342bbebb08f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -371,7 +371,7 @@ ha-philipsjs==0.0.3 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.climate.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 939e4314718..976f4d87280 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -75,7 +75,7 @@ ha-ffmpeg==1.9 haversine==0.4.5 # homeassistant.components.mqtt.server -hbmqtt==0.9.1 +hbmqtt==0.9.2 # homeassistant.components.binary_sensor.workday holidays==0.9.5 From 6318178a8b211666b1ac00c93747af6640fa5915 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 10:00:54 -0400 Subject: [PATCH 010/144] Update netdisco to 1.4.1 --- homeassistant/components/discovery.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 46ac58d43b1..68cf293ce48 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -21,7 +21,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util -REQUIREMENTS = ['netdisco==1.4.0'] +REQUIREMENTS = ['netdisco==1.4.1'] DOMAIN = 'discovery' diff --git a/requirements_all.txt b/requirements_all.txt index 342bbebb08f..66b011e0440 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -553,7 +553,7 @@ nad_receiver==0.0.9 nanoleaf==0.4.1 # homeassistant.components.discovery -netdisco==1.4.0 +netdisco==1.4.1 # homeassistant.components.sensor.neurio_energy neurio==0.3.1 From c7166241f73a969c7ce01fb49a4ae534b16a92b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 May 2018 13:12:12 -0400 Subject: [PATCH 011/144] Ignore more loading errors (#14331) --- homeassistant/loader.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index e94fb2d6833..67647a323c9 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -105,7 +105,16 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: except ImportError as err: # This error happens if for example custom_components/switch # exists and we try to load switch.demo. - if str(err) != "No module named '{}'".format(path): + # Ignore errors for custom_components, custom_components.switch + # and custom_components.switch.demo. + white_listed_errors = [] + parts = [] + for part in path.split('.'): + parts.append(part) + white_listed_errors.append( + "No module named '{}'".format('.'.join(parts))) + + if str(err) not in white_listed_errors: _LOGGER.exception( ("Error loading %s. Make sure all " "dependencies are installed"), path) From e7c7b9b2a947e177eb8d40331983ab3bfdf48359 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 7 May 2018 11:18:51 -0600 Subject: [PATCH 012/144] Adds unique ID to Roku for entity registry inclusion (#14325) * Adds unique ID to Roku for entity registry inclusion * Owner-requested changes --- homeassistant/components/media_player/roku.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/media_player/roku.py b/homeassistant/components/media_player/roku.py index 87129f30db5..a46e781de59 100644 --- a/homeassistant/components/media_player/roku.py +++ b/homeassistant/components/media_player/roku.py @@ -146,6 +146,11 @@ class RokuDevice(MediaPlayerDevice): """Flag media player features that are supported.""" return SUPPORT_ROKU + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.device_info.sernum + @property def media_content_type(self): """Content type of current playing media.""" From 48b13cc86501e09ddd6694faa08193b048b55609 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 8 May 2018 01:26:46 -0600 Subject: [PATCH 013/144] Update hitron_coda.py to fix login for Shaw modems (#14306) I have a Hitron modem provided by Shaw communications rather than from Rogers as the Docs specify for this device_tracker but it seems like the api/code is all the same except that the login failed due to the password being passed as "pws" instead of "pwd". Making that one character change allowed HASS to read the connected device details from my Hitron modem. If this difference is actually one that stands between the Rogers-provided Hitron modems and the Shaw-provided variant, I am happy to create another device-tracker file for the Shaw modem. I just figured I would go with the simplest solution first. --- .../components/device_tracker/hitron_coda.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py index aa437eeef86..c9cd30cdb25 100644 --- a/homeassistant/components/device_tracker/hitron_coda.py +++ b/homeassistant/components/device_tracker/hitron_coda.py @@ -14,15 +14,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner) from homeassistant.const import ( - CONF_HOST, CONF_PASSWORD, CONF_USERNAME + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE ) _LOGGER = logging.getLogger(__name__) +DEFAULT_TYPE = "rogers" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TYPE, default=DEFAULT_TYPE): cv.string, }) @@ -49,6 +52,11 @@ class HitronCODADeviceScanner(DeviceScanner): self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) + if config.get(CONF_TYPE) == "shaw": + self._type = 'pwd' + else: + self.type = 'pws' + self._userid = None self.success_init = self._update_info() @@ -74,7 +82,7 @@ class HitronCODADeviceScanner(DeviceScanner): try: data = [ ('user', self._username), - ('pws', self._password), + (self._type, self._password), ] res = requests.post(self._loginurl, data=data, timeout=10) except requests.exceptions.Timeout: From ba7333e804210487f92e8df1e34cf28228812970 Mon Sep 17 00:00:00 2001 From: Gerard Date: Tue, 8 May 2018 09:52:21 +0200 Subject: [PATCH 014/144] Add sensors for BMW electric cars (#14293) * Add sensors for electric cars * Updates based on review of @MartinHjelmare * Fix Travis error * Another fix for Travis --- .../binary_sensor/bmw_connected_drive.py | 75 +++++++++++++++++-- .../bmw_connected_drive/__init__.py | 2 +- .../components/sensor/bmw_connected_drive.py | 69 ++++++++++------- requirements_all.txt | 2 +- 4 files changed, 114 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index 0abf6eb1064..af3ebd53b80 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -17,9 +17,19 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { 'lids': ['Doors', 'opening'], 'windows': ['Windows', 'opening'], - 'door_lock_state': ['Door lock state', 'safety'] + 'door_lock_state': ['Door lock state', 'safety'], + 'lights_parking': ['Parking lights', 'light'], + 'condition_based_services': ['Condition based services', 'problem'], + 'check_control_messages': ['Control messages', 'problem'] } +SENSOR_TYPES_ELEC = { + 'charging_status': ['Charging status', 'power'], + 'connection_status': ['Connection status', 'plug'] +} + +SENSOR_TYPES_ELEC.update(SENSOR_TYPES) + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -29,10 +39,18 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) - devices.append(device) + if vehicle.has_hv_battery: + _LOGGER.debug('BMW with a high voltage battery') + for key, value in sorted(SENSOR_TYPES_ELEC.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) + elif vehicle.has_internal_combustion_engine: + _LOGGER.debug('BMW with an internal combustion engine') + for key, value in sorted(SENSOR_TYPES.items()): + device = BMWConnectedDriveSensor(account, vehicle, key, + value[0], value[1]) + devices.append(device) add_devices(devices, True) @@ -92,12 +110,41 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result[window.name] = window.state.value elif self._attribute == 'door_lock_state': result['door_lock_state'] = vehicle_state.door_lock_state.value + result['last_update_reason'] = vehicle_state.last_update_reason + elif self._attribute == 'lights_parking': + result['lights_parking'] = vehicle_state.parking_lights.value + elif self._attribute == 'condition_based_services': + for report in vehicle_state.condition_based_services: + service_type = report.service_type.lower().replace('_', ' ') + result['{} status'.format(service_type)] = report.state.value + if report.due_date is not None: + result['{} date'.format(service_type)] = \ + report.due_date.strftime('%Y-%m-%d') + if report.due_distance is not None: + result['{} distance'.format(service_type)] = \ + '{} km'.format(report.due_distance) + elif self._attribute == 'check_control_messages': + check_control_messages = vehicle_state.check_control_messages + if not check_control_messages: + result['check_control_messages'] = 'OK' + else: + result['check_control_messages'] = check_control_messages + elif self._attribute == 'charging_status': + result['charging_status'] = vehicle_state.charging_status.value + # pylint: disable=W0212 + result['last_charging_end_result'] = \ + vehicle_state._attributes['lastChargingEndResult'] + if self._attribute == 'connection_status': + # pylint: disable=W0212 + result['connection_status'] = \ + vehicle_state._attributes['connectionStatus'] return result def update(self): """Read new state data from the library.""" from bimmer_connected.state import LockState + from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state # device class opening: On means open, Off means closed @@ -111,6 +158,24 @@ class BMWConnectedDriveSensor(BinarySensorDevice): # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED self._state = vehicle_state.door_lock_state not in \ [LockState.LOCKED, LockState.SECURED] + # device class light: On means light detected, Off means no light + if self._attribute == 'lights_parking': + self._state = vehicle_state.are_parking_lights_on + # device class problem: On means problem detected, Off means no problem + if self._attribute == 'condition_based_services': + self._state = not vehicle_state.are_all_cbs_ok + if self._attribute == 'check_control_messages': + self._state = vehicle_state.has_check_control_messages + # device class power: On means power detected, Off means no power + if self._attribute == 'charging_status': + self._state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + # device class plug: On means device is plugged in, + # Off means device is unplugged + if self._attribute == 'connection_status': + # pylint: disable=W0212 + self._state = (vehicle_state._attributes['connectionStatus'] == + 'CONNECTED') def update_callback(self): """Schedule a state update.""" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 347bab6f529..a7ed262ac2c 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers import discovery from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['bimmer_connected==0.5.0'] +REQUIREMENTS = ['bimmer_connected==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index ed75520c179..8e06836b102 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -9,22 +9,12 @@ import logging from homeassistant.components.bmw_connected_drive import DOMAIN as BMW_DOMAIN from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) -LENGTH_ATTRIBUTES = { - 'remaining_range_fuel': ['Range (fuel)', 'mdi:ruler'], - 'mileage': ['Mileage', 'mdi:speedometer'] -} - -VALID_ATTRIBUTES = { - 'remaining_fuel': ['Remaining Fuel', 'mdi:gas-station'] -} - -VALID_ATTRIBUTES.update(LENGTH_ATTRIBUTES) - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -34,27 +24,26 @@ def setup_platform(hass, config, add_devices, discovery_info=None): devices = [] for account in accounts: for vehicle in account.account.vehicles: - for key, value in sorted(VALID_ATTRIBUTES.items()): - device = BMWConnectedDriveSensor(account, vehicle, key, - value[0], value[1]) + for attribute_name in vehicle.drive_train_attributes: + device = BMWConnectedDriveSensor(account, vehicle, + attribute_name) devices.append(device) + device = BMWConnectedDriveSensor(account, vehicle, 'mileage') + devices.append(device) add_devices(devices, True) class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, icon): + def __init__(self, account, vehicle, attribute: str): """Constructor.""" self._vehicle = vehicle self._account = account self._attribute = attribute self._state = None - self._unit_of_measurement = None self._name = '{} {}'.format(self._vehicle.name, self._attribute) self._unique_id = '{}-{}'.format(self._vehicle.vin, self._attribute) - self._sensor_name = sensor_name - self._icon = icon @property def should_poll(self) -> bool: @@ -74,7 +63,27 @@ class BMWConnectedDriveSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return self._icon + from bimmer_connected.state import ChargingState + vehicle_state = self._vehicle.state + charging_state = vehicle_state.charging_status in \ + [ChargingState.CHARGING] + + if self._attribute == 'mileage': + return 'mdi:speedometer' + elif self._attribute in ( + 'remaining_range_total', 'remaining_range_electric', + 'remaining_range_fuel', 'max_range_electric'): + return 'mdi:ruler' + elif self._attribute == 'remaining_fuel': + return 'mdi:gas-station' + elif self._attribute == 'charging_time_remaining': + return 'mdi:update' + elif self._attribute == 'charging_status': + return 'mdi:battery-charging' + elif self._attribute == 'charging_level_hv': + return icon_for_battery_level( + battery_level=vehicle_state.charging_level_hv, + charging=charging_state) @property def state(self): @@ -88,7 +97,17 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - return self._unit_of_measurement + if self._attribute in ( + 'mileage', 'remaining_range_total', 'remaining_range_electric', + 'remaining_range_fuel', 'max_range_electric'): + return 'km' + elif self._attribute == 'remaining_fuel': + return 'l' + elif self._attribute == 'charging_time_remaining': + return 'h' + elif self._attribute == 'charging_level_hv': + return '%' + return None @property def device_state_attributes(self): @@ -101,14 +120,10 @@ class BMWConnectedDriveSensor(Entity): """Read new state data from the library.""" _LOGGER.debug('Updating %s', self._vehicle.name) vehicle_state = self._vehicle.state - self._state = getattr(vehicle_state, self._attribute) - - if self._attribute in LENGTH_ATTRIBUTES: - self._unit_of_measurement = 'km' - elif self._attribute == 'remaining_fuel': - self._unit_of_measurement = 'l' + if self._attribute == 'charging_status': + self._state = getattr(vehicle_state, self._attribute).value else: - self._unit_of_measurement = None + self._state = getattr(vehicle_state, self._attribute) def update_callback(self): """Schedule a state update.""" diff --git a/requirements_all.txt b/requirements_all.txt index 66b011e0440..2db7a66d7b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -149,7 +149,7 @@ beautifulsoup4==4.6.0 bellows==0.6.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.0 +bimmer_connected==0.5.1 # homeassistant.components.blink blinkpy==0.6.0 From 230bd3929c716aeed1e9dbca0bf1154e0821fade Mon Sep 17 00:00:00 2001 From: Mattias Welponer Date: Tue, 8 May 2018 09:57:51 +0200 Subject: [PATCH 015/144] Add more homematicip cloud components (#14084) * Add support for shutter contact and motion detector device * Add support for power switch devices * Add support for light switch device * Cleanup binary_switch and light platform * Update comment --- .../binary_sensor/homematicip_cloud.py | 85 +++++++++++++++++++ homeassistant/components/homematicip_cloud.py | 5 +- .../components/light/homematicip_cloud.py | 76 +++++++++++++++++ .../components/switch/homematicip_cloud.py | 84 ++++++++++++++++++ 4 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/binary_sensor/homematicip_cloud.py create mode 100644 homeassistant/components/light/homematicip_cloud.py create mode 100644 homeassistant/components/switch/homematicip_cloud.py diff --git a/homeassistant/components/binary_sensor/homematicip_cloud.py b/homeassistant/components/binary_sensor/homematicip_cloud.py new file mode 100644 index 00000000000..40ffe498402 --- /dev/null +++ b/homeassistant/components/binary_sensor/homematicip_cloud.py @@ -0,0 +1,85 @@ +""" +Support for HomematicIP binary sensor. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_WINDOW_STATE = 'window_state' +ATTR_EVENT_DELAY = 'event_delay' +ATTR_MOTION_DETECTED = 'motion_detected' +ATTR_ILLUMINATION = 'illumination' + +HMIP_OPEN = 'open' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP binary sensor devices.""" + from homematicip.device import (ShutterContact, MotionDetectorIndoor) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, ShutterContact): + devices.append(HomematicipShutterContact(home, device)) + elif isinstance(device, MotionDetectorIndoor): + devices.append(HomematicipMotionDetector(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): + """HomematicIP shutter contact.""" + + def __init__(self, home, device): + """Initialize the shutter contact.""" + super().__init__(home, device) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'door' + + @property + def is_on(self): + """Return true if the shutter contact is on/open.""" + if self._device.sabotage: + return True + if self._device.windowState is None: + return None + return self._device.windowState.lower() == HMIP_OPEN + + +class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): + """MomematicIP motion detector.""" + + def __init__(self, home, device): + """Initialize the shutter contact.""" + super().__init__(home, device) + + @property + def device_class(self): + """Return the class of this sensor.""" + return 'motion' + + @property + def is_on(self): + """Return true if motion is detected.""" + if self._device.sabotage: + return True + return self._device.motionDetected diff --git a/homeassistant/components/homematicip_cloud.py b/homeassistant/components/homematicip_cloud.py index 0b15d7a3dfe..d85d867d8f8 100644 --- a/homeassistant/components/homematicip_cloud.py +++ b/homeassistant/components/homematicip_cloud.py @@ -24,7 +24,10 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = 'homematicip_cloud' COMPONENTS = [ - 'sensor' + 'sensor', + 'binary_sensor', + 'switch', + 'light' ] CONF_NAME = 'name' diff --git a/homeassistant/components/light/homematicip_cloud.py b/homeassistant/components/light/homematicip_cloud.py new file mode 100644 index 00000000000..e433da44ae7 --- /dev/null +++ b/homeassistant/components/light/homematicip_cloud.py @@ -0,0 +1,76 @@ +""" +Support for HomematicIP light. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/light.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.light import Light +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGIE_COUNTER = 'energie_counter' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP light devices.""" + from homematicip.device import ( + BrandSwitchMeasuring) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, BrandSwitchMeasuring): + devices.append(HomematicipLightMeasuring(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipLight(HomematicipGenericDevice, Light): + """MomematicIP light device.""" + + def __init__(self, home, device): + """Initialize the light device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipLightMeasuring(HomematicipLight): + """MomematicIP measuring light device.""" + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.currentPowerConsumption + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if self._device.energyCounter is None: + return 0 + return round(self._device.energyCounter) diff --git a/homeassistant/components/switch/homematicip_cloud.py b/homeassistant/components/switch/homematicip_cloud.py new file mode 100644 index 00000000000..9123d46c87b --- /dev/null +++ b/homeassistant/components/switch/homematicip_cloud.py @@ -0,0 +1,84 @@ +""" +Support for HomematicIP switch. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.homematicip_cloud/ +""" + +import logging + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.homematicip_cloud import ( + HomematicipGenericDevice, DOMAIN as HOMEMATICIP_CLOUD_DOMAIN, + ATTR_HOME_ID) + +DEPENDENCIES = ['homematicip_cloud'] + +_LOGGER = logging.getLogger(__name__) + +ATTR_POWER_CONSUMPTION = 'power_consumption' +ATTR_ENERGIE_COUNTER = 'energie_counter' +ATTR_PROFILE_MODE = 'profile_mode' + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the HomematicIP switch devices.""" + from homematicip.device import ( + PlugableSwitch, PlugableSwitchMeasuring, + BrandSwitchMeasuring) + + if discovery_info is None: + return + home = hass.data[HOMEMATICIP_CLOUD_DOMAIN][discovery_info[ATTR_HOME_ID]] + devices = [] + for device in home.devices: + if isinstance(device, BrandSwitchMeasuring): + # BrandSwitchMeasuring inherits PlugableSwitchMeasuring + # This device is implemented in the light platform and will + # not be added in the switch platform + pass + elif isinstance(device, PlugableSwitchMeasuring): + devices.append(HomematicipSwitchMeasuring(home, device)) + elif isinstance(device, PlugableSwitch): + devices.append(HomematicipSwitch(home, device)) + + if devices: + async_add_devices(devices) + + +class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): + """MomematicIP switch device.""" + + def __init__(self, home, device): + """Initialize the switch device.""" + super().__init__(home, device) + + @property + def is_on(self): + """Return true if device is on.""" + return self._device.on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + await self._device.turn_on() + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + await self._device.turn_off() + + +class HomematicipSwitchMeasuring(HomematicipSwitch): + """MomematicIP measuring switch device.""" + + @property + def current_power_w(self): + """Return the current power usage in W.""" + return self._device.currentPowerConsumption + + @property + def today_energy_kwh(self): + """Return the today total energy usage in kWh.""" + if self._device.energyCounter is None: + return 0 + return round(self._device.energyCounter) From 434365974243e8671b8d8944de02c91b95f40abc Mon Sep 17 00:00:00 2001 From: m4dmin <39031482+m4dmin@users.noreply.github.com> Date: Tue, 8 May 2018 13:43:07 +0200 Subject: [PATCH 016/144] add 2 devices (#14321) * add 2 devices io:RollerShutterUnoIOComponent io:ExteriorVenetianBlindIOComponent * add 2 devices * Update tahoma.py * Fix hounci-bot violation * Fixed Travis CI build failure ./homeassistant/components/cover/tahoma.py:83:13: E125 continuation line with same indent as next logical line * Fixed Travis CI build failure E125 continuation line with same indent as next logical line * Fixed Travis CI build failure E127 continuation line over-indented for visual indent * Fix indent * Change check --- homeassistant/components/cover/tahoma.py | 4 +++- homeassistant/components/tahoma.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cover/tahoma.py b/homeassistant/components/cover/tahoma.py index 20625143daf..cf8b7dfad48 100644 --- a/homeassistant/components/cover/tahoma.py +++ b/homeassistant/components/cover/tahoma.py @@ -79,7 +79,9 @@ class TahomaCover(TahomaDevice, CoverDevice): if self.tahoma_device.type == \ 'io:RollerShutterWithLowSpeedManagementIOComponent': self.apply_action('setPosition', 'secured') - elif self.tahoma_device.type == 'rts:BlindRTSComponent': + elif self.tahoma_device.type in \ + ('rts:BlindRTSComponent', + 'io:ExteriorVenetianBlindIOComponent'): self.apply_action('my') else: self.apply_action('stopIdentify') diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 9848d20094c..84edd9afd40 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -40,6 +40,8 @@ TAHOMA_TYPES = { 'rts:CurtainRTSComponent': 'cover', 'rts:BlindRTSComponent': 'cover', 'rts:VenetianBlindRTSComponent': 'cover', + 'io:ExteriorVenetianBlindIOComponent': 'cover', + 'io:RollerShutterUnoIOComponent': 'cover', 'io:RollerShutterWithLowSpeedManagementIOComponent': 'cover', 'io:RollerShutterVeluxIOComponent': 'cover', 'io:RollerShutterGenericIOComponent': 'cover', From eb551a6d5afa5581d26ae739a79787cc363c87d1 Mon Sep 17 00:00:00 2001 From: David Broadfoot Date: Wed, 9 May 2018 01:42:18 +1000 Subject: [PATCH 017/144] Gogogate2 0.1.1 (#14294) * Gogogate2 - bump version Uses latest version of library which ensures commands to device are idempotent * Update requirements_all * Expose sensor temperature * update version * import attribute * Set temperature * Remove temperature attribute Removed temperature attribute until it can be re-implemented as a separate sensor. * Update ordering * Fix copy-&-paste issue --- homeassistant/components/cover/gogogate2.py | 13 +++++++------ requirements_all.txt | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cover/gogogate2.py b/homeassistant/components/cover/gogogate2.py index 688df62ca6a..2b91591e71b 100644 --- a/homeassistant/components/cover/gogogate2.py +++ b/homeassistant/components/cover/gogogate2.py @@ -1,5 +1,5 @@ """ -Support for Gogogate2 Garage Doors. +Support for Gogogate2 garage Doors. For more details about this platform, please refer to the documentation https://home-assistant.io/components/cover.gogogate2/ @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pygogogate2==0.0.7'] +REQUIREMENTS = ['pygogogate2==0.1.1'] _LOGGER = logging.getLogger(__name__) @@ -25,9 +25,9 @@ NOTIFICATION_ID = 'gogogate2_notification' NOTIFICATION_TITLE = 'Gogogate2 Cover Setup' COVER_SCHEMA = vol.Schema({ - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) @@ -36,10 +36,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Gogogate2 component.""" from pygogogate2 import Gogogate2API as pygogogate2 - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) ip_address = config.get(CONF_IP_ADDRESS) name = config.get(CONF_NAME) + password = config.get(CONF_PASSWORD) + username = config.get(CONF_USERNAME) + mygogogate2 = pygogogate2(username, password, ip_address) try: diff --git a/requirements_all.txt b/requirements_all.txt index 2db7a66d7b2..0dcfdbcce70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -784,7 +784,7 @@ pyfritzhome==0.3.7 pyfttt==0.3 # homeassistant.components.cover.gogogate2 -pygogogate2==0.0.7 +pygogogate2==0.1.1 # homeassistant.components.remote.harmony pyharmony==1.0.20 From c664c20165ebeb248b98716cf61e865f274a2dac Mon Sep 17 00:00:00 2001 From: Tod Schmidt Date: Tue, 8 May 2018 11:43:31 -0400 Subject: [PATCH 018/144] Snips: Added slot values for siteId and probability (#14315) * Added solt values for siteId and probability * Update snips.py * Update test_snips.py --- homeassistant/components/snips.py | 2 ++ tests/components/test_snips.py | 54 ++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/snips.py b/homeassistant/components/snips.py index 812906e7be9..4f50c6beaaa 100644 --- a/homeassistant/components/snips.py +++ b/homeassistant/components/snips.py @@ -131,6 +131,8 @@ async def async_setup(hass, config): slots = {} for slot in request.get('slots', []): slots[slot['slotName']] = {'value': resolve_slot_values(slot)} + slots['site_id'] = {'value': request.get('siteId')} + slots['probability'] = {'value': request['intent']['probability']} try: intent_response = await intent.async_handle( diff --git a/tests/components/test_snips.py b/tests/components/test_snips.py index 2342e897708..d9238336768 100644 --- a/tests/components/test_snips.py +++ b/tests/components/test_snips.py @@ -118,7 +118,9 @@ async def test_snips_intent(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'Lights' - assert intent.slots == {'light_color': {'value': 'green'}} + assert intent.slots == {'light_color': {'value': 'green'}, + 'probability': {'value': 1}, + 'site_id': {'value': None}} assert intent.text_input == 'turn the lights green' @@ -169,7 +171,9 @@ async def test_snips_intent_with_duration(hass, mqtt_mock): intent = intents[0] assert intent.platform == 'snips' assert intent.intent_type == 'SetTimer' - assert intent.slots == {'timer_duration': {'value': 300}} + assert intent.slots == {'probability': {'value': 1}, + 'site_id': {'value': None}, + 'timer_duration': {'value': 300}} async def test_intent_speech_response(hass, mqtt_mock): @@ -318,11 +322,51 @@ async def test_snips_low_probability(hass, mqtt_mock, caplog): assert 'Intent below probaility threshold 0.49 < 0.5' in caplog.text +async def test_intent_special_slots(hass, mqtt_mock): + """Test intent special slot values via Snips.""" + calls = async_mock_service(hass, 'light', 'turn_on') + result = await async_setup_component(hass, "snips", { + "snips": {}, + }) + assert result + result = await async_setup_component(hass, "intent_script", { + "intent_script": { + "Lights": { + "action": { + "service": "light.turn_on", + "data_template": { + "probability": "{{ probability }}", + "site_id": "{{ site_id }}" + } + } + } + } + }) + assert result + payload = """ + { + "input": "turn the light on", + "intent": { + "intentName": "Lights", + "probability": 0.85 + }, + "siteId": "default", + "slots": [] + } + """ + async_fire_mqtt_message(hass, 'hermes/intent/Lights', payload) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].domain == 'light' + assert calls[0].service == 'turn_on' + assert calls[0].data['probability'] == '0.85' + assert calls[0].data['site_id'] == 'default' + + async def test_snips_say(hass, caplog): """Test snips say with invalid config.""" - calls = async_mock_service(hass, 'snips', 'say', - snips.SERVICE_SCHEMA_SAY) - + calls = async_mock_service(hass, 'snips', 'say', snips.SERVICE_SCHEMA_SAY) data = {'text': 'Hello'} await hass.services.async_call('snips', 'say', data) await hass.async_block_till_done() From 6199e50e8051992ba0fc661babc66c3ac9069e5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 11:55:04 -0400 Subject: [PATCH 019/144] Fix Insteon PLM coverage --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 9030cc9a097..1f2a8f8d233 100644 --- a/.coveragerc +++ b/.coveragerc @@ -127,7 +127,7 @@ omit = homeassistant/components/insteon_local.py homeassistant/components/*/insteon_local.py - homeassistant/components/insteon_plm.py + homeassistant/components/insteon_plm/* homeassistant/components/*/insteon_plm.py homeassistant/components/ios.py From ff01aa40c93faf4659ffb1ec8729180cb1dabeeb Mon Sep 17 00:00:00 2001 From: stephanerosi Date: Tue, 8 May 2018 19:24:27 +0200 Subject: [PATCH 020/144] Add help for conversation/process service (#14323) * Add help for conversation/process service * Add logging to debug text received when service is called * Move conversation to specific folder --- .../{conversation.py => conversation/__init__.py} | 1 + homeassistant/components/conversation/services.yaml | 10 ++++++++++ 2 files changed, 11 insertions(+) rename homeassistant/components/{conversation.py => conversation/__init__.py} (99%) create mode 100644 homeassistant/components/conversation/services.yaml diff --git a/homeassistant/components/conversation.py b/homeassistant/components/conversation/__init__.py similarity index 99% rename from homeassistant/components/conversation.py rename to homeassistant/components/conversation/__init__.py index ddd96c99177..9cb00a84583 100644 --- a/homeassistant/components/conversation.py +++ b/homeassistant/components/conversation/__init__.py @@ -96,6 +96,7 @@ async def async_setup(hass, config): async def process(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] + _LOGGER.debug('Processing: <%s>', text) try: await _process(hass, text) except intent.IntentHandleError as err: diff --git a/homeassistant/components/conversation/services.yaml b/homeassistant/components/conversation/services.yaml new file mode 100644 index 00000000000..a1b980d8e05 --- /dev/null +++ b/homeassistant/components/conversation/services.yaml @@ -0,0 +1,10 @@ +# Describes the format for available component services + +process: + description: Launch a conversation from a transcribed text. + fields: + text: + description: Transcribed text + example: Turn all lights on + + From e12994a0cdc4855173ad8ee9055bdfd3205ed799 Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Wed, 9 May 2018 03:35:55 +1000 Subject: [PATCH 021/144] Fix BOM weather '-' value (#14042) --- homeassistant/components/sensor/bom.py | 44 +++++++---- homeassistant/components/weather/bom.py | 14 ++-- tests/components/sensor/test_bom.py | 97 +++++++++++++++++++++++++ tests/fixtures/bom_weather.json | 42 +++++++++++ 4 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 tests/components/sensor/test_bom.py create mode 100644 tests/fixtures/bom_weather.json diff --git a/homeassistant/components/sensor/bom.py b/homeassistant/components/sensor/bom.py index 128f532e459..d6764e5e994 100644 --- a/homeassistant/components/sensor/bom.py +++ b/homeassistant/components/sensor/bom.py @@ -19,8 +19,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, STATE_UNKNOWN, CONF_NAME, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE) + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, CONF_NAME, ATTR_ATTRIBUTION, + CONF_LATITUDE, CONF_LONGITUDE) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -145,21 +145,18 @@ class BOMCurrentSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self.bom_data.data and self._condition in self.bom_data.data: - return self.bom_data.data[self._condition] - - return STATE_UNKNOWN + return self.bom_data.get_reading(self._condition) @property def device_state_attributes(self): """Return the state attributes of the device.""" attr = {} attr['Sensor Id'] = self._condition - attr['Zone Id'] = self.bom_data.data['history_product'] - attr['Station Id'] = self.bom_data.data['wmo'] - attr['Station Name'] = self.bom_data.data['name'] + attr['Zone Id'] = self.bom_data.latest_data['history_product'] + attr['Station Id'] = self.bom_data.latest_data['wmo'] + attr['Station Name'] = self.bom_data.latest_data['name'] attr['Last Update'] = datetime.datetime.strptime(str( - self.bom_data.data['local_date_time_full']), '%Y%m%d%H%M%S') + self.bom_data.latest_data['local_date_time_full']), '%Y%m%d%H%M%S') attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attr @@ -180,22 +177,43 @@ class BOMCurrentData(object): """Initialize the data object.""" self._hass = hass self._zone_id, self._wmo_id = station_id.split('.') - self.data = None + self._data = None def _build_url(self): url = _RESOURCE.format(self._zone_id, self._zone_id, self._wmo_id) _LOGGER.info("BOM URL %s", url) return url + @property + def latest_data(self): + """Return the latest data object.""" + if self._data: + return self._data[0] + return None + + def get_reading(self, condition): + """Return the value for the given condition. + + BOM weather publishes condition readings for weather (and a few other + conditions) at intervals throughout the day. To avoid a `-` value in + the frontend for these conditions, we traverse the historical data + for the latest value that is not `-`. + + Iterators are used in this method to avoid iterating needlessly + iterating through the entire BOM provided dataset + """ + condition_readings = (entry[condition] for entry in self._data) + return next((x for x in condition_readings if x != '-'), None) + @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from BOM.""" try: result = requests.get(self._build_url(), timeout=10).json() - self.data = result['observations']['data'][0] + self._data = result['observations']['data'] except ValueError as err: _LOGGER.error("Check BOM %s", err.args) - self.data = None + self._data = None raise diff --git a/homeassistant/components/weather/bom.py b/homeassistant/components/weather/bom.py index 236aeb2fa2e..ad74bb4fb77 100644 --- a/homeassistant/components/weather/bom.py +++ b/homeassistant/components/weather/bom.py @@ -48,7 +48,7 @@ class BOMWeather(WeatherEntity): def __init__(self, bom_data, stationname=None): """Initialise the platform with a data instance and station name.""" self.bom_data = bom_data - self.stationname = stationname or self.bom_data.data.get('name') + self.stationname = stationname or self.bom_data.latest_data.get('name') def update(self): """Update current conditions.""" @@ -62,14 +62,14 @@ class BOMWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - return self.bom_data.data.get('weather') + return self.bom_data.get_reading('weather') # Now implement the WeatherEntity interface @property def temperature(self): """Return the platform temperature.""" - return self.bom_data.data.get('air_temp') + return self.bom_data.get_reading('air_temp') @property def temperature_unit(self): @@ -79,17 +79,17 @@ class BOMWeather(WeatherEntity): @property def pressure(self): """Return the mean sea-level pressure.""" - return self.bom_data.data.get('press_msl') + return self.bom_data.get_reading('press_msl') @property def humidity(self): """Return the relative humidity.""" - return self.bom_data.data.get('rel_hum') + return self.bom_data.get_reading('rel_hum') @property def wind_speed(self): """Return the wind speed.""" - return self.bom_data.data.get('wind_spd_kmh') + return self.bom_data.get_reading('wind_spd_kmh') @property def wind_bearing(self): @@ -99,7 +99,7 @@ class BOMWeather(WeatherEntity): 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] wind = {name: idx * 360 / 16 for idx, name in enumerate(directions)} - return wind.get(self.bom_data.data.get('wind_dir')) + return wind.get(self.bom_data.get_reading('wind_dir')) @property def attribution(self): diff --git a/tests/components/sensor/test_bom.py b/tests/components/sensor/test_bom.py new file mode 100644 index 00000000000..06a7089e052 --- /dev/null +++ b/tests/components/sensor/test_bom.py @@ -0,0 +1,97 @@ +"""The tests for the BOM Weather sensor platform.""" +import re +import unittest +import json +import requests +from unittest.mock import patch +from urllib.parse import urlparse + +from homeassistant.setup import setup_component +from homeassistant.components import sensor + +from tests.common import ( + get_test_home_assistant, assert_setup_component, load_fixture) + +VALID_CONFIG = { + 'platform': 'bom', + 'station': 'IDN60901.94767', + 'name': 'Fake', + 'monitored_conditions': [ + 'apparent_t', + 'press', + 'weather' + ] +} + + +def mocked_requests(*args, **kwargs): + """Mock requests.get invocations.""" + class MockResponse: + """Class to represent a mocked response.""" + + def __init__(self, json_data, status_code): + """Initialize the mock response class.""" + self.json_data = json_data + self.status_code = status_code + + def json(self): + """Return the json of the response.""" + return self.json_data + + @property + def content(self): + """Return the content of the response.""" + return self.json() + + def raise_for_status(self): + """Raise an HTTPError if status is not 200.""" + if self.status_code != 200: + raise requests.HTTPError(self.status_code) + + url = urlparse(args[0]) + if re.match(r'^/fwo/[\w]+/[\w.]+\.json', url.path): + return MockResponse(json.loads(load_fixture('bom_weather.json')), 200) + + raise NotImplementedError('Unknown route {}'.format(url.path)) + + +class TestBOMWeatherSensor(unittest.TestCase): + """Test the BOM Weather sensor.""" + + def setUp(self): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.config = VALID_CONFIG + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + @patch('requests.get', side_effect=mocked_requests) + def test_setup(self, mock_get): + """Test the setup with custom settings.""" + with assert_setup_component(1, sensor.DOMAIN): + self.assertTrue(setup_component(self.hass, sensor.DOMAIN, { + 'sensor': VALID_CONFIG})) + + fake_entities = [ + 'bom_fake_feels_like_c', + 'bom_fake_pressure_mb', + 'bom_fake_weather'] + + for entity_id in fake_entities: + state = self.hass.states.get('sensor.{}'.format(entity_id)) + self.assertIsNotNone(state) + + @patch('requests.get', side_effect=mocked_requests) + def test_sensor_values(self, mock_get): + """Test retrieval of sensor values.""" + self.assertTrue(setup_component( + self.hass, sensor.DOMAIN, {'sensor': VALID_CONFIG})) + + self.assertEqual('Fine', self.hass.states.get( + 'sensor.bom_fake_weather').state) + self.assertEqual('1021.7', self.hass.states.get( + 'sensor.bom_fake_pressure_mb').state) + self.assertEqual('25.0', self.hass.states.get( + 'sensor.bom_fake_feels_like_c').state) diff --git a/tests/fixtures/bom_weather.json b/tests/fixtures/bom_weather.json new file mode 100644 index 00000000000..d40ea6fb21a --- /dev/null +++ b/tests/fixtures/bom_weather.json @@ -0,0 +1,42 @@ +{ + "observations": { + "data": [ + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 25.0, + "press": 1021.7, + "weather": "-" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 22.0, + "press": 1019.7, + "weather": "-" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 20.0, + "press": 1011.7, + "weather": "Fine" + }, + { + "wmo": 94767, + "name": "Fake", + "history_product": "IDN00000", + "local_date_time_full": "20180422130000", + "apparent_t": 18.0, + "press": 1010.0, + "weather": "-" + } + ] + } +} From 10505d542ad49fbb4643d4f0cb3ae8b13477f2e2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 8 May 2018 22:30:28 +0300 Subject: [PATCH 022/144] Make sure zwave nodes/entities enter the registry is proper state. (#14251) * When zwave node's info is parsed remove it and re-add back. * Delay value entity if not ready * If node is ready consider it parsed even if manufacturer/product are missing. * Add annotations --- homeassistant/components/zwave/__init__.py | 81 ++++++++++++------- homeassistant/components/zwave/node_entity.py | 16 +++- homeassistant/components/zwave/util.py | 23 ++++++ tests/components/zwave/test_init.py | 2 +- 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 01b17023c12..7562ac0ff14 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,7 +11,7 @@ from pprint import pprint import voluptuous as vol -from homeassistant.core import CoreState +from homeassistant.core import callback, CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id @@ -31,7 +31,8 @@ from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS -from .util import check_node_schema, check_value_schema, node_name +from .util import (check_node_schema, check_value_schema, node_name, + check_has_unique_id, is_node_parsed) REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] @@ -313,30 +314,22 @@ def setup(hass, config): _add_node_to_component() return - async def _check_node_ready(): - """Wait for node to be parsed.""" - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow()-start_time).total_seconds()) - - if entity.unique_id: - _LOGGER.info("Z-Wave node %d ready after %d seconds", - entity.node_id, waited) - break - elif waited >= const.NODE_READY_WAIT_SECS: - # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave - # node to be ready. - _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " - "continuing anyway", - entity.node_id, waited) - break - else: - await asyncio.sleep(1, loop=hass.loop) - + @callback + def _on_ready(sec): + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, sec) hass.async_add_job(_add_node_to_component) - hass.add_job(_check_node_ready) + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, sec) + hass.async_add_job(_add_node_to_component) + + hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout, + hass.loop) def network_ready(): """Handle the query of all awake nodes.""" @@ -839,13 +832,35 @@ class ZWaveDeviceEntityValues(): dict_id = id(self) + @callback + def _on_ready(sec): + _LOGGER.info( + "Z-Wave entity %s (node_id: %d) ready after %d seconds", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave entity %s (node_id: %d) not ready after %d seconds, " + "continuing anyway", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) - self._hass.add_job(discover_device, component, device, dict_id) + + if device.unique_id: + self._hass.add_job(discover_device, component, device, dict_id) + else: + self._hass.add_job(check_has_unique_id, device, _on_ready, + _on_timeout, self._hass.loop) class ZWaveDeviceEntity(ZWaveBaseEntity): @@ -862,8 +877,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = self._compute_unique_id() self._update_attributes() dispatcher.connect( @@ -894,6 +908,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def _update_attributes(self): """Update the node attributes. May only be used inside callback.""" self.node_id = self.node.node_id + self._name = _value_name(self.values.primary) + if not self._unique_id: + self._unique_id = self._compute_unique_id() + if self._unique_id: + self.try_remove_and_add() if self.values.power: self.power_consumption = round( @@ -940,3 +959,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): for value in self.values: if value is not None: self.node.refresh_value(value.value_id) + + def _compute_unique_id(self): + if (is_node_parsed(self.node) and + self.values.primary.label != "Unknown") or \ + self.node.is_ready: + return "{}-{}".format(self.node.node_id, + self.values.primary.object_id) + return None diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index bcddcb0b800..2c6d26802bd 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -9,7 +9,7 @@ from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE) -from .util import node_name +from .util import node_name, is_node_parsed _LOGGER = logging.getLogger(__name__) @@ -65,6 +65,15 @@ class ZWaveBaseEntity(Entity): self._update_scheduled = True self.hass.loop.call_later(0.1, do_update) + def try_remove_and_add(self): + """Remove this entity and add it back.""" + async def _async_remove_and_add(): + await self.async_remove() + self.entity_id = None + await self.platform.async_add_entities([self]) + if self.hass and self.platform: + self.hass.add_job(_async_remove_and_add) + class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" @@ -151,6 +160,9 @@ class ZWaveNodeEntity(ZWaveBaseEntity): if not self._unique_id: self._unique_id = self._compute_unique_id() + if self._unique_id: + # Node info parsed. Remove and re-add + self.try_remove_and_add() self.maybe_schedule_update() @@ -243,6 +255,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return attrs def _compute_unique_id(self): - if self._manufacturer_name and self._product_name: + if is_node_parsed(self.node) or self.node.is_ready: return 'node-{}'.format(self.node_id) return None diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 8c74b731ad6..1c0bb14f7e5 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -1,6 +1,9 @@ """Zwave util methods.""" +import asyncio import logging +import homeassistant.util.dt as dt_util + from . import const _LOGGER = logging.getLogger(__name__) @@ -67,3 +70,23 @@ def node_name(node): """Return the name of the node.""" return node.name or '{} {}'.format( node.manufacturer_name, node.product_name) + + +async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): + """Wait for entity to have unique_id.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + if entity.unique_id: + ready_callback(waited) + return + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. + timeout_callback(waited) + return + await asyncio.sleep(1, loop=loop) + + +def is_node_parsed(node): + """Check whether the node has been parsed or still waiting to be parsed.""" + return node.manufacturer_name and node.product_name diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index faa7357bd8a..0eba19f03a4 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -237,7 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None) + node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) sleeps = [] From 9c7523d7b0b38895b4d405d6e5d1f922d66d0e8c Mon Sep 17 00:00:00 2001 From: Evgeniy <592652+evgeniy-khatko@users.noreply.github.com> Date: Tue, 8 May 2018 14:42:57 -0700 Subject: [PATCH 023/144] Improving icloud device tracker (#14078) * Improving icloud device tracker * Adding config validations for new values * Adding config validations for new values * Moving icloud specific setup to platform schema. Setting default in platform schema. --- .../components/device_tracker/icloud.py | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/device_tracker/icloud.py b/homeassistant/components/device_tracker/icloud.py index 5d40f5d533a..8ea81e88440 100644 --- a/homeassistant/components/device_tracker/icloud.py +++ b/homeassistant/components/device_tracker/icloud.py @@ -24,8 +24,9 @@ _LOGGER = logging.getLogger(__name__) REQUIREMENTS = ['pyicloud==0.9.1'] -CONF_IGNORED_DEVICES = 'ignored_devices' CONF_ACCOUNTNAME = 'account_name' +CONF_MAX_INTERVAL = 'max_interval' +CONF_GPS_ACCURACY_THRESHOLD = 'gps_accuracy_threshold' # entity attributes ATTR_ACCOUNTNAME = 'account_name' @@ -64,13 +65,15 @@ DEVICESTATUSCODES = { SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), vol.Optional(ATTR_DEVICENAME): cv.slugify, - vol.Optional(ATTR_INTERVAL): cv.positive_int, + vol.Optional(ATTR_INTERVAL): cv.positive_int }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, + vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int, + vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int }) @@ -79,8 +82,11 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) account = config.get(CONF_ACCOUNTNAME, slugify(username.partition('@')[0])) + max_interval = config.get(CONF_MAX_INTERVAL) + gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD) - icloudaccount = Icloud(hass, username, password, account, see) + icloudaccount = Icloud(hass, username, password, account, max_interval, + gps_accuracy_threshold, see) if icloudaccount.api is not None: ICLOUDTRACKERS[account] = icloudaccount @@ -96,6 +102,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].lost_iphone(devicename) + hass.services.register(DOMAIN, 'icloud_lost_iphone', lost_iphone, schema=SERVICE_SCHEMA) @@ -106,6 +113,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].update_icloud(devicename) + hass.services.register(DOMAIN, 'icloud_update', update_icloud, schema=SERVICE_SCHEMA) @@ -115,6 +123,7 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): for account in accounts: if account in ICLOUDTRACKERS: ICLOUDTRACKERS[account].reset_account_icloud() + hass.services.register(DOMAIN, 'icloud_reset_account', reset_account_icloud, schema=SERVICE_SCHEMA) @@ -137,7 +146,8 @@ def setup_scanner(hass, config: dict, see, discovery_info=None): class Icloud(DeviceScanner): """Representation of an iCloud account.""" - def __init__(self, hass, username, password, name, see): + def __init__(self, hass, username, password, name, max_interval, + gps_accuracy_threshold, see): """Initialize an iCloud account.""" self.hass = hass self.username = username @@ -148,6 +158,8 @@ class Icloud(DeviceScanner): self.seen_devices = {} self._overridestates = {} self._intervals = {} + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold self.see = see self._trusted_device = None @@ -348,7 +360,7 @@ class Icloud(DeviceScanner): self._overridestates[devicename] = None if currentzone is not None: - self._intervals[devicename] = 30 + self._intervals[devicename] = self._max_interval return if mindistance is None: @@ -363,7 +375,6 @@ class Icloud(DeviceScanner): if interval > 180: # Three hour drive? This is far enough that they might be flying - # home - check every half hour interval = 30 if battery is not None and battery <= 33 and mindistance > 3: @@ -403,22 +414,24 @@ class Icloud(DeviceScanner): status = device.status(DEVICESTATUSSET) battery = status.get('batteryLevel', 0) * 100 location = status['location'] - if location: - self.determine_interval( - devicename, location['latitude'], - location['longitude'], battery) - interval = self._intervals.get(devicename, 1) - attrs[ATTR_INTERVAL] = interval - accuracy = location['horizontalAccuracy'] - kwargs['dev_id'] = dev_id - kwargs['host_name'] = status['name'] - kwargs['gps'] = (location['latitude'], - location['longitude']) - kwargs['battery'] = battery - kwargs['gps_accuracy'] = accuracy - kwargs[ATTR_ATTRIBUTES] = attrs - self.see(**kwargs) - self.seen_devices[devicename] = True + if location and location['horizontalAccuracy']: + horizontal_accuracy = int(location['horizontalAccuracy']) + if horizontal_accuracy < self._gps_accuracy_threshold: + self.determine_interval( + devicename, location['latitude'], + location['longitude'], battery) + interval = self._intervals.get(devicename, 1) + attrs[ATTR_INTERVAL] = interval + accuracy = location['horizontalAccuracy'] + kwargs['dev_id'] = dev_id + kwargs['host_name'] = status['name'] + kwargs['gps'] = (location['latitude'], + location['longitude']) + kwargs['battery'] = battery + kwargs['gps_accuracy'] = accuracy + kwargs[ATTR_ATTRIBUTES] = attrs + self.see(**kwargs) + self.seen_devices[devicename] = True except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") @@ -434,7 +447,7 @@ class Icloud(DeviceScanner): device.play_sound() def update_icloud(self, devicename=None): - """Authenticate against iCloud and scan for devices.""" + """Request device information from iCloud and update device_tracker.""" from pyicloud.exceptions import PyiCloudNoDevicesException if self.api is None: @@ -443,13 +456,13 @@ class Icloud(DeviceScanner): try: if devicename is not None: if devicename in self.devices: - self.devices[devicename].location() + self.update_device(devicename) else: _LOGGER.error("devicename %s unknown for account %s", devicename, self._attrs[ATTR_ACCOUNTNAME]) else: for device in self.devices: - self.devices[device].location() + self.update_device(device) except PyiCloudNoDevicesException: _LOGGER.error("No iCloud Devices found") From f516cc7dc62134e5952529779e6dd5821a4ee6f9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 8 May 2018 16:10:03 -0600 Subject: [PATCH 024/144] Adds useful attributes to RainMachine programs and zones (#14087) * Starting to add attributes * All attributes added to programs * Basic zone attributes in place * Added advanced properties for zones * Working to move common logic into component + dispatcher * We shouldn't calculate the MAC with every entity * Small fixes * Small adjustments * Owner-requested changes * Restart * Restart part 2 * Added ID attribute to each switch * Collaborator-requested changes --- homeassistant/components/rainmachine.py | 57 +++- .../components/switch/rainmachine.py | 287 ++++++++++++------ 2 files changed, 247 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py index 99cec53c2ed..f2d5893d60b 100644 --- a/homeassistant/components/rainmachine.py +++ b/homeassistant/components/rainmachine.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/rainmachine/ """ import logging -from datetime import timedelta import voluptuous as vol -from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SWITCHES) + ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['regenmaschine==0.4.1'] @@ -26,11 +27,11 @@ NOTIFICATION_TITLE = 'RainMachine Component Setup' CONF_ZONE_RUN_TIME = 'zone_run_time' DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' DEFAULT_PORT = 8080 DEFAULT_SSL = True -MIN_SCAN_TIME = timedelta(seconds=1) -MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) SWITCH_SCHEMA = vol.Schema({ vol.Optional(CONF_ZONE_RUN_TIME): @@ -68,8 +69,7 @@ def setup(hass, config): auth = Authenticator.create_local( ip_address, password, port=port, https=ssl) client = Client(auth) - mac = client.provision.wifi()['macAddress'] - hass.data[DATA_RAINMACHINE] = (client, mac) + hass.data[DATA_RAINMACHINE] = RainMachine(client) except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info: _LOGGER.error('An error occurred: %s', str(exc_info)) hass.components.persistent_notification.create( @@ -87,3 +87,46 @@ def setup(hass, config): _LOGGER.debug('Setup complete') return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, + rainmachine, + rainmachine_type, + rainmachine_entity_id, + icon=DEFAULT_ICON): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._icon = icon + self._rainmachine_type = rainmachine_type + self._rainmachine_entity_id = rainmachine_entity_id + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace( + ':', ''), self._rainmachine_type, + self._rainmachine_entity_id) diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 8306b323330..beb00eeca44 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,26 +1,118 @@ -"""Implements a RainMachine sprinkler controller for Home Assistant.""" +""" +This component provides support for RainMachine programs and zones. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/switch.rainmachine/ +""" from logging import getLogger from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, - MIN_SCAN_TIME_FORCED) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, + RainMachineEntity) +from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.util import Throttle +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) DEPENDENCIES = ['rainmachine'] _LOGGER = getLogger(__name__) +ATTR_AREA = 'area' +ATTR_CS_ON = 'cs_on' +ATTR_CURRENT_CYCLE = 'current_cycle' ATTR_CYCLES = 'cycles' -ATTR_TOTAL_DURATION = 'total_duration' +ATTR_DELAY = 'delay' +ATTR_DELAY_ON = 'delay_on' +ATTR_FIELD_CAPACITY = 'field_capacity' +ATTR_NO_CYCLES = 'number_of_cycles' +ATTR_PRECIP_RATE = 'sprinkler_head_precipitation_rate' +ATTR_RESTRICTIONS = 'restrictions' +ATTR_SLOPE = 'slope' +ATTR_SOAK = 'soak' +ATTR_SOIL_TYPE = 'soil_type' +ATTR_SPRINKLER_TYPE = 'sprinkler_head_type' +ATTR_STATUS = 'status' +ATTR_SUN_EXPOSURE = 'sun_exposure' +ATTR_VEGETATION_TYPE = 'vegetation_type' +ATTR_ZONES = 'zones' DEFAULT_ZONE_RUN = 60 * 10 +DAYS = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday' +] + +PROGRAM_STATUS_MAP = { + 0: 'Not Running', + 1: 'Running', + 2: 'Queued' +} + +SOIL_TYPE_MAP = { + 0: 'Not Set', + 1: 'Clay Loam', + 2: 'Silty Clay', + 3: 'Clay', + 4: 'Loam', + 5: 'Sandy Loam', + 6: 'Loamy Sand', + 7: 'Sand', + 8: 'Sandy Clay', + 9: 'Silt Loam', + 10: 'Silt', + 99: 'Other' +} + +SLOPE_TYPE_MAP = { + 0: 'Not Set', + 1: 'Flat', + 2: 'Moderate', + 3: 'High', + 4: 'Very High', + 99: 'Other' +} + +SPRINKLER_TYPE_MAP = { + 0: 'Not Set', + 1: 'Popup Spray', + 2: 'Rotors', + 3: 'Surface Drip', + 4: 'Bubblers', + 99: 'Other' +} + +SUN_EXPOSURE_MAP = { + 0: 'Not Set', + 1: 'Full Sun', + 2: 'Partial Shade', + 3: 'Full Shade' +} + +VEGETATION_MAP = { + 0: 'Not Set', + 1: 'Not Set', + 2: 'Grass', + 3: 'Fruit Trees', + 4: 'Flowers', + 5: 'Vegetables', + 6: 'Citrus', + 7: 'Bushes', + 8: 'Xeriscape', + 99: 'Other' +} + def setup_platform(hass, config, add_devices, discovery_info=None): - """Set this component up under its platform.""" + """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -28,181 +120,196 @@ def setup_platform(hass, config, add_devices, discovery_info=None): zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) - client, device_mac = hass.data.get(DATA_RAINMACHINE) + rainmachine = hass.data[DATA_RAINMACHINE] entities = [] - for program in client.programs.all().get('programs', {}): + for program in rainmachine.client.programs.all().get('programs', {}): if not program.get('active'): continue _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_mac, program)) + entities.append(RainMachineProgram(rainmachine, program)) - for zone in client.zones.all().get('zones', {}): + for zone in rainmachine.client.zones.all().get('zones', {}): if not zone.get('active'): continue _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_mac, zone, - zone_run_time)) + entities.append(RainMachineZone(rainmachine, zone, zone_run_time)) add_devices(entities, True) -class RainMachineEntity(SwitchDevice): +class RainMachineSwitch(RainMachineEntity, SwitchDevice): """A class to represent a generic RainMachine entity.""" - def __init__(self, client, device_mac, entity_json): + def __init__(self, rainmachine, rainmachine_type, obj): """Initialize a generic RainMachine entity.""" - self._api_type = 'remote' if client.auth.using_remote_api else 'local' - self._client = client - self._entity_json = entity_json + self._obj = obj + self._type = rainmachine_type - self.device_mac = device_mac - - self._attrs = { - ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION - } - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return 'mdi:water' + super().__init__(rainmachine, rainmachine_type, obj.get('uid')) @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" - return self._entity_json.get('active') - - @property - def rainmachine_entity_id(self) -> int: - """Return the RainMachine ID for this entity.""" - return self._entity_json.get('uid') + return self._obj.get('active') -class RainMachineProgram(RainMachineEntity): +class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" + def __init__(self, rainmachine, obj): + """Initialize.""" + super().__init__(rainmachine, 'program', obj) + @property def is_on(self) -> bool: """Return whether the program is running.""" - return bool(self._entity_json.get('status')) + return bool(self._obj.get('status')) @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {0}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._obj.get('name')) @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_program_{1}'.format( - self.device_mac.replace(':', ''), self.rainmachine_entity_id) + def zones(self) -> list: + """Return a list of active zones associated with this program.""" + return [z for z in self._obj['wateringTimes'] if z['active']] def turn_off(self, **kwargs) -> None: """Turn the program off.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.programs.stop(self.rainmachine_entity_id) - except exceptions.BrokenAPICall: - _LOGGER.error('programs.stop currently broken in remote API') - except exceptions.HTTPError as exc_info: + self.rainmachine.client.programs.stop(self._rainmachine_entity_id) + dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except HTTPError as exc_info: _LOGGER.error('Unable to turn off program "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the program on.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.programs.start(self.rainmachine_entity_id) - except exceptions.BrokenAPICall: - _LOGGER.error('programs.start currently broken in remote API') - except exceptions.HTTPError as exc_info: + self.rainmachine.client.programs.start(self._rainmachine_entity_id) + dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except HTTPError as exc_info: _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _LOGGER.debug(exc_info) - @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) def update(self) -> None: """Update info for the program.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._entity_json = self._client.programs.get( - self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self._obj = self.rainmachine.client.programs.get( + self._rainmachine_entity_id) + + self._attrs.update({ + ATTR_ID: self._obj['uid'], + ATTR_CS_ON: self._obj.get('cs_on'), + ATTR_CYCLES: self._obj.get('cycles'), + ATTR_DELAY: self._obj.get('delay'), + ATTR_DELAY_ON: self._obj.get('delay_on'), + ATTR_SOAK: self._obj.get('soak'), + ATTR_STATUS: + PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_ZONES: ', '.join(z['name'] for z in self.zones) + }) + except HTTPError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) -class RainMachineZone(RainMachineEntity): +class RainMachineZone(RainMachineSwitch): """A RainMachine zone.""" - def __init__(self, client, device_mac, zone_json, - zone_run_time): + def __init__(self, rainmachine, obj, zone_run_time): """Initialize a RainMachine zone.""" - super().__init__(client, device_mac, zone_json) + super().__init__(rainmachine, 'zone', obj) + + self._properties_json = {} self._run_time = zone_run_time - self._attrs.update({ - ATTR_CYCLES: self._entity_json.get('noOfCycles'), - ATTR_TOTAL_DURATION: self._entity_json.get('userDuration') - }) @property def is_on(self) -> bool: """Return whether the zone is running.""" - return bool(self._entity_json.get('state')) + return bool(self._obj.get('state')) @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {0}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._obj.get('name')) - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_zone_{1}'.format( - self.device_mac.replace(':', ''), self.rainmachine_entity_id) + @callback + def _program_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC, + self._program_updated) def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.zones.stop(self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self.rainmachine.client.zones.stop(self._rainmachine_entity_id) + except HTTPError as exc_info: _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) _LOGGER.debug(exc_info) def turn_on(self, **kwargs) -> None: """Turn the zone on.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._client.zones.start(self.rainmachine_entity_id, - self._run_time) - except exceptions.HTTPError as exc_info: + self.rainmachine.client.zones.start(self._rainmachine_entity_id, + self._run_time) + except HTTPError as exc_info: _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _LOGGER.debug(exc_info) - @Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) def update(self) -> None: """Update info for the zone.""" - import regenmaschine.exceptions as exceptions + from regenmaschine.exceptions import HTTPError try: - self._entity_json = self._client.zones.get( - self.rainmachine_entity_id) - except exceptions.HTTPError as exc_info: + self._obj = self.rainmachine.client.zones.get( + self._rainmachine_entity_id) + + self._properties_json = self.rainmachine.client.zones.get( + self._rainmachine_entity_id, properties=True) + + self._attrs.update({ + ATTR_ID: self._obj['uid'], + ATTR_AREA: self._properties_json.get('waterSense').get('area'), + ATTR_CURRENT_CYCLE: self._obj.get('cycle'), + ATTR_FIELD_CAPACITY: + self._properties_json.get( + 'waterSense').get('fieldCapacity'), + ATTR_NO_CYCLES: self._obj.get('noOfCycles'), + ATTR_PRECIP_RATE: + self._properties_json.get( + 'waterSense').get('precipitationRate'), + ATTR_RESTRICTIONS: self._obj.get('restriction'), + ATTR_SLOPE: SLOPE_TYPE_MAP[self._properties_json.get('slope')], + ATTR_SOIL_TYPE: + SOIL_TYPE_MAP[self._properties_json.get('sun')], + ATTR_SPRINKLER_TYPE: + SPRINKLER_TYPE_MAP[self._properties_json.get('group_id')], + ATTR_SUN_EXPOSURE: + SUN_EXPOSURE_MAP[self._properties_json.get('sun')], + ATTR_VEGETATION_TYPE: + VEGETATION_MAP[self._obj.get('type')], + }) + except HTTPError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) From 62313946149154362189c1979720e5e58dc59ed6 Mon Sep 17 00:00:00 2001 From: Mario Di Raimondo Date: Wed, 9 May 2018 00:35:03 +0200 Subject: [PATCH 025/144] Waze Travel Time: optional inclusive/exclusive filters (#14000) * Waze Travel Time: optional inclusive/exclusive filters Added optional `inc_filter` and `excl_filter' params that allow to refine the reported routes: the first is not always the best/desired. A simple case-insensitive filtering (no regular expression) is used. * fix line lenght * fix spaces * Rename var * Fix typo * Fix missing var --- .../components/sensor/waze_travel_time.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/waze_travel_time.py b/homeassistant/components/sensor/waze_travel_time.py index 47589f33530..dbcfcb9cc27 100644 --- a/homeassistant/components/sensor/waze_travel_time.py +++ b/homeassistant/components/sensor/waze_travel_time.py @@ -26,6 +26,8 @@ ATTR_ROUTE = 'route' CONF_ATTRIBUTION = "Data provided by the Waze.com" CONF_DESTINATION = 'destination' CONF_ORIGIN = 'origin' +CONF_INCL_FILTER = 'incl_filter' +CONF_EXCL_FILTER = 'excl_filter' DEFAULT_NAME = 'Waze Travel Time' @@ -40,6 +42,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_DESTINATION): cv.string, vol.Required(CONF_REGION): vol.In(REGIONS), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_INCL_FILTER): cv.string, + vol.Optional(CONF_EXCL_FILTER): cv.string, }) @@ -49,9 +53,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): name = config.get(CONF_NAME) origin = config.get(CONF_ORIGIN) region = config.get(CONF_REGION) + incl_filter = config.get(CONF_INCL_FILTER) + excl_filter = config.get(CONF_EXCL_FILTER) try: - waze_data = WazeRouteData(origin, destination, region) + waze_data = WazeRouteData( + origin, destination, region, incl_filter, excl_filter) except requests.exceptions.HTTPError as error: _LOGGER.error("%s", error) return @@ -109,11 +116,13 @@ class WazeTravelTime(Entity): class WazeRouteData(object): """Get data from Waze.""" - def __init__(self, origin, destination, region): + def __init__(self, origin, destination, region, incl_filter, excl_filter): """Initialize the data object.""" self._destination = destination self._origin = origin self._region = region + self._incl_filter = incl_filter + self._excl_filter = excl_filter self.data = {} @Throttle(SCAN_INTERVAL) @@ -125,6 +134,12 @@ class WazeRouteData(object): params = WazeRouteCalculator.WazeRouteCalculator( self._origin, self._destination, self._region, None) results = params.calc_all_routes_info() + if self._incl_filter is not None: + results = {k: v for k, v in results.items() if + self._incl_filter.lower() in k.lower()} + if self._excl_filter is not None: + results = {k: v for k, v in results.items() if + self._excl_filter.lower() not in k.lower()} best_route = next(iter(results)) (duration, distance) = results[best_route] best_route_str = bytes(best_route, 'ISO-8859-1').decode('UTF-8') From 50cea778879e64a9b52072ea641c5273ccaf916f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 May 2018 20:48:46 -0400 Subject: [PATCH 026/144] Bump frontend to 20180509.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b4eb6df07e1..0d267077991 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180505.0'] +REQUIREMENTS = ['home-assistant-frontend==20180509.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0dcfdbcce70..5e7df874f17 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 976f4d87280..a25f36a8195 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180505.0 +home-assistant-frontend==20180509.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From d43e6a28883dc3a513b9dc99002de4f15703c58d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 9 May 2018 02:54:38 +0200 Subject: [PATCH 027/144] Ignore NaN values for influxdb (#14347) * Ignore NaN values for influxdb * Catch TypeError --- homeassistant/components/influxdb.py | 10 +++++++--- tests/components/test_influxdb.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/influxdb.py b/homeassistant/components/influxdb.py index 1f7f9f6262f..6d54324542a 100644 --- a/homeassistant/components/influxdb.py +++ b/homeassistant/components/influxdb.py @@ -9,6 +9,7 @@ import re import queue import threading import time +import math import requests.exceptions import voluptuous as vol @@ -220,9 +221,12 @@ def setup(hass, config): json['fields'][key] = float( RE_DECIMAL.sub('', new_value)) - # Infinity is not a valid float in InfluxDB - if (key, float("inf")) in json['fields'].items(): - del json['fields'][key] + # Infinity and NaN are not valid floats in InfluxDB + try: + if not math.isfinite(json['fields'][key]): + del json['fields'][key] + except (KeyError, TypeError): + pass json['tags'].update(tags) diff --git a/tests/components/test_influxdb.py b/tests/components/test_influxdb.py index c909a8488be..e2323aca855 100644 --- a/tests/components/test_influxdb.py +++ b/tests/components/test_influxdb.py @@ -217,7 +217,7 @@ class TestInfluxDB(unittest.TestCase): """Test the event listener for missing units.""" self._setup() - attrs = {'bignumstring': "9" * 999} + attrs = {'bignumstring': '9' * 999, 'nonumstring': 'nan'} state = mock.MagicMock( state=8, domain='fake', entity_id='fake.entity-id', object_id='entity', attributes=attrs) From a91c1bc668e6a5ebee49ee2239d90a0a2feb4ea8 Mon Sep 17 00:00:00 2001 From: Mal Curtis Date: Wed, 9 May 2018 14:33:38 +1200 Subject: [PATCH 028/144] Add zone 3 for Onkyo media player (#14295) * Add zone 3 for Onkyo media player * CR Updates * Fix travis lint errors --- .../components/media_player/onkyo.py | 89 ++++++++++++++----- 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 39c278ff95d..71b74868544 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -24,7 +24,6 @@ _LOGGER = logging.getLogger(__name__) CONF_SOURCES = 'sources' CONF_MAX_VOLUME = 'max_volume' -CONF_ZONE2 = 'zone2' DEFAULT_NAME = 'Onkyo Receiver' SUPPORTED_MAX_VOLUME = 80 @@ -47,9 +46,36 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.All(vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, - vol.Optional(CONF_ZONE2, default=False): cv.boolean, }) +TIMEOUT_MESSAGE = 'Timeout waiting for response.' + + +def determine_zones(receiver): + """Determine what zones are available for the receiver.""" + out = { + "zone2": False, + "zone3": False, + } + try: + _LOGGER.debug("Checking for zone 2 capability") + receiver.raw("ZPW") + out["zone2"] = True + except ValueError as error: + if str(error) != TIMEOUT_MESSAGE: + raise error + _LOGGER.debug("Zone 2 timed out, assuming no functionality") + try: + _LOGGER.debug("Checking for zone 3 capability") + receiver.raw("PW3") + out["zone3"] = True + except ValueError as error: + if str(error) != TIMEOUT_MESSAGE: + raise error + _LOGGER.debug("Zone 3 timed out, assuming no functionality") + + return out + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Onkyo platform.""" @@ -61,20 +87,31 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if CONF_HOST in config and host not in KNOWN_HOSTS: try: + receiver = eiscp.eISCP(host) hosts.append(OnkyoDevice( - eiscp.eISCP(host), config.get(CONF_SOURCES), + receiver, + config.get(CONF_SOURCES), name=config.get(CONF_NAME), max_volume=config.get(CONF_MAX_VOLUME), )) KNOWN_HOSTS.append(host) - # Add Zone2 if configured - if config.get(CONF_ZONE2): + zones = determine_zones(receiver) + + # Add Zone2 if available + if zones["zone2"]: _LOGGER.debug("Setting up zone 2") - hosts.append(OnkyoDeviceZone2(eiscp.eISCP(host), - config.get(CONF_SOURCES), - name=config.get(CONF_NAME) + - " Zone 2")) + hosts.append(OnkyoDeviceZone( + "2", receiver, + config.get(CONF_SOURCES), + name="{} Zone 2".format(config[CONF_NAME]))) + # Add Zone3 if available + if zones["zone3"]: + _LOGGER.debug("Setting up zone 3") + hosts.append(OnkyoDeviceZone( + "3", receiver, + config.get(CONF_SOURCES), + name="{} Zone 3".format(config[CONF_NAME]))) except OSError: _LOGGER.error("Unable to connect to receiver at %s", host) else: @@ -227,12 +264,17 @@ class OnkyoDevice(MediaPlayerDevice): self.command('input-selector {}'.format(source)) -class OnkyoDeviceZone2(OnkyoDevice): - """Representation of an Onkyo device's zone 2.""" +class OnkyoDeviceZone(OnkyoDevice): + """Representation of an Onkyo device's extra zone.""" + + def __init__(self, zone, receiver, sources, name=None): + """Initialize the Zone with the zone identifier.""" + self._zone = zone + super().__init__(receiver, sources, name) def update(self): """Get the latest state from the device.""" - status = self.command('zone2.power=query') + status = self.command('zone{}.power=query'.format(self._zone)) if not status: return @@ -242,9 +284,10 @@ class OnkyoDeviceZone2(OnkyoDevice): self._pwstate = STATE_OFF return - volume_raw = self.command('zone2.volume=query') - mute_raw = self.command('zone2.muting=query') - current_source_raw = self.command('zone2.selector=query') + volume_raw = self.command('zone{}.volume=query'.format(self._zone)) + mute_raw = self.command('zone{}.muting=query'.format(self._zone)) + current_source_raw = self.command( + 'zone{}.selector=query'.format(self._zone)) if not (volume_raw and mute_raw and current_source_raw): return @@ -268,33 +311,33 @@ class OnkyoDeviceZone2(OnkyoDevice): def turn_off(self): """Turn the media player off.""" - self.command('zone2.power=standby') + self.command('zone{}.power=standby'.format(self._zone)) def set_volume_level(self, volume): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command('zone2.volume={}'.format(int(volume*80))) + self.command('zone{}.volume={}'.format(self._zone, int(volume*80))) def volume_up(self): """Increase volume by 1 step.""" - self.command('zone2.volume=level-up') + self.command('zone{}.volume=level-up'.format(self._zone)) def volume_down(self): """Decrease volume by 1 step.""" - self.command('zone2.volume=level-down') + self.command('zone{}.volume=level-down'.format(self._zone)) def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: - self.command('zone2.muting=on') + self.command('zone{}.muting=on'.format(self._zone)) else: - self.command('zone2.muting=off') + self.command('zone{}.muting=off'.format(self._zone)) def turn_on(self): """Turn the media player on.""" - self.command('zone2.power=on') + self.command('zone{}.power=on'.format(self._zone)) def select_source(self, source): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command('zone2.selector={}'.format(source)) + self.command('zone{}.selector={}'.format(self._zone, source)) From cf8562a030ba79d9962848f0254d362408bc3956 Mon Sep 17 00:00:00 2001 From: Nash Kaminski Date: Wed, 9 May 2018 04:26:29 -0500 Subject: [PATCH 029/144] Support control of away mode and hold mode in Venstar component. Correctly detect humidifiers. (#14256) * Implement support for away mode and hold mode in Venstar component * Fix Venstar humidifier capability detection * Add option to configure humidifier control in Venstar component * style fix: add missing space and resolve pylint issues * Remove quotes --- homeassistant/components/climate/venstar.py | 69 ++++++++++++++++++--- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/climate/venstar.py b/homeassistant/components/climate/venstar.py index 6e63cc4092b..c2b82e1cc84 100644 --- a/homeassistant/components/climate/venstar.py +++ b/homeassistant/components/climate/venstar.py @@ -11,9 +11,11 @@ import voluptuous as vol from homeassistant.components.climate import ( ATTR_OPERATION_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, PLATFORM_SCHEMA, STATE_AUTO, STATE_COOL, STATE_HEAT, SUPPORT_FAN_MODE, - SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_HIGH, - SUPPORT_TARGET_TEMPERATURE_LOW, ClimateDevice) + SUPPORT_OPERATION_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_AWAY_MODE, + SUPPORT_TARGET_HUMIDITY_HIGH, SUPPORT_TARGET_HUMIDITY_LOW, + SUPPORT_HOLD_MODE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW, + ClimateDevice) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, PRECISION_WHOLE, STATE_OFF, STATE_ON, TEMP_CELSIUS, @@ -27,14 +29,20 @@ _LOGGER = logging.getLogger(__name__) ATTR_FAN_STATE = 'fan_state' ATTR_HVAC_STATE = 'hvac_state' +CONF_HUMIDIFIER = 'humidifier' + DEFAULT_SSL = False VALID_FAN_STATES = [STATE_ON, STATE_AUTO] VALID_THERMOSTAT_MODES = [STATE_HEAT, STATE_COOL, STATE_OFF, STATE_AUTO] +HOLD_MODE_OFF = 'off' +HOLD_MODE_TEMPERATURE = 'temperature' + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HUMIDIFIER, default=True): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_TIMEOUT, default=5): vol.All(vol.Coerce(int), vol.Range(min=1)), @@ -50,6 +58,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) timeout = config.get(CONF_TIMEOUT) + humidifier = config.get(CONF_HUMIDIFIER) if config.get(CONF_SSL): proto = 'https' @@ -60,15 +69,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): addr=host, timeout=timeout, user=username, password=password, proto=proto) - add_devices([VenstarThermostat(client)], True) + add_devices([VenstarThermostat(client, humidifier)], True) class VenstarThermostat(ClimateDevice): """Representation of a Venstar thermostat.""" - def __init__(self, client): + def __init__(self, client, humidifier): """Initialize the thermostat.""" self._client = client + self._humidifier = humidifier def update(self): """Update the data from the thermostat.""" @@ -81,14 +91,18 @@ class VenstarThermostat(ClimateDevice): def supported_features(self): """Return the list of supported features.""" features = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | - SUPPORT_OPERATION_MODE) + SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE | + SUPPORT_HOLD_MODE) if self._client.mode == self._client.MODE_AUTO: features |= (SUPPORT_TARGET_TEMPERATURE_HIGH | SUPPORT_TARGET_TEMPERATURE_LOW) - if self._client.hum_active == 1: - features |= SUPPORT_TARGET_HUMIDITY + if (self._humidifier and + hasattr(self._client, 'hum_active')): + features |= (SUPPORT_TARGET_HUMIDITY | + SUPPORT_TARGET_HUMIDITY_HIGH | + SUPPORT_TARGET_HUMIDITY_LOW) return features @@ -197,6 +211,18 @@ class VenstarThermostat(ClimateDevice): """Return the maximum humidity. Hardcoded to 60 in API.""" return 60 + @property + def is_away_mode_on(self): + """Return the status of away mode.""" + return self._client.away == self._client.AWAY_AWAY + + @property + def current_hold_mode(self): + """Return the status of hold mode.""" + if self._client.schedule == 0: + return HOLD_MODE_TEMPERATURE + return HOLD_MODE_OFF + def _set_operation_mode(self, operation_mode): """Change the operation mode (internal).""" if operation_mode == STATE_HEAT: @@ -259,3 +285,30 @@ class VenstarThermostat(ClimateDevice): if not success: _LOGGER.error("Failed to change the target humidity level") + + def set_hold_mode(self, hold_mode): + """Set the hold mode.""" + if hold_mode == HOLD_MODE_TEMPERATURE: + success = self._client.set_schedule(0) + elif hold_mode == HOLD_MODE_OFF: + success = self._client.set_schedule(1) + else: + _LOGGER.error("Unknown hold mode: %s", hold_mode) + success = False + + if not success: + _LOGGER.error("Failed to change the schedule/hold state") + + def turn_away_mode_on(self): + """Activate away mode.""" + success = self._client.set_away(self._client.AWAY_AWAY) + + if not success: + _LOGGER.error("Failed to activate away mode") + + def turn_away_mode_off(self): + """Deactivate away mode.""" + success = self._client.set_away(self._client.AWAY_HOME) + + if not success: + _LOGGER.error("Failed to deactivate away mode") From 2c566072f5094f1e0c0c97f2ccb00c91fa833181 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 9 May 2018 11:31:18 +0200 Subject: [PATCH 030/144] Upgrade keyring to 12.2.0 and keyrings.alt to 3.1 (#14355) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 82a57c90263..11e337a76b5 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -5,7 +5,7 @@ import os from homeassistant.util.yaml import _SECRET_NAMESPACE -REQUIREMENTS = ['keyring==12.0.0', 'keyrings.alt==3.0'] +REQUIREMENTS = ['keyring==12.2.0', 'keyrings.alt==3.1'] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 5e7df874f17..1400237a683 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -459,10 +459,10 @@ jsonrpc-async==0.6 jsonrpc-websocket==0.6 # homeassistant.scripts.keyring -keyring==12.0.0 +keyring==12.2.0 # homeassistant.scripts.keyring -keyrings.alt==3.0 +keyrings.alt==3.1 # homeassistant.components.eufy lakeside==0.5 From 0f3ec94fbaf13d590837037ed54d62764643632d Mon Sep 17 00:00:00 2001 From: Teemu R Date: Wed, 9 May 2018 13:44:42 +0200 Subject: [PATCH 031/144] debug++ for multiple volume controls (#14349) Be less noisy for those who have more volume controls than one, mentioned in #13022. --- homeassistant/components/media_player/songpal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/songpal.py b/homeassistant/components/media_player/songpal.py index 955456f2465..5d0962775f0 100644 --- a/homeassistant/components/media_player/songpal.py +++ b/homeassistant/components/media_player/songpal.py @@ -151,8 +151,8 @@ class SongpalDevice(MediaPlayerDevice): return if len(volumes) > 1: - _LOGGER.warning("Got %s volume controls, using the first one", - volumes) + _LOGGER.debug("Got %s volume controls, using the first one", + volumes) volume = volumes[0] _LOGGER.debug("Current volume: %s", volume) From 5ec7fc7ddb23c83f6718ef8c544fff5265073112 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 04:38:11 -0400 Subject: [PATCH 032/144] Backend tweaks to make authorization work (#14339) * Backend tweaks to make authorization work * Lint * Add test * Validate redirect uris * Fix tests * Fix tests * Lint --- homeassistant/auth.py | 19 ++++++-- homeassistant/components/api.py | 3 +- homeassistant/components/auth/__init__.py | 21 ++++++--- homeassistant/components/auth/client.py | 38 ++++++++++----- homeassistant/components/frontend/__init__.py | 22 +++++++-- homeassistant/components/http/auth.py | 7 ++- homeassistant/components/http/view.py | 6 --- homeassistant/components/websocket_api.py | 18 +++++--- homeassistant/helpers/data_entry_flow.py | 2 +- tests/components/auth/__init__.py | 4 +- tests/components/auth/test_client.py | 6 +-- tests/components/auth/test_init.py | 5 +- tests/components/auth/test_init_link_user.py | 8 ++-- tests/components/auth/test_init_login_flow.py | 5 +- tests/components/conftest.py | 16 +++++++ tests/components/test_api.py | 8 ++-- tests/components/test_websocket_api.py | 46 +++++++++++++++++++ 17 files changed, 176 insertions(+), 58 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 55de9309954..5c9d437e067 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -210,6 +210,7 @@ class Client: name = attr.ib(type=str) id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) secret = attr.ib(type=str, default=attr.Factory(generate_secret)) + redirect_uris = attr.ib(type=list, default=attr.Factory(list)) async def load_auth_provider_module(hass, provider): @@ -340,9 +341,11 @@ class AuthManager: """Get an access token.""" return self.access_tokens.get(token) - async def async_create_client(self, name): + async def async_create_client(self, name, *, redirect_uris=None, + no_secret=False): """Create a new client.""" - return await self._store.async_create_client(name) + return await self._store.async_create_client( + name, redirect_uris, no_secret) async def async_get_client(self, client_id): """Get a client.""" @@ -477,12 +480,20 @@ class AuthStore: return None - async def async_create_client(self, name): + async def async_create_client(self, name, redirect_uris, no_secret): """Create a new client.""" if self.clients is None: await self.async_load() - client = Client(name) + kwargs = { + 'name': name, + 'redirect_uris': redirect_uris + } + + if no_secret: + kwargs['secret'] = None + + client = Client(**kwargs) self.clients[client.id] = client await self.async_save() return client diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 83e05dae641..dc34006ad03 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -356,7 +356,8 @@ class APIErrorLog(HomeAssistantView): async def get(self, request): """Retrieve API error log.""" - return await self.file(request, request.app['hass'].data[DATA_LOGGING]) + return web.FileResponse( + request.app['hass'].data[DATA_LOGGING]) async def async_services_json(hass): diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d4b4b0f4591..0f7295a41e0 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -144,7 +144,7 @@ class AuthProvidersView(HomeAssistantView): requires_auth = False @verify_client - async def get(self, request, client_id): + async def get(self, request, client): """Get available auth providers.""" return self.json([{ 'name': provider.name, @@ -166,8 +166,15 @@ class LoginFlowIndexView(FlowManagerIndexView): # pylint: disable=arguments-differ @verify_client - async def post(self, request, client_id): + @RequestDataValidator(vol.Schema({ + vol.Required('handler'): vol.Any(str, list), + vol.Required('redirect_uri'): str, + })) + async def post(self, request, client, data): """Create a new login flow.""" + if data['redirect_uri'] not in client.redirect_uris: + return self.json_message('invalid redirect uri', ) + # pylint: disable=no-value-for-parameter return await super().post(request) @@ -192,7 +199,7 @@ class LoginFlowResourceView(FlowManagerResourceView): # pylint: disable=arguments-differ @verify_client @RequestDataValidator(vol.Schema(dict), allow_empty=True) - async def post(self, request, client_id, flow_id, data): + async def post(self, request, client, flow_id, data): """Handle progressing a login flow request.""" try: result = await self._flow_mgr.async_configure(flow_id, data) @@ -205,7 +212,7 @@ class LoginFlowResourceView(FlowManagerResourceView): return self.json(self._prepare_result_json(result)) result.pop('data') - result['result'] = self._store_credentials(client_id, result['result']) + result['result'] = self._store_credentials(client.id, result['result']) return self.json(result) @@ -222,7 +229,7 @@ class GrantTokenView(HomeAssistantView): self._retrieve_credentials = retrieve_credentials @verify_client - async def post(self, request, client_id): + async def post(self, request, client): """Grant a token.""" hass = request.app['hass'] data = await request.post() @@ -230,11 +237,11 @@ class GrantTokenView(HomeAssistantView): if grant_type == 'authorization_code': return await self._async_handle_auth_code( - hass, client_id, data) + hass, client.id, data) elif grant_type == 'refresh_token': return await self._async_handle_refresh_token( - hass, client_id, data) + hass, client.id, data) return self.json({ 'error': 'unsupported_grant_type', diff --git a/homeassistant/components/auth/client.py b/homeassistant/components/auth/client.py index 28d72aefe0f..122c3032188 100644 --- a/homeassistant/components/auth/client.py +++ b/homeassistant/components/auth/client.py @@ -11,15 +11,15 @@ def verify_client(method): @wraps(method) async def wrapper(view, request, *args, **kwargs): """Verify client id/secret before doing request.""" - client_id = await _verify_client(request) + client = await _verify_client(request) - if client_id is None: + if client is None: return view.json({ 'error': 'invalid_client', }, status_code=401) return await method( - view, request, *args, client_id=client_id, **kwargs) + view, request, *args, **kwargs, client=client) return wrapper @@ -46,18 +46,34 @@ async def _verify_client(request): client_id, client_secret = decoded.split(':', 1) except ValueError: # If no ':' in decoded - return None + client_id, client_secret = decoded, None - client = await request.app['hass'].auth.async_get_client(client_id) + return await async_secure_get_client( + request.app['hass'], client_id, client_secret) + + +async def async_secure_get_client(hass, client_id, client_secret): + """Get a client id/secret in consistent time.""" + client = await hass.auth.async_get_client(client_id) if client is None: - # Still do a compare so we run same time as if a client was found. - hmac.compare_digest(client_secret.encode('utf-8'), - client_secret.encode('utf-8')) + if client_secret is not None: + # Still do a compare so we run same time as if a client was found. + hmac.compare_digest(client_secret.encode('utf-8'), + client_secret.encode('utf-8')) return None - if hmac.compare_digest(client_secret.encode('utf-8'), - client.secret.encode('utf-8')): - return client_id + if client.secret is None: + return client + + elif client_secret is None: + # Still do a compare so we run same time as if a secret was passed. + hmac.compare_digest(client.secret.encode('utf-8'), + client.secret.encode('utf-8')) + return None + + elif hmac.compare_digest(client_secret.encode('utf-8'), + client.secret.encode('utf-8')): + return client return None diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0d267077991..c30e0dfb69f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -296,6 +296,15 @@ def add_manifest_json_key(key, val): @asyncio.coroutine def async_setup(hass, config): """Set up the serving of the frontend.""" + if list(hass.auth.async_auth_providers): + client = yield from hass.auth.async_create_client( + 'Home Assistant Frontend', + redirect_uris=['/'], + no_secret=True, + ) + else: + client = None + hass.components.websocket_api.async_register_command( WS_TYPE_GET_PANELS, websocket_handle_get_panels, SCHEMA_GET_PANELS) hass.http.register_view(ManifestJSONView) @@ -353,7 +362,7 @@ def async_setup(hass, config): if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) - index_view = IndexView(repo_path, js_version) + index_view = IndexView(repo_path, js_version, client) hass.http.register_view(index_view) @asyncio.coroutine @@ -451,10 +460,11 @@ class IndexView(HomeAssistantView): requires_auth = False extra_urls = ['/states', '/states/{extra}'] - def __init__(self, repo_path, js_option): + def __init__(self, repo_path, js_option, client): """Initialize the frontend view.""" self.repo_path = repo_path self.js_option = js_option + self.client = client self._template_cache = {} def get_template(self, latest): @@ -508,7 +518,7 @@ class IndexView(HomeAssistantView): extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5 - resp = template.render( + template_params = dict( no_auth=no_auth, panel_url=panel_url, panels=hass.data[DATA_PANELS], @@ -516,7 +526,11 @@ class IndexView(HomeAssistantView): extra_urls=hass.data[extra_key], ) - return web.Response(text=resp, content_type='text/html') + if self.client is not None: + template_params['client_id'] = self.client.id + + return web.Response(text=template.render(**template_params), + content_type='text/html') class ManifestJSONView(HomeAssistantView): diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 5558063c5c4..c4723abccee 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -81,7 +81,12 @@ async def async_validate_auth_header(api_password, request): if hdrs.AUTHORIZATION not in request.headers: return False - auth_type, auth_val = request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + try: + auth_type, auth_val = \ + request.headers.get(hdrs.AUTHORIZATION).split(' ', 1) + except ValueError: + # If no space in authorization header + return False if auth_type == 'Basic': decoded = base64.b64decode(auth_val).decode('utf-8') diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 81c6ea4bcfb..3de276564eb 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -51,12 +51,6 @@ class HomeAssistantView(object): data['code'] = message_code return self.json(data, status_code, headers=headers) - # pylint: disable=no-self-use - async def file(self, request, fil): - """Return a file.""" - assert isinstance(fil, str), 'only string paths allowed' - return web.FileResponse(fil) - def register(self, router): """Register the view with a router.""" assert self.url is not None, 'No url set for view' diff --git a/homeassistant/components/websocket_api.py b/homeassistant/components/websocket_api.py index 4989f4f0db2..11094acd3e2 100644 --- a/homeassistant/components/websocket_api.py +++ b/homeassistant/components/websocket_api.py @@ -60,7 +60,8 @@ JSON_DUMP = partial(json.dumps, cls=JSONEncoder) AUTH_MESSAGE_SCHEMA = vol.Schema({ vol.Required('type'): TYPE_AUTH, - vol.Required('api_password'): str, + vol.Exclusive('api_password', 'auth'): str, + vol.Exclusive('access_token', 'auth'): str, }) # Minimal requirements of a message @@ -318,15 +319,18 @@ class ActiveConnection: msg = await wsock.receive_json() msg = AUTH_MESSAGE_SCHEMA(msg) - if validate_password(request, msg['api_password']): - authenticated = True + if 'api_password' in msg: + authenticated = validate_password( + request, msg['api_password']) - else: - self.debug("Invalid password") - await self.wsock.send_json( - auth_invalid_message('Invalid password')) + elif 'access_token' in msg: + authenticated = \ + msg['access_token'] in self.hass.auth.access_tokens if not authenticated: + self.debug("Invalid password") + await self.wsock.send_json( + auth_invalid_message('Invalid password')) await process_wrong_login(request) return wsock diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 913e90a859d..5a0b2ca56ea 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -44,7 +44,7 @@ class FlowManagerIndexView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema({ vol.Required('handler'): vol.Any(str, list), - })) + }, extra=vol.ALLOW_EXTRA)) async def post(self, request, data): """Handle a POST request.""" if isinstance(data['handler'], list): diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index 3e5a59e8386..f0b205ff5ce 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -19,6 +19,7 @@ BASE_CONFIG = [{ CLIENT_ID = 'test-id' CLIENT_SECRET = 'test-secret' CLIENT_AUTH = BasicAuth(CLIENT_ID, CLIENT_SECRET) +CLIENT_REDIRECT_URI = 'http://example.com/callback' async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, @@ -31,7 +32,8 @@ async def async_setup_auth(hass, aiohttp_client, provider_configs=BASE_CONFIG, 'api_password': 'bla' } }) - client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET) + client = auth.Client('Test Client', CLIENT_ID, CLIENT_SECRET, + redirect_uris=[CLIENT_REDIRECT_URI]) hass.auth._store.clients[client.id] = client if setup_api: await async_setup_component(hass, 'api', {}) diff --git a/tests/components/auth/test_client.py b/tests/components/auth/test_client.py index 2995a6ac81a..65ad22efae2 100644 --- a/tests/components/auth/test_client.py +++ b/tests/components/auth/test_client.py @@ -21,9 +21,9 @@ def mock_view(hass): name = 'bla' @verify_client - async def get(self, request, client_id): + async def get(self, request, client): """Handle GET request.""" - clients.append(client_id) + clients.append(client) hass.http.register_view(ClientView) return clients @@ -36,7 +36,7 @@ async def test_verify_client(hass, aiohttp_client, mock_view): resp = await http_client.get('/', auth=BasicAuth(client.id, client.secret)) assert resp.status == 200 - assert mock_view == [client.id] + assert mock_view[0] is client async def test_verify_client_no_auth_header(hass, aiohttp_client, mock_view): diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 5d9bf6b98cc..7cff04327b8 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,12 +1,13 @@ """Integration tests for the auth component.""" -from . import async_setup_auth, CLIENT_AUTH +from . import async_setup_auth, CLIENT_AUTH, CLIENT_REDIRECT_URI async def test_login_new_user_and_refresh_token(hass, aiohttp_client): """Test logging in with new user and refreshing tokens.""" client = await async_setup_auth(hass, aiohttp_client, setup_api=True) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 44695bce202..853c002ba46 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -1,5 +1,5 @@ """Tests for the link user flow.""" -from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID +from . import async_setup_auth, CLIENT_AUTH, CLIENT_ID, CLIENT_REDIRECT_URI async def async_get_code(hass, aiohttp_client): @@ -25,7 +25,8 @@ async def async_get_code(hass, aiohttp_client): client = await async_setup_auth(hass, aiohttp_client, config) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() @@ -56,7 +57,8 @@ async def async_get_code(hass, aiohttp_client): # Now authenticate with the 2nd flow resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', '2nd auth'] + 'handler': ['insecure_example', '2nd auth'], + 'redirect_uri': CLIENT_REDIRECT_URI, }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/auth/test_init_login_flow.py b/tests/components/auth/test_init_login_flow.py index 96fece6506b..ad39fba3997 100644 --- a/tests/components/auth/test_init_login_flow.py +++ b/tests/components/auth/test_init_login_flow.py @@ -1,7 +1,7 @@ """Tests for the login flow.""" from aiohttp.helpers import BasicAuth -from . import async_setup_auth, CLIENT_AUTH +from . import async_setup_auth, CLIENT_AUTH, CLIENT_REDIRECT_URI async def test_fetch_auth_providers(hass, aiohttp_client): @@ -34,7 +34,8 @@ async def test_invalid_username_password(hass, aiohttp_client): """Test we cannot get flows in progress.""" client = await async_setup_auth(hass, aiohttp_client) resp = await client.post('/auth/login_flow', json={ - 'handler': ['insecure_example', None] + 'handler': ['insecure_example', None], + 'redirect_uri': CLIENT_REDIRECT_URI }, auth=CLIENT_AUTH) assert resp.status == 200 step = await resp.json() diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 53caeb80783..8a1b934ab76 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -3,6 +3,8 @@ import pytest from homeassistant.setup import async_setup_component +from tests.common import MockUser + @pytest.fixture def hass_ws_client(aiohttp_client): @@ -20,3 +22,17 @@ def hass_ws_client(aiohttp_client): return websocket return create_client + + +@pytest.fixture +def hass_access_token(hass): + """Return an access token to access Home Assistant.""" + user = MockUser().add_to_hass(hass) + client = hass.loop.run_until_complete(hass.auth.async_create_client( + 'Access Token Fixture', + redirect_uris=['/'], + no_secret=True, + )) + refresh_token = hass.loop.run_until_complete( + hass.auth.async_create_refresh_token(user, client.id)) + yield hass.auth.async_create_access_token(refresh_token) diff --git a/tests/components/test_api.py b/tests/components/test_api.py index c9dae27d14c..f53010ef27f 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -12,8 +12,6 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.common import mock_coro - @pytest.fixture def mock_api_client(hass, aiohttp_client): @@ -420,14 +418,14 @@ async def test_api_error_log(hass, aiohttp_client): assert resp.status == 401 with patch( - 'homeassistant.components.http.view.HomeAssistantView.file', - return_value=mock_coro(web.Response(status=200, text='Hello')) + 'aiohttp.web.FileResponse', + return_value=web.Response(status=200, text='Hello') ) as mock_file: resp = await client.get(const.URL_API_ERROR_LOG, headers={ 'x-ha-access': 'yolo' }) assert len(mock_file.mock_calls) == 1 - assert mock_file.mock_calls[0][1][1] == hass.data[DATA_LOGGING] + assert mock_file.mock_calls[0][1][0] == hass.data[DATA_LOGGING] assert resp.status == 200 assert await resp.text() == 'Hello' diff --git a/tests/components/test_websocket_api.py b/tests/components/test_websocket_api.py index 0a130e507d4..cff103142b0 100644 --- a/tests/components/test_websocket_api.py +++ b/tests/components/test_websocket_api.py @@ -313,3 +313,49 @@ def test_unknown_command(websocket_client): msg = yield from websocket_client.receive() assert msg.type == WSMsgType.close + + +async def test_auth_with_token(hass, aiohttp_client, hass_access_token): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': hass_access_token.token + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_OK + + +async def test_auth_with_invalid_token(hass, aiohttp_client): + """Test authenticating with a token.""" + assert await async_setup_component(hass, 'websocket_api', { + 'http': { + 'api_password': API_PASSWORD + } + }) + + client = await aiohttp_client(hass.http.app) + + async with client.ws_connect(wapi.URL) as ws: + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_REQUIRED + + await ws.send_json({ + 'type': wapi.TYPE_AUTH, + 'access_token': 'incorrect' + }) + + auth_msg = await ws.receive_json() + assert auth_msg['type'] == wapi.TYPE_AUTH_INVALID From 8d017b7678d654da25bcb171f9c15cfaa7a3cf80 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Thu, 10 May 2018 12:47:04 +0200 Subject: [PATCH 033/144] script/lint: Ensure there are files to test with pylint (#14363) --- script/lint | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/script/lint b/script/lint index dc6884f4882..8ba14d8939e 100755 --- a/script/lint +++ b/script/lint @@ -8,7 +8,7 @@ echo '=================================================' echo '= FILES CHANGED =' echo '=================================================' if [ -z "$files" ] ; then - echo "No python file changed. Rather use: tox -e lint" + echo "No python file changed. Rather use: tox -e lint\n" exit fi printf "%s\n" $files @@ -19,5 +19,10 @@ flake8 --doctests $files echo "================" echo "LINT with pylint" echo "================" -pylint $(echo "$files" | grep -v '^tests.*') +pylint_files=$(echo "$files" | grep -v '^tests.*') +if [ -z "$pylint_files" ] ; then + echo "Only test files changed. Skipping\n" + exit +fi +pylint $pylint_files echo From eb2671f4bbbcb270784597ae8ddf166d7bc65119 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 10 May 2018 13:18:13 +0200 Subject: [PATCH 034/144] Update .coveragerc (#14368) --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 1f2a8f8d233..28fe39430f3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,8 @@ source = homeassistant omit = homeassistant/__main__.py homeassistant/scripts/*.py + homeassistant/util/async.py + homeassistant/monkey_patch.py homeassistant/helpers/typing.py homeassistant/helpers/signal.py From 6e831138b407776de549b29907c9efb04fe0a0ea Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 10 May 2018 19:59:23 +0200 Subject: [PATCH 035/144] Fix binary_sensor async_update (#14376) --- homeassistant/components/binary_sensor/zha.py | 3 ++- homeassistant/components/zha/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 756323f41d9..be61a9e9ba4 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -133,7 +133,8 @@ class BinarySensor(zha.Entity, BinarySensorDevice): from bellows.types.basic import uint16_t result = await zha.safe_read(self._endpoint.ias_zone, - ['zone_status']) + ['zone_status'], + allow_cache=False) state = result.get('zone_status', self._state) if isinstance(state, (int, uint16_t)): self._state = result.get('zone_status', self._state) & 3 diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 9d7556fc334..d293d4d07cd 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -410,7 +410,7 @@ def get_discovery_info(hass, discovery_info): return all_discovery_info.get(discovery_key, None) -async def safe_read(cluster, attributes): +async def safe_read(cluster, attributes, allow_cache=True): """Swallow all exceptions from network read. If we throw during initialization, setup fails. Rather have an entity that @@ -420,7 +420,7 @@ async def safe_read(cluster, attributes): try: result, _ = await cluster.read_attributes( attributes, - allow_cache=True, + allow_cache=allow_cache, ) return result except Exception: # pylint: disable=broad-except From ea01b127c277d3539a2b4d2362d73afa9b26b9b4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 14:09:22 -0400 Subject: [PATCH 036/144] Add local auth provider (#14365) * Add local auth provider * Lint * Docstring --- homeassistant/auth.py | 22 +-- homeassistant/auth_providers/homeassistant.py | 181 ++++++++++++++++++ .../auth_providers/insecure_example.py | 14 +- homeassistant/scripts/auth.py | 78 ++++++++ tests/auth_providers/test_homeassistant.py | 124 ++++++++++++ tests/auth_providers/test_insecure_example.py | 18 +- tests/scripts/test_auth.py | 100 ++++++++++ 7 files changed, 501 insertions(+), 36 deletions(-) create mode 100644 homeassistant/auth_providers/homeassistant.py create mode 100644 homeassistant/scripts/auth.py create mode 100644 tests/auth_providers/test_homeassistant.py create mode 100644 tests/scripts/test_auth.py diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 5c9d437e067..2c6c95f9b42 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -15,7 +15,6 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.core import callback from homeassistant.const import CONF_TYPE, CONF_NAME, CONF_ID -from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry from homeassistant.util import dt as dt_util @@ -36,22 +35,6 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) DATA_REQS = 'auth_reqs_processed' -class AuthError(HomeAssistantError): - """Generic authentication error.""" - - -class InvalidUser(AuthError): - """Raised when an invalid user has been specified.""" - - -class InvalidPassword(AuthError): - """Raised when an invalid password has been supplied.""" - - -class UnknownError(AuthError): - """When an unknown error occurs.""" - - def generate_secret(entropy=32): """Generate a secret. @@ -69,8 +52,9 @@ class AuthProvider: initialized = False - def __init__(self, store, config): + def __init__(self, hass, store, config): """Initialize an auth provider.""" + self.hass = hass self.store = store self.config = config @@ -284,7 +268,7 @@ async def _auth_provider_from_config(hass, store, config): provider_name, humanize_error(config, err)) return None - return AUTH_PROVIDERS[provider_name](store, config) + return AUTH_PROVIDERS[provider_name](hass, store, config) class AuthManager: diff --git a/homeassistant/auth_providers/homeassistant.py b/homeassistant/auth_providers/homeassistant.py new file mode 100644 index 00000000000..c2db193ce1a --- /dev/null +++ b/homeassistant/auth_providers/homeassistant.py @@ -0,0 +1,181 @@ +"""Home Assistant auth provider.""" +import base64 +from collections import OrderedDict +import hashlib +import hmac + +import voluptuous as vol + +from homeassistant import auth, data_entry_flow +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import json + + +PATH_DATA = '.users.json' + +CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ +}, extra=vol.PREVENT_EXTRA) + + +class InvalidAuth(HomeAssistantError): + """Raised when we encounter invalid authentication.""" + + +class InvalidUser(HomeAssistantError): + """Raised when invalid user is specified. + + Will not be raised when validating authentication. + """ + + +class Data: + """Hold the user data.""" + + def __init__(self, path, data): + """Initialize the user data store.""" + self.path = path + if data is None: + data = { + 'salt': auth.generate_secret(), + 'users': [] + } + self._data = data + + @property + def users(self): + """Return users.""" + return self._data['users'] + + def validate_login(self, username, password): + """Validate a username and password. + + Raises InvalidAuth if auth invalid. + """ + password = self.hash_password(password) + + found = None + + # Compare all users to avoid timing attacks. + for user in self._data['users']: + if username == user['username']: + found = user + + if found is None: + # Do one more compare to make timing the same as if user was found. + hmac.compare_digest(password, password) + raise InvalidAuth + + if not hmac.compare_digest(password, + base64.b64decode(found['password'])): + raise InvalidAuth + + def hash_password(self, password, for_storage=False): + """Encode a password.""" + hashed = hashlib.pbkdf2_hmac( + 'sha512', password.encode(), self._data['salt'].encode(), 100000) + if for_storage: + hashed = base64.b64encode(hashed).decode() + return hashed + + def add_user(self, username, password): + """Add a user.""" + if any(user['username'] == username for user in self.users): + raise InvalidUser + + self.users.append({ + 'username': username, + 'password': self.hash_password(password, True), + }) + + def change_password(self, username, new_password): + """Update the password of a user. + + Raises InvalidUser if user cannot be found. + """ + for user in self.users: + if user['username'] == username: + user['password'] = self.hash_password(new_password, True) + break + else: + raise InvalidUser + + def save(self): + """Save data.""" + json.save_json(self.path, self._data) + + +def load_data(path): + """Load auth data.""" + return Data(path, json.load_json(path, None)) + + +@auth.AUTH_PROVIDERS.register('homeassistant') +class HassAuthProvider(auth.AuthProvider): + """Auth provider based on a local storage of users in HASS config dir.""" + + DEFAULT_TITLE = 'Home Assistant Local' + + async def async_credential_flow(self): + """Return a flow to login.""" + return LoginFlow(self) + + async def async_validate_login(self, username, password): + """Helper to validate a username and password.""" + def validate(): + """Validate creds.""" + data = self._auth_data() + data.validate_login(username, password) + + await self.hass.async_add_job(validate) + + async def async_get_or_create_credentials(self, flow_result): + """Get credentials based on the flow result.""" + username = flow_result['username'] + + for credential in await self.async_credentials(): + if credential.data['username'] == username: + return credential + + # Create new credentials. + return self.async_create_credentials({ + 'username': username + }) + + def _auth_data(self): + """Return the auth provider data.""" + return load_data(self.hass.config.path(PATH_DATA)) + + +class LoginFlow(data_entry_flow.FlowHandler): + """Handler for the login flow.""" + + def __init__(self, auth_provider): + """Initialize the login flow.""" + self._auth_provider = auth_provider + + async def async_step_init(self, user_input=None): + """Handle the step of the form.""" + errors = {} + + if user_input is not None: + try: + await self._auth_provider.async_validate_login( + user_input['username'], user_input['password']) + except InvalidAuth: + errors['base'] = 'invalid_auth' + + if not errors: + return self.async_create_entry( + title=self._auth_provider.name, + data=user_input + ) + + schema = OrderedDict() + schema['username'] = str + schema['password'] = str + + return self.async_show_form( + step_id='init', + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/homeassistant/auth_providers/insecure_example.py b/homeassistant/auth_providers/insecure_example.py index 8538e8c2f3e..a8e8cd0cb0e 100644 --- a/homeassistant/auth_providers/insecure_example.py +++ b/homeassistant/auth_providers/insecure_example.py @@ -4,6 +4,7 @@ import hmac import voluptuous as vol +from homeassistant.exceptions import HomeAssistantError from homeassistant import auth, data_entry_flow from homeassistant.core import callback @@ -20,6 +21,10 @@ CONFIG_SCHEMA = auth.AUTH_PROVIDER_SCHEMA.extend({ }, extra=vol.PREVENT_EXTRA) +class InvalidAuthError(HomeAssistantError): + """Raised when submitting invalid authentication.""" + + @auth.AUTH_PROVIDERS.register('insecure_example') class ExampleAuthProvider(auth.AuthProvider): """Example auth provider based on hardcoded usernames and passwords.""" @@ -43,18 +48,15 @@ class ExampleAuthProvider(auth.AuthProvider): # Do one more compare to make timing the same as if user was found. hmac.compare_digest(password.encode('utf-8'), password.encode('utf-8')) - raise auth.InvalidUser + raise InvalidAuthError if not hmac.compare_digest(user['password'].encode('utf-8'), password.encode('utf-8')): - raise auth.InvalidPassword + raise InvalidAuthError async def async_get_or_create_credentials(self, flow_result): """Get credentials based on the flow result.""" username = flow_result['username'] - password = flow_result['password'] - - self.async_validate_login(username, password) for credential in await self.async_credentials(): if credential.data['username'] == username: @@ -96,7 +98,7 @@ class LoginFlow(data_entry_flow.FlowHandler): try: self._auth_provider.async_validate_login( user_input['username'], user_input['password']) - except (auth.InvalidUser, auth.InvalidPassword): + except InvalidAuthError: errors['base'] = 'invalid_auth' if not errors: diff --git a/homeassistant/scripts/auth.py b/homeassistant/scripts/auth.py new file mode 100644 index 00000000000..b4f1ddd2f11 --- /dev/null +++ b/homeassistant/scripts/auth.py @@ -0,0 +1,78 @@ +"""Script to manage users for the Home Assistant auth provider.""" +import argparse +import os + +from homeassistant.config import get_default_config_dir +from homeassistant.auth_providers import homeassistant as hass_auth + + +def run(args): + """Handle Home Assistant auth provider script.""" + parser = argparse.ArgumentParser( + description=("Manage Home Assistant users")) + parser.add_argument( + '--script', choices=['auth']) + parser.add_argument( + '-c', '--config', + default=get_default_config_dir(), + help="Directory that contains the Home Assistant configuration") + + subparsers = parser.add_subparsers() + parser_list = subparsers.add_parser('list') + parser_list.set_defaults(func=list_users) + + parser_add = subparsers.add_parser('add') + parser_add.add_argument('username', type=str) + parser_add.add_argument('password', type=str) + parser_add.set_defaults(func=add_user) + + parser_validate_login = subparsers.add_parser('validate') + parser_validate_login.add_argument('username', type=str) + parser_validate_login.add_argument('password', type=str) + parser_validate_login.set_defaults(func=validate_login) + + parser_change_pw = subparsers.add_parser('change_password') + parser_change_pw.add_argument('username', type=str) + parser_change_pw.add_argument('new_password', type=str) + parser_change_pw.set_defaults(func=change_password) + + args = parser.parse_args(args) + path = os.path.join(os.getcwd(), args.config, hass_auth.PATH_DATA) + args.func(hass_auth.load_data(path), args) + + +def list_users(data, args): + """List the users.""" + count = 0 + for user in data.users: + count += 1 + print(user['username']) + + print() + print("Total users:", count) + + +def add_user(data, args): + """Create a user.""" + data.add_user(args.username, args.password) + data.save() + print("User created") + + +def validate_login(data, args): + """Validate a login.""" + try: + data.validate_login(args.username, args.password) + print("Auth valid") + except hass_auth.InvalidAuth: + print("Auth invalid") + + +def change_password(data, args): + """Change password.""" + try: + data.change_password(args.username, args.new_password) + data.save() + print("Password changed") + except hass_auth.InvalidUser: + print("User not found") diff --git a/tests/auth_providers/test_homeassistant.py b/tests/auth_providers/test_homeassistant.py new file mode 100644 index 00000000000..8b12e682865 --- /dev/null +++ b/tests/auth_providers/test_homeassistant.py @@ -0,0 +1,124 @@ +"""Test the Home Assistant local auth provider.""" +from unittest.mock import patch, mock_open + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.auth_providers import homeassistant as hass_auth + + +MOCK_PATH = '/bla/users.json' +JSON__OPEN_PATH = 'homeassistant.util.json.open' + + +def test_initialize_empty_config_file_not_found(): + """Test that we initialize an empty config.""" + with patch('homeassistant.util.json.open', side_effect=FileNotFoundError): + data = hass_auth.load_data(MOCK_PATH) + + assert data is not None + + +def test_adding_user(): + """Test adding a user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + data.validate_login('test-user', 'test-pass') + + +def test_adding_user_duplicate_username(): + """Test adding a user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + with pytest.raises(hass_auth.InvalidUser): + data.add_user('test-user', 'other-pass') + + +def test_validating_password_invalid_user(): + """Test validating an invalid user.""" + data = hass_auth.Data(MOCK_PATH, None) + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('non-existing', 'pw') + + +def test_validating_password_invalid_password(): + """Test validating an invalid user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'invalid-pass') + + +def test_changing_password(): + """Test adding a user.""" + user = 'test-user' + data = hass_auth.Data(MOCK_PATH, None) + data.add_user(user, 'test-pass') + data.change_password(user, 'new-pass') + + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login(user, 'test-pass') + + data.validate_login(user, 'new-pass') + + +def test_changing_password_raises_invalid_user(): + """Test that we initialize an empty config.""" + data = hass_auth.Data(MOCK_PATH, None) + + with pytest.raises(hass_auth.InvalidUser): + data.change_password('non-existing', 'pw') + + +async def test_login_flow_validates(hass): + """Test login flow.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + provider = hass_auth.HassAuthProvider(hass, None, {}) + flow = hass_auth.LoginFlow(provider) + result = await flow.async_step_init() + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + + with patch.object(provider, '_auth_data', return_value=data): + result = await flow.async_step_init({ + 'username': 'incorrect-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' + + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'incorrect-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['errors']['base'] == 'invalid_auth' + + result = await flow.async_step_init({ + 'username': 'test-user', + 'password': 'test-pass', + }) + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_saving_loading(hass): + """Test saving and loading JSON.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + data.add_user('second-user', 'second-pass') + + with patch(JSON__OPEN_PATH, mock_open(), create=True) as mock_write: + await hass.async_add_job(data.save) + + # Mock open calls are: open file, context enter, write, context leave + written = mock_write.mock_calls[2][1][0] + + with patch('os.path.isfile', return_value=True), \ + patch(JSON__OPEN_PATH, mock_open(read_data=written), create=True): + await hass.async_add_job(hass_auth.load_data, MOCK_PATH) + + data.validate_login('test-user', 'test-pass') + data.validate_login('second-user', 'second-pass') diff --git a/tests/auth_providers/test_insecure_example.py b/tests/auth_providers/test_insecure_example.py index 92fc2974e27..0b481f93099 100644 --- a/tests/auth_providers/test_insecure_example.py +++ b/tests/auth_providers/test_insecure_example.py @@ -19,7 +19,7 @@ def store(): @pytest.fixture def provider(store): """Mock provider.""" - return insecure_example.ExampleAuthProvider(store, { + return insecure_example.ExampleAuthProvider(None, store, { 'type': 'insecure_example', 'users': [ { @@ -64,20 +64,16 @@ async def test_match_existing_credentials(store, provider): async def test_verify_username(provider): """Test we raise if incorrect user specified.""" - with pytest.raises(auth.InvalidUser): - await provider.async_get_or_create_credentials({ - 'username': 'non-existing-user', - 'password': 'password-test', - }) + with pytest.raises(insecure_example.InvalidAuthError): + await provider.async_validate_login( + 'non-existing-user', 'password-test') async def test_verify_password(provider): """Test we raise if incorrect user specified.""" - with pytest.raises(auth.InvalidPassword): - await provider.async_get_or_create_credentials({ - 'username': 'user-test', - 'password': 'incorrect-password', - }) + with pytest.raises(insecure_example.InvalidAuthError): + await provider.async_validate_login( + 'user-test', 'incorrect-password') async def test_utf_8_username_password(provider): diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py new file mode 100644 index 00000000000..2e837b06b58 --- /dev/null +++ b/tests/scripts/test_auth.py @@ -0,0 +1,100 @@ +"""Test the auth script to manage local users.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.scripts import auth as script_auth +from homeassistant.auth_providers import homeassistant as hass_auth + +MOCK_PATH = '/bla/users.json' + + +def test_list_user(capsys): + """Test we can list users.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + data.add_user('second-user', 'second-pass') + + script_auth.list_users(data, None) + + captured = capsys.readouterr() + + assert captured.out == '\n'.join([ + 'test-user', + 'second-user', + '', + 'Total users: 2', + '' + ]) + + +def test_add_user(capsys): + """Test we can add a user.""" + data = hass_auth.Data(MOCK_PATH, None) + + with patch.object(data, 'save') as mock_save: + script_auth.add_user( + data, Mock(username='paulus', password='test-pass')) + + assert len(mock_save.mock_calls) == 1 + + captured = capsys.readouterr() + assert captured.out == 'User created\n' + + assert len(data.users) == 1 + data.validate_login('paulus', 'test-pass') + + +def test_validate_login(capsys): + """Test we can validate a user login.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + script_auth.validate_login( + data, Mock(username='test-user', password='test-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth valid\n' + + script_auth.validate_login( + data, Mock(username='test-user', password='invalid-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth invalid\n' + + script_auth.validate_login( + data, Mock(username='invalid-user', password='test-pass')) + captured = capsys.readouterr() + assert captured.out == 'Auth invalid\n' + + +def test_change_password(capsys): + """Test we can change a password.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + with patch.object(data, 'save') as mock_save: + script_auth.change_password( + data, Mock(username='test-user', new_password='new-pass')) + + assert len(mock_save.mock_calls) == 1 + captured = capsys.readouterr() + assert captured.out == 'Password changed\n' + data.validate_login('test-user', 'new-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('test-user', 'test-pass') + + +def test_change_password_invalid_user(capsys): + """Test changing password of non-existing user.""" + data = hass_auth.Data(MOCK_PATH, None) + data.add_user('test-user', 'test-pass') + + with patch.object(data, 'save') as mock_save: + script_auth.change_password( + data, Mock(username='invalid-user', new_password='new-pass')) + + assert len(mock_save.mock_calls) == 0 + captured = capsys.readouterr() + assert captured.out == 'User not found\n' + data.validate_login('test-user', 'test-pass') + with pytest.raises(hass_auth.InvalidAuth): + data.validate_login('invalid-user', 'new-pass') From f168226be9880feef1506a5018557836c080f7ef Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 10 May 2018 22:11:02 +0300 Subject: [PATCH 037/144] Update to sensibo 1.0.3 with better error reporting (#14380) --- homeassistant/components/climate/sensibo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/climate/sensibo.py b/homeassistant/components/climate/sensibo.py index e2a455aefc7..2b92d050d3b 100644 --- a/homeassistant/components/climate/sensibo.py +++ b/homeassistant/components/climate/sensibo.py @@ -25,7 +25,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.temperature import convert as convert_temperature -REQUIREMENTS = ['pysensibo==1.0.2'] +REQUIREMENTS = ['pysensibo==1.0.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1400237a683..3f301589264 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -913,7 +913,7 @@ pyrainbird==0.1.3 pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo -pysensibo==1.0.2 +pysensibo==1.0.3 # homeassistant.components.sensor.serial pyserial-asyncio==0.4 From db31cdf075f17a338b01a2a5499a3c82df66c6ab Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 10 May 2018 21:28:57 +0200 Subject: [PATCH 038/144] Fix binary_sensor device_state_attributes (#14375) --- homeassistant/components/binary_sensor/zha.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index be61a9e9ba4..4f3f824c8f9 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -219,7 +219,10 @@ class Switch(zha.Entity, BinarySensorDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - return {'level': self._state and self._level or 0} + self._device_state_attributes.update({ + 'level': self._state and self._level or 0 + }) + return self._device_state_attributes def move_level(self, change): """Increment the level, setting state if appropriate.""" From f192ef8219cdbd20423357c1814b1e7174437f8d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 17:13:00 -0400 Subject: [PATCH 039/144] Remove domain expiry sensor (#14381) --- .../components/sensor/domain_expiry.py | 76 ------------------- requirements_all.txt | 3 - 2 files changed, 79 deletions(-) delete mode 100644 homeassistant/components/sensor/domain_expiry.py diff --git a/homeassistant/components/sensor/domain_expiry.py b/homeassistant/components/sensor/domain_expiry.py deleted file mode 100644 index 9364ce041f2..00000000000 --- a/homeassistant/components/sensor/domain_expiry.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Counter for the days till domain will expire. - -For more details about this sensor please refer to the documentation at -https://home-assistant.io/components/sensor.domain_expiry/ -""" -import logging -from datetime import datetime, timedelta - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_DOMAIN) -from homeassistant.helpers.entity import Entity - -REQUIREMENTS = ['python-whois==0.6.9'] - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = 'Domain Expiry' - -SCAN_INTERVAL = timedelta(hours=24) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DOMAIN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string -}) - - -def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up domain expiry sensor.""" - server_name = config.get(CONF_DOMAIN) - sensor_name = config.get(CONF_NAME) - - add_devices([DomainExpiry(sensor_name, server_name)], True) - - -class DomainExpiry(Entity): - """Implementation of the domain expiry sensor.""" - - def __init__(self, sensor_name, server_name): - """Initialize the sensor.""" - self.server_name = server_name - self._name = sensor_name - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return 'days' - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return 'mdi:earth' - - def update(self): - """Fetch the domain information.""" - import whois - domain = whois.whois(self.server_name) - if isinstance(domain.expiration_date, datetime): - expiry = domain.expiration_date - datetime.today() - self._state = expiry.days - else: - _LOGGER.error("Cannot get expiry date for %s", self.server_name) diff --git a/requirements_all.txt b/requirements_all.txt index 3f301589264..bfaeb72ae7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1041,9 +1041,6 @@ python-velbus==2.0.11 # homeassistant.components.media_player.vlc python-vlc==1.1.2 -# homeassistant.components.sensor.domain_expiry -python-whois==0.6.9 - # homeassistant.components.wink python-wink==1.7.3 From bc664c276c2050544b9ff96196a79b8ed1338669 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 May 2018 17:33:10 -0400 Subject: [PATCH 040/144] Bump frontend to 20180510.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c30e0dfb69f..f60d095a682 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180509.0'] +REQUIREMENTS = ['home-assistant-frontend==20180510.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index bfaeb72ae7e..cbc5cce0590 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180509.0 +home-assistant-frontend==20180510.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a25f36a8195..630ed06580c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180509.0 +home-assistant-frontend==20180510.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From e963fc5acf610333410b33c58d63f2b6e343f258 Mon Sep 17 00:00:00 2001 From: damarco Date: Thu, 10 May 2018 23:55:32 +0200 Subject: [PATCH 041/144] Add support for pressure sensors (#14361) --- homeassistant/components/sensor/zha.py | 21 ++++++++++++++++++++- homeassistant/components/zha/const.py | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index d856ed1a17e..41dab282997 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -32,13 +32,15 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): def make_sensor(discovery_info): """Create ZHA sensors factory.""" from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, TemperatureMeasurement + RelativeHumidity, TemperatureMeasurement, PressureMeasurement ) in_clusters = discovery_info['in_clusters'] if RelativeHumidity.cluster_id in in_clusters: sensor = RelativeHumiditySensor(**discovery_info) elif TemperatureMeasurement.cluster_id in in_clusters: sensor = TemperatureSensor(**discovery_info) + elif PressureMeasurement.cluster_id in in_clusters: + sensor = PressureSensor(**discovery_info) else: sensor = Sensor(**discovery_info) @@ -111,3 +113,20 @@ class RelativeHumiditySensor(Sensor): return 'unknown' return round(float(self._state) / 100, 1) + + +class PressureSensor(Sensor): + """ZHA pressure sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return 'hPa' + + @property + def state(self): + """Return the state of the entity.""" + if self._state == 'unknown': + return 'unknown' + + return round(float(self._state)) diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 36eb4d55c97..1c083c3ca93 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -47,6 +47,7 @@ def populate_data(): zcl.clusters.general.OnOff: 'switch', zcl.clusters.measurement.RelativeHumidity: 'sensor', zcl.clusters.measurement.TemperatureMeasurement: 'sensor', + zcl.clusters.measurement.PressureMeasurement: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) From 6843893d9f334467d82d89a9a218114e91d082f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Thu, 10 May 2018 23:22:02 +0100 Subject: [PATCH 042/144] Add "framerate" parameter to generic camera (#14079) * add "framerate" parameter to generic camera * fix lint --- homeassistant/components/camera/__init__.py | 15 +++++++++------ homeassistant/components/camera/generic.py | 8 ++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index c1f92965198..60f8979bb16 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -256,6 +256,11 @@ class Camera(Entity): """Return the camera model.""" return None + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return 0.5 + def camera_image(self): """Return bytes of camera image.""" raise NotImplementedError() @@ -272,10 +277,6 @@ class Camera(Entity): This method must be run in the event loop. """ - if interval < MIN_STREAM_INTERVAL: - raise ValueError("Stream interval must be be > {}" - .format(MIN_STREAM_INTERVAL)) - response = web.StreamResponse() response.content_type = ('multipart/x-mixed-replace; ' 'boundary=--frameboundary') @@ -325,8 +326,7 @@ class Camera(Entity): a direct stream from the camera. This method must be run in the event loop. """ - await self.handle_async_still_stream(request, - FALLBACK_STREAM_INTERVAL) + await self.handle_async_still_stream(request, self.frame_interval) @property def state(self): @@ -448,6 +448,9 @@ class CameraMjpegStream(CameraView): try: # Compose camera stream from stills interval = float(request.query.get('interval')) + if interval < MIN_STREAM_INTERVAL: + raise ValueError("Stream interval must be be > {}" + .format(MIN_STREAM_INTERVAL)) await camera.handle_async_still_stream(request, interval) return except ValueError: diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 2f5d8d28979..e11bd599e45 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) CONF_CONTENT_TYPE = 'content_type' CONF_LIMIT_REFETCH_TO_URL_CHANGE = 'limit_refetch_to_url_change' CONF_STILL_IMAGE_URL = 'still_image_url' +CONF_FRAMERATE = 'framerate' DEFAULT_NAME = 'Generic Camera' @@ -40,6 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_CONTENT_TYPE, default=DEFAULT_CONTENT_TYPE): cv.string, + vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, }) @@ -62,6 +64,7 @@ class GenericCamera(Camera): self._still_image_url = device_info[CONF_STILL_IMAGE_URL] self._still_image_url.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] + self._frame_interval = 1 / device_info[CONF_FRAMERATE] self.content_type = device_info[CONF_CONTENT_TYPE] username = device_info.get(CONF_USERNAME) @@ -78,6 +81,11 @@ class GenericCamera(Camera): self._last_url = None self._last_image = None + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return self._frame_interval + def camera_image(self): """Return bytes of camera image.""" return run_coroutine_threadsafe( From 8fcf085829def67fa4e79b0591f037d822da5703 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 11 May 2018 01:21:59 +0200 Subject: [PATCH 043/144] Rewritten HomeKit tests (#14377) * Use pytest fixtures and parametrize * Use async --- tests/components/homekit/test_accessories.py | 220 +++---- .../homekit/test_get_accessories.py | 253 +++----- tests/components/homekit/test_homekit.py | 308 +++++---- tests/components/homekit/test_type_covers.py | 396 ++++++------ tests/components/homekit/test_type_lights.py | 280 ++++---- tests/components/homekit/test_type_locks.py | 102 ++- .../homekit/test_type_security_systems.py | 188 +++--- tests/components/homekit/test_type_sensors.py | 298 +++++---- .../components/homekit/test_type_switches.py | 121 +--- .../homekit/test_type_thermostats.py | 607 +++++++++--------- tests/components/homekit/test_util.py | 73 +-- 11 files changed, 1255 insertions(+), 1591 deletions(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index faa982f62f3..48c6357c28d 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,8 +3,7 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -import unittest -from unittest.mock import call, patch, Mock +from unittest.mock import patch, Mock from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) @@ -15,8 +14,6 @@ from homeassistant.components.homekit.const import ( from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -from tests.common import get_test_home_assistant - def patch_debounce(): """Return patch for debounce method.""" @@ -24,141 +21,122 @@ def patch_debounce(): lambda f: lambda *args, **kwargs: f(*args, **kwargs)) -class TestAccessories(unittest.TestCase): - """Test pyhap adapter methods.""" +async def test_debounce(hass): + """Test add_timeout decorator function.""" + def demo_func(*args): + nonlocal arguments, counter + counter += 1 + arguments = args - def test_debounce(self): - """Test add_timeout decorator function.""" - def demo_func(*args): - nonlocal arguments, counter - counter += 1 - arguments = args + arguments = None + counter = 0 + mock = Mock(hass=hass) - arguments = None - counter = 0 - hass = get_test_home_assistant() - mock = Mock(hass=hass) + debounce_demo = debounce(demo_func) + assert debounce_demo.__name__ == 'demo_func' + now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) - debounce_demo = debounce(demo_func) - self.assertEqual(debounce_demo.__name__, 'demo_func') - now = datetime(2018, 1, 1, 20, 0, 0, tzinfo=dt_util.UTC) + with patch('homeassistant.util.dt.utcnow', return_value=now): + await hass.async_add_job(debounce_demo, mock, 'value') + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + await hass.async_block_till_done() + assert counter == 1 + assert len(arguments) == 2 - with patch('homeassistant.util.dt.utcnow', return_value=now): - debounce_demo(mock, 'value') - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) - hass.block_till_done() - assert counter == 1 - assert len(arguments) == 2 + with patch('homeassistant.util.dt.utcnow', return_value=now): + await hass.async_add_job(debounce_demo, mock, 'value') + await hass.async_add_job(debounce_demo, mock, 'value') - with patch('homeassistant.util.dt.utcnow', return_value=now): - debounce_demo(mock, 'value') - debounce_demo(mock, 'value') + hass.bus.async_fire( + EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) + await hass.async_block_till_done() + assert counter == 2 - hass.bus.fire( - EVENT_TIME_CHANGED, {ATTR_NOW: now + timedelta(seconds=3)}) - hass.block_till_done() - assert counter == 2 - hass.stop() +async def test_home_accessory(hass): + """Test HomeAccessory class.""" + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) + assert acc.hass == hass + assert acc.display_name == 'Home Accessory' + assert acc.category == 1 # Category.OTHER + assert len(acc.services) == 1 + serv = acc.services[0] # SERV_ACCESSORY_INFO + assert serv.display_name == SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == 'Home Accessory' + assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER + assert serv.get_characteristic(CHAR_MODEL).value == 'Homekit' + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ + 'homekit.accessory' - def test_home_accessory(self): - """Test HomeAccessory class.""" - hass = get_test_home_assistant() + hass.states.async_set('homekit.accessory', 'on') + await hass.async_block_till_done() + await hass.async_add_job(acc.run) + hass.states.async_set('homekit.accessory', 'off') + await hass.async_block_till_done() - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) - self.assertEqual(acc.hass, hass) - self.assertEqual(acc.display_name, 'Home Accessory') - self.assertEqual(acc.category, 1) # Category.OTHER - self.assertEqual(len(acc.services), 1) - serv = acc.services[0] # SERV_ACCESSORY_INFO - self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) - self.assertEqual( - serv.get_characteristic(CHAR_NAME).value, 'Home Accessory') - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'Homekit') - self.assertEqual(serv.get_characteristic(CHAR_SERIAL_NUMBER).value, - 'homekit.accessory') + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) + assert acc.display_name == 'test_name' + assert acc.aid == 2 + assert len(acc.services) == 1 + serv = acc.services[0] # SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' - hass.states.set('homekit.accessory', 'on') - hass.block_till_done() - acc.run() - hass.states.set('homekit.accessory', 'off') - hass.block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) - self.assertEqual(acc.display_name, 'test_name') - self.assertEqual(acc.aid, 2) - self.assertEqual(len(acc.services), 1) - serv = acc.services[0] # SERV_ACCESSORY_INFO - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, 'Test Model') +def test_home_bridge(): + """Test HomeBridge class.""" + bridge = HomeBridge('hass') + assert bridge.hass == 'hass' + assert bridge.display_name == BRIDGE_NAME + assert bridge.category == 2 # Category.BRIDGE + assert len(bridge.services) == 1 + serv = bridge.services[0] # SERV_ACCESSORY_INFO + assert serv.display_name == SERV_ACCESSORY_INFO + assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME + assert serv.get_characteristic(CHAR_FIRMWARE_REVISION).value == __version__ + assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER + assert serv.get_characteristic(CHAR_MODEL).value == BRIDGE_MODEL + assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ + BRIDGE_SERIAL_NUMBER - hass.stop() + bridge = HomeBridge('hass', 'test_name') + assert bridge.display_name == 'test_name' + assert len(bridge.services) == 1 + serv = bridge.services[0] # SERV_ACCESSORY_INFO - def test_home_bridge(self): - """Test HomeBridge class.""" - bridge = HomeBridge('hass') - self.assertEqual(bridge.hass, 'hass') - self.assertEqual(bridge.display_name, BRIDGE_NAME) - self.assertEqual(bridge.category, 2) # Category.BRIDGE - self.assertEqual(len(bridge.services), 1) - serv = bridge.services[0] # SERV_ACCESSORY_INFO - self.assertEqual(serv.display_name, SERV_ACCESSORY_INFO) - self.assertEqual( - serv.get_characteristic(CHAR_NAME).value, BRIDGE_NAME) - self.assertEqual( - serv.get_characteristic(CHAR_FIRMWARE_REVISION).value, __version__) - self.assertEqual( - serv.get_characteristic(CHAR_MANUFACTURER).value, MANUFACTURER) - self.assertEqual( - serv.get_characteristic(CHAR_MODEL).value, BRIDGE_MODEL) - self.assertEqual( - serv.get_characteristic(CHAR_SERIAL_NUMBER).value, - BRIDGE_SERIAL_NUMBER) + # setup_message + bridge.setup_message() - bridge = HomeBridge('hass', 'test_name') - self.assertEqual(bridge.display_name, 'test_name') - self.assertEqual(len(bridge.services), 1) - serv = bridge.services[0] # SERV_ACCESSORY_INFO + # add_paired_client + with patch('pyhap.accessory.Accessory.add_paired_client') \ + as mock_add_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + bridge.add_paired_client('client_uuid', 'client_public') - # setup_message - bridge.setup_message() + mock_add_paired_client.assert_called_with('client_uuid', 'client_public') + mock_dissmiss_msg.assert_called_with('hass') - # add_paired_client - with patch('pyhap.accessory.Accessory.add_paired_client') \ - as mock_add_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'dismiss_setup_message') as mock_dissmiss_msg: - bridge.add_paired_client('client_uuid', 'client_public') + # remove_paired_client + with patch('pyhap.accessory.Accessory.remove_paired_client') \ + as mock_remove_paired_client, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + bridge.remove_paired_client('client_uuid') - self.assertEqual(mock_add_paired_client.call_args, - call('client_uuid', 'client_public')) - self.assertEqual(mock_dissmiss_msg.call_args, call('hass')) + mock_remove_paired_client.assert_called_with('client_uuid') + mock_show_msg.assert_called_with('hass', bridge) - # remove_paired_client - with patch('pyhap.accessory.Accessory.remove_paired_client') \ - as mock_remove_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'show_setup_message') as mock_show_msg: - bridge.remove_paired_client('client_uuid') - self.assertEqual( - mock_remove_paired_client.call_args, call('client_uuid')) - self.assertEqual(mock_show_msg.call_args, call('hass', bridge)) +def test_home_driver(): + """Test HomeDriver class.""" + bridge = HomeBridge('hass') + ip_address = '127.0.0.1' + port = 51826 + path = '.homekit.state' - def test_home_driver(self): - """Test HomeDriver class.""" - bridge = HomeBridge('hass') - ip_address = '127.0.0.1' - port = 51826 - path = '.homekit.state' + with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ + as mock_driver: + HomeDriver(bridge, ip_address, port, path) - with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ - as mock_driver: - HomeDriver(bridge, ip_address, port, path) - - self.assertEqual( - mock_driver.call_args, call(bridge, ip_address, port, path)) + mock_driver.assert_called_with(bridge, ip_address, port, path) diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index cff52b2ff20..2ff591983c6 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,22 +1,20 @@ """Package to test the get_accessory method.""" import logging -import unittest from unittest.mock import patch, Mock +import pytest + from homeassistant.core import State -from homeassistant.components.cover import ( - SUPPORT_OPEN, SUPPORT_CLOSE) +from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( - ATTR_CODE, ATTR_UNIT_OF_MEASUREMENT, ATTR_SUPPORTED_FEATURES, - TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_DEVICE_CLASS) + ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) _LOGGER = logging.getLogger(__name__) -CONFIG = {} - def test_get_accessory_invalid_aid(caplog): """Test with unsupported component.""" @@ -32,182 +30,93 @@ def test_not_supported(): is None -class TestGetAccessories(unittest.TestCase): - """Methods to test the get_accessory method.""" +@pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Light', 'light.test', 'on', {}, None), + ('Lock', 'lock.test', 'locked', {}, None), - def setUp(self): - """Setup Mock type.""" - self.mock_type = Mock() + ('Thermostat', 'climate.test', 'auto', {}, None), + ('Thermostat', 'climate.test', 'auto', + {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | + SUPPORT_TARGET_TEMPERATURE_HIGH}, None), - def tearDown(self): - """Test if mock type was called.""" - self.assertTrue(self.mock_type.called) + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), +]) +def test_types(type_name, entity_id, state, attrs, config): + """Test if types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, config) + assert mock_type.called - def test_sensor_temperature(self): - """Test temperature sensor with device class temperature.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '23', - {ATTR_DEVICE_CLASS: 'temperature'}) - get_accessory(None, state, 2, {}) + if config: + assert mock_type.call_args[1]['config'] == config - def test_sensor_temperature_celsius(self): - """Test temperature sensor with Celsius as unit.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '23', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - get_accessory(None, state, 2, {}) - def test_sensor_temperature_fahrenheit(self): - """Test temperature sensor with Fahrenheit as unit.""" - with patch.dict(TYPES, {'TemperatureSensor': self.mock_type}): - state = State('sensor.temperature', '74', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('GarageDoorOpener', 'cover.garage_door', 'open', + {ATTR_DEVICE_CLASS: 'garage', + ATTR_SUPPORTED_FEATURES: SUPPORT_OPEN | SUPPORT_CLOSE}), + ('WindowCovering', 'cover.set_position', 'open', + {ATTR_SUPPORTED_FEATURES: 4}), + ('WindowCoveringBasic', 'cover.open_window', 'open', + {ATTR_SUPPORTED_FEATURES: 3}), +]) +def test_type_covers(type_name, entity_id, state, attrs): + """Test if cover types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, None) + assert mock_type.called - def test_sensor_humidity(self): - """Test humidity sensor with device class humidity.""" - with patch.dict(TYPES, {'HumiditySensor': self.mock_type}): - state = State('sensor.humidity', '20', - {ATTR_DEVICE_CLASS: 'humidity', - ATTR_UNIT_OF_MEASUREMENT: '%'}) - get_accessory(None, state, 2, {}) - def test_air_quality_sensor(self): - """Test air quality sensor with pm25 class.""" - with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): - state = State('sensor.air_quality', '40', - {ATTR_DEVICE_CLASS: 'pm25'}) - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('BinarySensor', 'binary_sensor.opening', 'on', + {ATTR_DEVICE_CLASS: 'opening'}), + ('BinarySensor', 'device_tracker.someone', 'not_home', {}), - def test_air_quality_sensor_entity_id(self): - """Test air quality sensor with entity_id contains pm25.""" - with patch.dict(TYPES, {'AirQualitySensor': self.mock_type}): - state = State('sensor.air_quality_pm25', '40', {}) - get_accessory(None, state, 2, {}) + ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), + ('AirQualitySensor', 'sensor.air_quality', '40', + {ATTR_DEVICE_CLASS: 'pm25'}), - def test_co2_sensor(self): - """Test co2 sensor with device class co2.""" - with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): - state = State('sensor.airmeter', '500', - {ATTR_DEVICE_CLASS: 'co2'}) - get_accessory(None, state, 2, {}) + ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), + ('CarbonDioxideSensor', 'sensor.airmeter', '500', + {ATTR_DEVICE_CLASS: 'co2'}), - def test_co2_sensor_entity_id(self): - """Test co2 sensor with entity_id contains co2.""" - with patch.dict(TYPES, {'CarbonDioxideSensor': self.mock_type}): - state = State('sensor.airmeter_co2', '500', {}) - get_accessory(None, state, 2, {}) + ('HumiditySensor', 'sensor.humidity', '20', + {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), - def test_light_sensor(self): - """Test light sensor with device class illuminance.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_DEVICE_CLASS: 'illuminance'}) - get_accessory(None, state, 2, {}) + ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), + ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), + ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), - def test_light_sensor_unit_lm(self): - """Test light sensor with lm as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lm'}) - get_accessory(None, state, 2, {}) + ('TemperatureSensor', 'sensor.temperature', '23', + {ATTR_DEVICE_CLASS: 'temperature'}), + ('TemperatureSensor', 'sensor.temperature', '23', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}), + ('TemperatureSensor', 'sensor.temperature', '74', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}), +]) +def test_type_sensors(type_name, entity_id, state, attrs): + """Test if sensor types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, None) + assert mock_type.called - def test_light_sensor_unit_lx(self): - """Test light sensor with lx as unit.""" - with patch.dict(TYPES, {'LightSensor': self.mock_type}): - state = State('sensor.light', '900', - {ATTR_UNIT_OF_MEASUREMENT: 'lx'}) - get_accessory(None, state, 2, {}) - def test_binary_sensor(self): - """Test binary sensor with opening class.""" - with patch.dict(TYPES, {'BinarySensor': self.mock_type}): - state = State('binary_sensor.opening', 'on', - {ATTR_DEVICE_CLASS: 'opening'}) - get_accessory(None, state, 2, {}) - - def test_device_tracker(self): - """Test binary sensor with opening class.""" - with patch.dict(TYPES, {'BinarySensor': self.mock_type}): - state = State('device_tracker.someone', 'not_home', {}) - get_accessory(None, state, 2, {}) - - def test_garage_door(self): - """Test cover with device_class: 'garage' and required features.""" - with patch.dict(TYPES, {'GarageDoorOpener': self.mock_type}): - state = State('cover.garage_door', 'open', { - ATTR_DEVICE_CLASS: 'garage', - ATTR_SUPPORTED_FEATURES: - SUPPORT_OPEN | SUPPORT_CLOSE}) - get_accessory(None, state, 2, {}) - - def test_cover_set_position(self): - """Test cover with support for set_cover_position.""" - with patch.dict(TYPES, {'WindowCovering': self.mock_type}): - state = State('cover.set_position', 'open', - {ATTR_SUPPORTED_FEATURES: 4}) - get_accessory(None, state, 2, {}) - - def test_cover_open_close(self): - """Test cover with support for open and close.""" - with patch.dict(TYPES, {'WindowCoveringBasic': self.mock_type}): - state = State('cover.open_window', 'open', - {ATTR_SUPPORTED_FEATURES: 3}) - get_accessory(None, state, 2, {}) - - def test_alarm_control_panel(self): - """Test alarm control panel.""" - config = {ATTR_CODE: '1234'} - with patch.dict(TYPES, {'SecuritySystem': self.mock_type}): - state = State('alarm_control_panel.test', 'armed') - get_accessory(None, state, 2, config) - - # pylint: disable=unsubscriptable-object - print(self.mock_type.call_args[1]) - self.assertEqual( - self.mock_type.call_args[1]['config'][ATTR_CODE], '1234') - - def test_climate(self): - """Test climate devices.""" - with patch.dict(TYPES, {'Thermostat': self.mock_type}): - state = State('climate.test', 'auto') - get_accessory(None, state, 2, {}) - - def test_light(self): - """Test light devices.""" - with patch.dict(TYPES, {'Light': self.mock_type}): - state = State('light.test', 'on') - get_accessory(None, state, 2, {}) - - def test_climate_support_auto(self): - """Test climate devices with support for auto mode.""" - with patch.dict(TYPES, {'Thermostat': self.mock_type}): - state = State('climate.test', 'auto', { - ATTR_SUPPORTED_FEATURES: - SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}) - get_accessory(None, state, 2, {}) - - def test_switch(self): - """Test switch.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('switch.test', 'on') - get_accessory(None, state, 2, {}) - - def test_remote(self): - """Test remote.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('remote.test', 'on') - get_accessory(None, state, 2, {}) - - def test_input_boolean(self): - """Test input_boolean.""" - with patch.dict(TYPES, {'Switch': self.mock_type}): - state = State('input_boolean.test', 'on') - get_accessory(None, state, 2, {}) - - def test_lock(self): - """Test lock.""" - with patch.dict(TYPES, {'Lock': self.mock_type}): - state = State('lock.test', 'locked') - get_accessory(None, state, 2, {}) +@pytest.mark.parametrize('type_name, entity_id, state, attrs', [ + ('Switch', 'switch.test', 'on', {}), + ('Switch', 'remote.test', 'on', {}), + ('Switch', 'input_boolean.test', 'on', {}), +]) +def test_type_switches(type_name, entity_id, state, attrs): + """Test if switch types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, entity_state, 2, None) + assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 082953038b5..23f117b15a0 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,6 +1,7 @@ """Tests for the HomeKit component.""" -import unittest -from unittest.mock import call, patch, ANY, Mock +from unittest.mock import patch, ANY, Mock + +import pytest from homeassistant import setup from homeassistant.core import State @@ -16,208 +17,193 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.common import get_test_home_assistant from tests.components.homekit.test_accessories import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -class TestHomeKit(unittest.TestCase): - """Test setup of HomeKit component and HomeKit class.""" +@pytest.fixture('module') +def debounce_patcher(request): + """Patch debounce method.""" + patcher = patch_debounce() + patcher.start() + request.addfinalizer(patcher.stop) - @classmethod - def setUpClass(cls): - """Setup debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +def test_generate_aid(): + """Test generate aid method.""" + aid = generate_aid('demo.entity') + assert isinstance(aid, int) + assert aid >= 2 and aid <= 18446744073709551615 - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: + mock_adler32.side_effect = [0, 1] + assert generate_aid('demo.entity') is None - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_generate_aid(self): - """Test generate aid method.""" - aid = generate_aid('demo.entity') - self.assertIsInstance(aid, int) - self.assertTrue(aid >= 2 and aid <= 18446744073709551615) +async def test_setup_min(hass): + """Test async_setup with min config options.""" + with patch(PATH_HOMEKIT + '.HomeKit') as mock_homekit: + assert await setup.async_setup_component( + hass, DOMAIN, {DOMAIN: {}}) - with patch(PATH_HOMEKIT + '.adler32') as mock_adler32: - mock_adler32.side_effect = [0, 1] - self.assertIsNone(generate_aid('demo.entity')) + mock_homekit.assert_any_call(hass, DEFAULT_PORT, None, ANY, {}) + assert mock_homekit().setup.called is True - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_min(self, mock_homekit): - """Test async_setup with min config options.""" - self.assertTrue(setup.setup_component( - self.hass, DOMAIN, {DOMAIN: {}})) + # Test auto start enabled + mock_homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() - self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, DEFAULT_PORT, None, ANY, {}), - call().setup()]) + mock_homekit().start.assert_called_with(ANY) - # Test auto start enabled - mock_homekit.reset_mock() - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() - self.assertEqual(mock_homekit.mock_calls, [call().start(ANY)]) +async def test_setup_auto_start_disabled(hass): + """Test async_setup with auto start disabled and test service calls.""" + config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, + CONF_IP_ADDRESS: '172.0.0.0'}} - @patch(PATH_HOMEKIT + '.HomeKit') - def test_setup_auto_start_disabled(self, mock_homekit): - """Test async_setup with auto start disabled and test service calls.""" + with patch(PATH_HOMEKIT + '.HomeKit') as mock_homekit: mock_homekit.return_value = homekit = Mock() + assert await setup.async_setup_component( + hass, DOMAIN, config) - config = {DOMAIN: {CONF_AUTO_START: False, CONF_PORT: 11111, - CONF_IP_ADDRESS: '172.0.0.0'}} - self.assertTrue(setup.setup_component( - self.hass, DOMAIN, config)) - self.hass.block_till_done() + mock_homekit.assert_any_call(hass, 11111, '172.0.0.0', ANY, {}) + assert mock_homekit().setup.called is True - self.assertEqual(mock_homekit.mock_calls, [ - call(self.hass, 11111, '172.0.0.0', ANY, {}), - call().setup()]) + # Test auto_start disabled + homekit.reset_mock() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert homekit.start.called is False - # Test auto_start disabled - homekit.reset_mock() - self.hass.bus.fire(EVENT_HOMEASSISTANT_START) - self.hass.block_till_done() - self.assertEqual(homekit.mock_calls, []) + # Test start call with driver is ready + homekit.reset_mock() + homekit.status = STATUS_READY - # Test start call with driver is ready - homekit.reset_mock() - homekit.status = STATUS_READY + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + assert homekit.start.called is True - self.hass.services.call('homekit', 'start') - self.assertEqual(homekit.mock_calls, [call.start()]) + # Test start call with driver started + homekit.reset_mock() + homekit.status = STATUS_STOPPED - # Test start call with driver started - homekit.reset_mock() - homekit.status = STATUS_STOPPED + await hass.services.async_call( + DOMAIN, SERVICE_HOMEKIT_START, blocking=True) + assert homekit.start.called is False - self.hass.services.call(DOMAIN, SERVICE_HOMEKIT_START) - self.assertEqual(homekit.mock_calls, []) - def test_homekit_setup(self): - """Test setup of bridge and driver.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, None, {}, {}) +async def test_homekit_setup(hass): + """Test setup of bridge and driver.""" + homekit = HomeKit(hass, DEFAULT_PORT, None, {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ - patch('homeassistant.util.get_local_ip') as mock_ip: - mock_ip.return_value = IP_ADDRESS - homekit.setup() + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver, \ + patch('homeassistant.util.get_local_ip') as mock_ip: + mock_ip.return_value = IP_ADDRESS + await hass.async_add_job(homekit.setup) - path = self.hass.config.path(HOMEKIT_FILE) - self.assertTrue(isinstance(homekit.bridge, HomeBridge)) - self.assertEqual(mock_driver.mock_calls, [ - call(homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path)]) + path = hass.config.path(HOMEKIT_FILE) + assert isinstance(homekit.bridge, HomeBridge) + mock_driver.assert_called_with( + homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path) - # Test if stop listener is setup - self.assertEqual( - self.hass.bus.listeners.get(EVENT_HOMEASSISTANT_STOP), 1) + # Test if stop listener is setup + assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 - def test_homekit_setup_ip_address(self): - """Test setup with given IP address.""" - homekit = HomeKit(self.hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: - homekit.setup() - mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) +async def test_homekit_setup_ip_address(hass): + """Test setup with given IP address.""" + homekit = HomeKit(hass, DEFAULT_PORT, '172.0.0.0', {}, {}) - def test_homekit_add_accessory(self): - """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(self.hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(self.hass) + with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: + await hass.async_add_job(homekit.setup) + mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - mock_get_acc.side_effect = [None, 'acc', None] - homekit.add_bridge_accessory(State('light.demo', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 363398124, {})) - self.assertFalse(mock_add_acc.called) - homekit.add_bridge_accessory(State('demo.test', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 294192020, {})) - self.assertTrue(mock_add_acc.called) - homekit.add_bridge_accessory(State('demo.test_2', 'on')) - self.assertEqual(mock_get_acc.call_args, - call(self.hass, ANY, 429982757, {})) - self.assertEqual(mock_add_acc.mock_calls, [call('acc')]) - def test_homekit_entity_filter(self): - """Test the entity filter.""" - entity_filter = generate_filter(['cover'], ['demo.test'], [], []) - homekit = HomeKit(self.hass, None, None, entity_filter, {}) +async def test_homekit_add_accessory(hass): + """Add accessory if config exists and get_acc returns an accessory.""" + homekit = HomeKit(hass, None, None, lambda entity_id: True, {}) + homekit.bridge = HomeBridge(hass) - with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - mock_get_acc.return_value = None + with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ + as mock_add_acc, \ + patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: - homekit.add_bridge_accessory(State('cover.test', 'open')) - self.assertTrue(mock_get_acc.called) - mock_get_acc.reset_mock() + mock_get_acc.side_effect = [None, 'acc', None] + homekit.add_bridge_accessory(State('light.demo', 'on')) + mock_get_acc.assert_called_with(hass, ANY, 363398124, {}) + assert mock_add_acc.called is False - homekit.add_bridge_accessory(State('demo.test', 'on')) - self.assertTrue(mock_get_acc.called) - mock_get_acc.reset_mock() + homekit.add_bridge_accessory(State('demo.test', 'on')) + mock_get_acc.assert_called_with(hass, ANY, 294192020, {}) + assert mock_add_acc.called is True - homekit.add_bridge_accessory(State('light.demo', 'light')) - self.assertFalse(mock_get_acc.called) + homekit.add_bridge_accessory(State('demo.test_2', 'on')) + mock_get_acc.assert_called_with(hass, ANY, 429982757, {}) + mock_add_acc.assert_called_with('acc') - @patch(PATH_HOMEKIT + '.show_setup_message') - @patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') - def test_homekit_start(self, mock_add_bridge_acc, mock_show_setup_msg): - """Test HomeKit start method.""" - homekit = HomeKit(self.hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(self.hass) - homekit.driver = Mock() - self.hass.states.set('light.demo', 'on') - state = self.hass.states.all()[0] +async def test_homekit_entity_filter(hass): + """Test the entity filter.""" + entity_filter = generate_filter(['cover'], ['demo.test'], [], []) + homekit = HomeKit(hass, None, None, entity_filter, {}) - homekit.start() - self.hass.block_till_done() + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + mock_get_acc.return_value = None - self.assertEqual(mock_add_bridge_acc.mock_calls, [call(state)]) - self.assertEqual(mock_show_setup_msg.mock_calls, [ - call(self.hass, homekit.bridge)]) - self.assertEqual(homekit.driver.mock_calls, [call.start()]) - self.assertEqual(homekit.status, STATUS_RUNNING) + homekit.add_bridge_accessory(State('cover.test', 'open')) + assert mock_get_acc.called is True + mock_get_acc.reset_mock() - # Test start() if already started - homekit.driver.reset_mock() - homekit.start() - self.hass.block_till_done() - self.assertEqual(homekit.driver.mock_calls, []) + homekit.add_bridge_accessory(State('demo.test', 'on')) + assert mock_get_acc.called is True + mock_get_acc.reset_mock() - def test_homekit_stop(self): - """Test HomeKit stop method.""" - homekit = HomeKit(self.hass, None, None, None, None) - homekit.driver = Mock() + homekit.add_bridge_accessory(State('light.demo', 'light')) + assert mock_get_acc.called is False - self.assertEqual(homekit.status, STATUS_READY) - homekit.stop() - self.hass.block_till_done() - homekit.status = STATUS_WAIT - homekit.stop() - self.hass.block_till_done() - homekit.status = STATUS_STOPPED - homekit.stop() - self.hass.block_till_done() - self.assertFalse(homekit.driver.stop.called) - # Test if driver is started - homekit.status = STATUS_RUNNING - homekit.stop() - self.hass.block_till_done() - self.assertTrue(homekit.driver.stop.called) +async def test_homekit_start(hass, debounce_patcher): + """Test HomeKit start method.""" + homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) + homekit.bridge = HomeBridge(hass) + homekit.driver = Mock() + + hass.states.async_set('light.demo', 'on') + state = hass.states.async_all()[0] + + with patch(PATH_HOMEKIT + '.HomeKit.add_bridge_accessory') as \ + mock_add_acc, \ + patch(PATH_HOMEKIT + '.show_setup_message') as mock_setup_msg: + await hass.async_add_job(homekit.start) + + mock_add_acc.assert_called_with(state) + mock_setup_msg.assert_called_with(hass, homekit.bridge) + assert homekit.driver.start.called is True + assert homekit.status == STATUS_RUNNING + + # Test start() if already started + homekit.driver.reset_mock() + await hass.async_add_job(homekit.start) + assert homekit.driver.start.called is False + + +async def test_homekit_stop(hass): + """Test HomeKit stop method.""" + homekit = HomeKit(hass, None, None, None, None) + homekit.driver = Mock() + + assert homekit.status == STATUS_READY + await hass.async_add_job(homekit.stop) + homekit.status = STATUS_WAIT + await hass.async_add_job(homekit.stop) + homekit.status = STATUS_STOPPED + await hass.async_add_job(homekit.stop) + assert homekit.driver.stop.called is False + + # Test if driver is started + homekit.status = STATUS_RUNNING + await hass.async_add_job(homekit.stop) + assert homekit.driver.stop.called is True diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 313d58e78fd..b833e1a03c9 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -1,265 +1,231 @@ """Test different accessory types: Covers.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.cover import ( - ATTR_POSITION, ATTR_CURRENT_POSITION, SUPPORT_STOP) + DOMAIN, ATTR_CURRENT_POSITION, ATTR_POSITION, SUPPORT_STOP) from homeassistant.const import ( - STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_OPEN, - ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitCovers(unittest.TestCase): - """Test class for all accessory types regarding covers.""" +@pytest.fixture(scope='module') +def cls(request): + """Patch debounce decorator during import of type_covers.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_covers', + fromlist=['GarageDoorOpener', 'WindowCovering,', + 'WindowCoveringBasic']) + request.addfinalizer(patcher.stop) + patcher_tuple = namedtuple('Cls', ['window', 'window_basic', 'garage']) + return patcher_tuple(window=_import.WindowCovering, + window_basic=_import.WindowCoveringBasic, + garage=_import.GarageDoorOpener) - @classmethod - def setUpClass(cls): - """Setup Light class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__('homeassistant.components.homekit.type_covers', - fromlist=['GarageDoorOpener', 'WindowCovering,', - 'WindowCoveringBasic']) - cls.garage_cls = _import.GarageDoorOpener - cls.window_cls = _import.WindowCovering - cls.window_basic_cls = _import.WindowCoveringBasic - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_garage_door_open_close(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.garage_door' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = cls.garage(hass, 'Garage Door', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 4 # GarageDoorOpener - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 1 - def test_garage_door_open_close(self): - """Test if accessory and HA are updated accordingly.""" - garage_door = 'cover.garage_door' + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - acc = self.garage_cls(self.hass, 'Cover', garage_door, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 4) # GarageDoorOpener + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') - self.hass.states.set(garage_door, STATE_CLOSED) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_state.value == 2 + assert acc.char_target_state.value == 1 - self.assertEqual(acc.char_current_state.value, 1) - self.assertEqual(acc.char_target_state.value, 1) + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() - self.hass.states.set(garage_door, STATE_OPEN) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_open_cover + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) - self.hass.states.set(garage_door, STATE_UNAVAILABLE) - self.hass.block_till_done() +async def test_window_set_cover_position(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + acc = cls.window(hass, 'Cover', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.hass.states.set(garage_door, STATE_UNKNOWN) - self.hass.block_till_done() + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 - # Set closed from HomeKit - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_CURRENT_POSITION: None}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 - self.assertEqual(acc.char_current_state.value, 2) - self.assertEqual(acc.char_target_state.value, 1) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') + hass.states.async_set(entity_id, STATE_OPEN, + {ATTR_CURRENT_POSITION: 50}) + await hass.async_block_till_done() + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 - self.hass.states.set(garage_door, STATE_CLOSED) - self.hass.block_till_done() + # Set from HomeKit + call_set_cover_position = async_mock_service(hass, DOMAIN, + 'set_cover_position') - # Set open from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_set_cover_position[0] + assert call_set_cover_position[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_cover_position[0].data[ATTR_POSITION] == 25 + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 25 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 0) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') + await hass.async_add_job(acc.char_target_position.client_update_value, 75) + await hass.async_block_till_done() + assert call_set_cover_position[1] + assert call_set_cover_position[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_cover_position[1].data[ATTR_POSITION] == 75 + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 75 - def test_window_set_cover_position(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - acc = self.window_cls(self.hass, 'Cover', window_cover, 2, config=None) - acc.run() +async def test_window_open_close(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 14) # WindowCovering + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: 0}) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) + assert acc.aid == 2 + assert acc.category == 14 # WindowCovering - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_CURRENT_POSITION: None}) - self.hass.block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.hass.states.set(window_cover, STATE_OPEN, - {ATTR_CURRENT_POSITION: 50}) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_OPEN) + await hass.async_block_till_done() + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 50) + hass.states.async_set(entity_id, STATE_CLOSED) + await hass.async_block_till_done() + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_cover_position') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_POSITION], 25) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 25) + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - # Set from HomeKit - acc.char_target_position.client_update_value(75) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_cover_position') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_POSITION], 75) + await hass.async_add_job(acc.char_target_position.client_update_value, 90) + await hass.async_block_till_done() + assert call_open_cover[0] + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 75) + await hass.async_add_job(acc.char_target_position.client_update_value, 55) + await hass.async_block_till_done() + assert call_open_cover[1] + assert call_open_cover[1].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - def test_window_open_close(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: 0}) - acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, - config=None) - acc.run() +async def test_window_open_close_stop(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'cover.window' - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 14) # WindowCovering + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) + # Set from HomeKit + call_close_cover = async_mock_service(hass, DOMAIN, 'close_cover') + call_open_cover = async_mock_service(hass, DOMAIN, 'open_cover') + call_stop_cover = async_mock_service(hass, DOMAIN, 'stop_cover') - self.hass.states.set(window_cover, STATE_UNKNOWN) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_position.client_update_value, 25) + await hass.async_block_till_done() + assert call_close_cover + assert call_close_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 0 + assert acc.char_target_position.value == 0 + assert acc.char_position_state.value == 2 - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) + await hass.async_add_job(acc.char_target_position.client_update_value, 90) + await hass.async_block_till_done() + assert call_open_cover + assert call_open_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 100 + assert acc.char_target_position.value == 100 + assert acc.char_position_state.value == 2 - self.hass.states.set(window_cover, STATE_OPEN) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - self.hass.states.set(window_cover, STATE_CLOSED) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(90) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(55) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - def test_window_open_close_stop(self): - """Test if accessory and HA are updated accordingly.""" - window_cover = 'cover.window' - - self.hass.states.set(window_cover, STATE_UNKNOWN, - {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = self.window_basic_cls(self.hass, 'Cover', window_cover, 2, - config=None) - acc.run() - - # Set from HomeKit - acc.char_target_position.client_update_value(25) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'close_cover') - - self.assertEqual(acc.char_current_position.value, 0) - self.assertEqual(acc.char_target_position.value, 0) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(90) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'open_cover') - - self.assertEqual(acc.char_current_position.value, 100) - self.assertEqual(acc.char_target_position.value, 100) - self.assertEqual(acc.char_position_state.value, 2) - - # Set from HomeKit - acc.char_target_position.client_update_value(55) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'stop_cover') - - self.assertEqual(acc.char_current_position.value, 50) - self.assertEqual(acc.char_target_position.value, 50) - self.assertEqual(acc.char_position_state.value, 2) + await hass.async_add_job(acc.char_target_position.client_update_value, 55) + await hass.async_block_till_done() + assert call_stop_cover + assert call_stop_cover[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_current_position.value == 50 + assert acc.char_target_position.value == 50 + assert acc.char_position_state.value == 2 diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 10bf469c08d..b4965fc5ab8 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,188 +1,174 @@ """Test different accessory types: Lights.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.light import ( DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, - ATTR_SUPPORTED_FEATURES, EVENT_CALL_SERVICE, SERVICE_TURN_ON, - SERVICE_TURN_OFF, STATE_ON, STATE_OFF, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_ON, STATE_OFF, STATE_UNKNOWN) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitLights(unittest.TestCase): - """Test class for all accessory types regarding lights.""" +@pytest.fixture(scope='module') +def cls(request): + """Patch debounce decorator during import of type_lights.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_lights', + fromlist=['Light']) + request.addfinalizer(patcher.stop) + patcher_tuple = namedtuple('Cls', ['light']) + return patcher_tuple(light=_import.Light) - @classmethod - def setUpClass(cls): - """Setup Light class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__('homeassistant.components.homekit.type_lights', - fromlist=['Light']) - cls.light_cls = _import.Light - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_light_basic(hass, cls): + """Test light with char state.""" + entity_id = 'light.demo' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 5 # Lightbulb + assert acc.char_on.value == 0 - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_on.value == 1 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - def test_light_basic(self): - """Test light with char state.""" - entity_id = 'light.demo' + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - self.hass.states.set(entity_id, STATE_ON, - {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 5) # Lightbulb - self.assertEqual(acc.char_on.value, 0) + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_on.value == 0 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 1) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 0) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, 0) + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() - # Set from HomeKit - acc.char_on.client_update_value(1) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) + await hass.async_add_job(acc.char_on.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - self.hass.states.set(entity_id, STATE_ON) - self.hass.block_till_done() - acc.char_on.client_update_value(0) - self.hass.block_till_done() - self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) +async def test_light_brightness(hass, cls): + """Test light with brightness.""" + entity_id = 'light.demo' - self.hass.states.set(entity_id, STATE_OFF) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) - # Remove entity - self.hass.states.remove(entity_id) - self.hass.block_till_done() + assert acc.char_brightness.value == 0 - def test_light_brightness(self): - """Test light with brightness.""" - entity_id = 'light.demo' + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_brightness.value, 0) + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_brightness.value, 100) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') - self.hass.states.set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) - self.hass.block_till_done() - self.assertEqual(acc.char_brightness.value, 40) + await hass.async_add_job(acc.char_brightness.client_update_value, 20) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 - # Set from HomeKit - acc.char_brightness.client_update_value(20) - acc.char_on.client_update_value(1) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 20}) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_add_job(acc.char_brightness.client_update_value, 40) + await hass.async_block_till_done() + assert call_turn_on[1] + assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 - acc.char_on.client_update_value(1) - acc.char_brightness.client_update_value(40) - self.hass.block_till_done() - self.assertEqual(self.events[1].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[1].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS_PCT: 40}) + await hass.async_add_job(acc.char_on.client_update_value, 1) + await hass.async_add_job(acc.char_brightness.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id - acc.char_on.client_update_value(1) - acc.char_brightness.client_update_value(0) - self.hass.block_till_done() - self.assertEqual(self.events[2].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[2].data[ATTR_SERVICE], SERVICE_TURN_OFF) - def test_light_color_temperature(self): - """Test light with color temperature.""" - entity_id = 'light.demo' +async def test_light_color_temperature(hass, cls): + """Test light with color temperature.""" + entity_id = 'light.demo' - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, - ATTR_COLOR_TEMP: 190}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_color_temperature.value, 153) + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, + ATTR_COLOR_TEMP: 190}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_color_temperature.value, 190) + assert acc.char_color_temperature.value == 153 - # Set from HomeKit - acc.char_color_temperature.client_update_value(250) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 250}) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_color_temperature.value == 190 - def test_light_rgb_color(self): - """Test light with rgb_color.""" - entity_id = 'light.demo' + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') - self.hass.states.set(entity_id, STATE_ON, { - ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, - ATTR_HS_COLOR: (260, 90)}) - self.hass.block_till_done() - acc = self.light_cls(self.hass, 'Light', entity_id, 2, config=None) - self.assertEqual(acc.char_hue.value, 0) - self.assertEqual(acc.char_saturation.value, 75) + await hass.async_add_job( + acc.char_color_temperature.client_update_value, 250) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 - acc.run() - self.hass.block_till_done() - self.assertEqual(acc.char_hue.value, 260) - self.assertEqual(acc.char_saturation.value, 90) - # Set from HomeKit - acc.char_hue.client_update_value(145) - acc.char_saturation.client_update_value(75) - self.hass.block_till_done() - self.assertEqual(self.events[0].data[ATTR_DOMAIN], DOMAIN) - self.assertEqual(self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA], { - ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (145, 75)}) +async def test_light_rgb_color(hass, cls): + """Test light with rgb_color.""" + entity_id = 'light.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, + ATTR_HS_COLOR: (260, 90)}) + await hass.async_block_till_done() + acc = cls.light(hass, 'Light', entity_id, 2, config=None) + + assert acc.char_hue.value == 0 + assert acc.char_saturation.value == 75 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_hue.value == 260 + assert acc.char_saturation.value == 90 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + + await hass.async_add_job(acc.char_hue.client_update_value, 145) + await hass.async_add_job(acc.char_saturation.client_update_value, 75) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index b2053116060..3442c0da6c8 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,77 +1,57 @@ """Test different accessory types: Locks.""" -import unittest - -from homeassistant.core import callback from homeassistant.components.homekit.type_locks import Lock +from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED, - ATTR_SERVICE, EVENT_CALL_SERVICE) + ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding covers.""" +async def test_lock_unlock(hass): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'lock.kitchen_door' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = Lock(hass, 'Lock', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 6 # DoorLock - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 1 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_LOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 1 + assert acc.char_target_state.value == 1 - def test_lock_unlock(self): - """Test if accessory and HA are updated accordingly.""" - kitchen_lock = 'lock.kitchen_door' + hass.states.async_set(entity_id, STATE_UNLOCKED) + await hass.async_block_till_done() + assert acc.char_current_state.value == 0 + assert acc.char_target_state.value == 0 - acc = Lock(self.hass, 'Lock', kitchen_lock, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 6) # DoorLock + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 0 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 1) + # Set from HomeKit + call_lock = async_mock_service(hass, DOMAIN, 'lock') + call_unlock = async_mock_service(hass, DOMAIN, 'unlock') - self.hass.states.set(kitchen_lock, STATE_LOCKED) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_lock + assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_state.value == 1 - self.assertEqual(acc.char_current_state.value, 1) - self.assertEqual(acc.char_target_state.value, 1) - - self.hass.states.set(kitchen_lock, STATE_UNLOCKED) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_state.value, 0) - self.assertEqual(acc.char_target_state.value, 0) - - self.hass.states.set(kitchen_lock, STATE_UNKNOWN) - self.hass.block_till_done() - - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 0) - - # Set from HomeKit - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'lock') - self.assertEqual(acc.char_target_state.value, 1) - - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'unlock') - self.assertEqual(acc.char_target_state.value, 0) - - self.hass.states.remove(kitchen_lock) - self.hass.block_till_done() + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_unlock + assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_state.value == 0 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index baa461af772..8c3d9474f26 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,134 +1,110 @@ """Test different accessory types: Security Systems.""" -import unittest +import pytest -from homeassistant.core import callback +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.homekit.type_security_systems import ( SecuritySystem) from homeassistant.const import ( - ATTR_CODE, ATTR_SERVICE, ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, + ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - STATE_UNKNOWN) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSecuritySystems(unittest.TestCase): - """Test class for all accessory types regarding security systems.""" +async def test_switch_set_state(hass): + """Test if accessory and HA are updated accordingly.""" + code = '1234' + config = {ATTR_CODE: code} + entity_id = 'alarm_control_panel.test' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 11 # AlarmSystem - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_state.value == 3 + assert acc.char_target_state.value == 3 - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_ALARM_ARMED_AWAY) + await hass.async_block_till_done() + assert acc.char_target_state.value == 1 + assert acc.char_current_state.value == 1 - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - acp = 'alarm_control_panel.test' + hass.states.async_set(entity_id, STATE_ALARM_ARMED_HOME) + await hass.async_block_till_done() + assert acc.char_target_state.value == 0 + assert acc.char_current_state.value == 0 - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={ATTR_CODE: '1234'}) - acc.run() + hass.states.async_set(entity_id, STATE_ALARM_ARMED_NIGHT) + await hass.async_block_till_done() + assert acc.char_target_state.value == 2 + assert acc.char_current_state.value == 2 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 11) # AlarmSystem + hass.states.async_set(entity_id, STATE_ALARM_DISARMED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 3 - self.assertEqual(acc.char_current_state.value, 3) - self.assertEqual(acc.char_target_state.value, 3) + hass.states.async_set(entity_id, STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 4 - self.hass.states.set(acp, STATE_ALARM_ARMED_AWAY) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 1) - self.assertEqual(acc.char_current_state.value, 1) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_target_state.value == 3 + assert acc.char_current_state.value == 4 - self.hass.states.set(acp, STATE_ALARM_ARMED_HOME) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 0) - self.assertEqual(acc.char_current_state.value, 0) + # Set from HomeKit + call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') + call_arm_away = async_mock_service(hass, DOMAIN, 'alarm_arm_away') + call_arm_night = async_mock_service(hass, DOMAIN, 'alarm_arm_night') + call_disarm = async_mock_service(hass, DOMAIN, 'alarm_disarm') - self.hass.states.set(acp, STATE_ALARM_ARMED_NIGHT) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 2) - self.assertEqual(acc.char_current_state.value, 2) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_arm_home + assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_home[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 0 - self.hass.states.set(acp, STATE_ALARM_DISARMED) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 3) + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_arm_away + assert call_arm_away[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_away[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 1 - self.hass.states.set(acp, STATE_ALARM_TRIGGERED) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 4) + await hass.async_add_job(acc.char_target_state.client_update_value, 2) + await hass.async_block_till_done() + assert call_arm_night + assert call_arm_night[0].data[ATTR_ENTITY_ID] == entity_id + assert call_arm_night[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 2 - self.hass.states.set(acp, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_target_state.value, 3) - self.assertEqual(acc.char_current_state.value, 4) + await hass.async_add_job(acc.char_target_state.client_update_value, 3) + await hass.async_block_till_done() + assert call_disarm + assert call_disarm[0].data[ATTR_ENTITY_ID] == entity_id + assert call_disarm[0].data[ATTR_CODE] == code + assert acc.char_target_state.value == 3 - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 0) - acc.char_target_state.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'alarm_arm_away') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 1) +@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) +async def test_no_alarm_code(hass, config): + """Test accessory if security_system doesn't require a alarm_code.""" + entity_id = 'alarm_control_panel.test' - acc.char_target_state.client_update_value(2) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'alarm_arm_night') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 2) + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) - acc.char_target_state.client_update_value(3) - self.hass.block_till_done() - self.assertEqual( - self.events[3].data[ATTR_SERVICE], 'alarm_disarm') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_CODE], '1234') - self.assertEqual(acc.char_target_state.value, 3) + # Set from HomeKit + call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') - def test_no_alarm_code(self): - """Test accessory if security_system doesn't require a alarm_code.""" - acp = 'alarm_control_panel.test' - - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={ATTR_CODE: None}) - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) - self.assertEqual(acc.char_target_state.value, 0) - - acc = SecuritySystem(self.hass, 'SecuritySystem', acp, - 2, config={}) - # Set from HomeKit - acc.char_target_state.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'alarm_arm_home') - self.assertNotIn(ATTR_CODE, self.events[0].data[ATTR_SERVICE_DATA]) - self.assertEqual(acc.char_target_state.value, 0) + await hass.async_add_job(acc.char_target_state.client_update_value, 0) + await hass.async_block_till_done() + assert call_arm_home + assert call_arm_home[0].data[ATTR_ENTITY_ID] == entity_id + assert ATTR_CODE not in call_arm_home[0].data + assert acc.char_target_state.value == 0 diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 77bfc0c8901..39f48abd60e 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,6 +1,4 @@ """Test different accessory types: Sensors.""" -import unittest - from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, @@ -9,201 +7,191 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant + +async def test_temperature(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.temperature' + + acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, config=None) + await hass.async_add_job(acc.run) + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_temp.value == 0.0 + for key, value in PROP_CELSIUS.items(): + assert acc.char_temp.properties[key] == value + + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 0.0 + + hass.states.async_set(entity_id, '20', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_temp.value == 20 + + hass.states.async_set(entity_id, '75.2', + {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + await hass.async_block_till_done() + assert acc.char_temp.value == 24 -class TestHomekitSensors(unittest.TestCase): - """Test class for all accessory types regarding sensors.""" +async def test_humidity(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.humidity' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() + acc = HumiditySensor(hass, 'Humidity', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + assert acc.aid == 2 + assert acc.category == 10 # Sensor - def test_temperature(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.temperature' + assert acc.char_humidity.value == 0 - acc = TemperatureSensor(self.hass, 'Temperature', entity_id, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_humidity.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, '20') + await hass.async_block_till_done() + assert acc.char_humidity.value == 20 - self.assertEqual(acc.char_temp.value, 0.0) - for key, value in PROP_CELSIUS.items(): - self.assertEqual(acc.char_temp.properties[key], value) - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 0.0) +async def test_air_quality(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.air_quality' - self.hass.states.set(entity_id, '20', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 20) + acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.hass.states.set(entity_id, '75.2', - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_temp.value, 24) + assert acc.aid == 2 + assert acc.category == 10 # Sensor - def test_humidity(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.humidity' + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 - acc = HumiditySensor(self.hass, 'Humidity', entity_id, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_set(entity_id, '34') + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 1 - self.assertEqual(acc.char_humidity.value, 0) + hass.states.async_set(entity_id, '200') + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 5 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_humidity.value, 0) - self.hass.states.set(entity_id, '20') - self.hass.block_till_done() - self.assertEqual(acc.char_humidity.value, 20) +async def test_co2(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.co2' - def test_air_quality(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.air_quality' + acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - acc = AirQualitySensor(self.hass, 'Air Quality', entity_id, - 2, config=None) - acc.run() + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + assert acc.char_co2.value == 0 + assert acc.char_peak.value == 0 + assert acc.char_detected.value == 0 - self.assertEqual(acc.char_density.value, 0) - self.assertEqual(acc.char_quality.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_co2.value == 0 + assert acc.char_peak.value == 0 + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 0) - self.assertEqual(acc.char_quality.value, 0) + hass.states.async_set(entity_id, '1100') + await hass.async_block_till_done() + assert acc.char_co2.value == 1100 + assert acc.char_peak.value == 1100 + assert acc.char_detected.value == 1 - self.hass.states.set(entity_id, '34') - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 34) - self.assertEqual(acc.char_quality.value, 1) + hass.states.async_set(entity_id, '800') + await hass.async_block_till_done() + assert acc.char_co2.value == 800 + assert acc.char_peak.value == 1100 + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, '200') - self.hass.block_till_done() - self.assertEqual(acc.char_density.value, 200) - self.assertEqual(acc.char_quality.value, 5) - def test_co2(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.co2' +async def test_light(hass): + """Test if accessory is updated after state change.""" + entity_id = 'sensor.light' - acc = CarbonDioxideSensor(self.hass, 'CO2', entity_id, 2, config=None) - acc.run() + acc = LightSensor(hass, 'Light', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.assertEqual(acc.char_co2.value, 0) - self.assertEqual(acc.char_peak.value, 0) - self.assertEqual(acc.char_detected.value, 0) + assert acc.char_light.value == 0.0001 - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 0) - self.assertEqual(acc.char_peak.value, 0) - self.assertEqual(acc.char_detected.value, 0) + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_light.value == 0.0001 - self.hass.states.set(entity_id, '1100') - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 1100) - self.assertEqual(acc.char_peak.value, 1100) - self.assertEqual(acc.char_detected.value, 1) + hass.states.async_set(entity_id, '300') + await hass.async_block_till_done() + assert acc.char_light.value == 300 - self.hass.states.set(entity_id, '800') - self.hass.block_till_done() - self.assertEqual(acc.char_co2.value, 800) - self.assertEqual(acc.char_peak.value, 1100) - self.assertEqual(acc.char_detected.value, 0) - def test_light(self): - """Test if accessory is updated after state change.""" - entity_id = 'sensor.light' +async def test_binary(hass): + """Test if accessory is updated after state change.""" + entity_id = 'binary_sensor.opening' - acc = LightSensor(self.hass, 'Light', entity_id, 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_UNKNOWN, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + acc = BinarySensor(hass, 'Window Opening', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - self.assertEqual(acc.char_light.value, 0.0001) + assert acc.aid == 2 + assert acc.category == 10 # Sensor - self.hass.states.set(entity_id, STATE_UNKNOWN) - self.hass.block_till_done() - self.assertEqual(acc.char_light.value, 0.0001) + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, '300') - self.hass.block_till_done() - self.assertEqual(acc.char_light.value, 300) + hass.states.async_set(entity_id, STATE_ON, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 1 - def test_binary(self): - """Test if accessory is updated after state change.""" - entity_id = 'binary_sensor.opening' + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.hass.states.set(entity_id, STATE_UNKNOWN, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() + hass.states.async_set(entity_id, STATE_HOME, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 1 - acc = BinarySensor(self.hass, 'Window Opening', entity_id, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_NOT_HOME, + {ATTR_DEVICE_CLASS: 'opening'}) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 10) # Sensor + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_detected.value == 0 - self.assertEqual(acc.char_detected.value, 0) - self.hass.states.set(entity_id, STATE_ON, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 1) +async def test_binary_device_classes(hass): + """Test if services and characteristics are assigned correctly.""" + entity_id = 'binary_sensor.demo' - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 0) + for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_DEVICE_CLASS: device_class}) + await hass.async_block_till_done() - self.hass.states.set(entity_id, STATE_HOME, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 1) - - self.hass.states.set(entity_id, STATE_NOT_HOME, - {ATTR_DEVICE_CLASS: "opening"}) - self.hass.block_till_done() - self.assertEqual(acc.char_detected.value, 0) - - self.hass.states.remove(entity_id) - self.hass.block_till_done() - - def test_binary_device_classes(self): - """Test if services and characteristics are assigned correctly.""" - entity_id = 'binary_sensor.demo' - - for device_class, (service, char) in BINARY_SENSOR_SERVICE_MAP.items(): - self.hass.states.set(entity_id, STATE_OFF, - {ATTR_DEVICE_CLASS: device_class}) - self.hass.block_till_done() - - acc = BinarySensor(self.hass, 'Binary Sensor', entity_id, - 2, config=None) - self.assertEqual(acc.get_service(service).display_name, service) - self.assertEqual(acc.char_detected.display_name, char) + acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, config=None) + assert acc.get_service(service).display_name == service + assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 65b107e24cd..7368179f232 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -1,104 +1,45 @@ """Test different accessory types: Switches.""" -import unittest +import pytest -from homeassistant.core import callback, split_entity_id +from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ( - ATTR_DOMAIN, ATTR_SERVICE, EVENT_CALL_SERVICE, - SERVICE_TURN_ON, SERVICE_TURN_OFF, STATE_ON, STATE_OFF) +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF -from tests.common import get_test_home_assistant +from tests.common import async_mock_service -class TestHomekitSwitches(unittest.TestCase): - """Test class for all accessory types regarding switches.""" +@pytest.mark.parametrize('entity_id', [ + 'switch.test', 'remote.test', 'input_boolean.test']) +async def test_switch_set_state(hass, entity_id): + """Test if accessory and HA are updated accordingly.""" + domain = split_entity_id(entity_id)[0] - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + acc = Switch(hass, 'Switch', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 8 # Switch - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_on.value is False - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value is True - def test_switch_set_state(self): - """Test if accessory and HA are updated accordingly.""" - entity_id = 'switch.test' - domain = split_entity_id(entity_id)[0] + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value is False - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() + # Set from HomeKit + call_turn_on = async_mock_service(hass, domain, 'turn_on') + call_turn_off = async_mock_service(hass, domain, 'turn_off') - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 8) # Switch + await hass.async_add_job(acc.char_on.client_update_value, True) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id - self.assertEqual(acc.char_on.value, False) - - self.hass.states.set(entity_id, STATE_ON) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, True) - - self.hass.states.set(entity_id, STATE_OFF) - self.hass.block_till_done() - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - - acc.char_on.client_update_value(False) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], SERVICE_TURN_OFF) - - def test_remote_set_state(self): - """Test service call for remote as domain.""" - entity_id = 'remote.test' - domain = split_entity_id(entity_id)[0] - - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() - - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual(acc.char_on.value, True) - - def test_input_boolean_set_state(self): - """Test service call for remote as domain.""" - entity_id = 'input_boolean.test' - domain = split_entity_id(entity_id)[0] - - acc = Switch(self.hass, 'Switch', entity_id, 2, config=None) - acc.run() - - self.assertEqual(acc.char_on.value, False) - - # Set from HomeKit - acc.char_on.client_update_value(True) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_DOMAIN], domain) - self.assertEqual( - self.events[0].data[ATTR_SERVICE], SERVICE_TURN_ON) - self.assertEqual(acc.char_on.value, True) + await hass.async_add_job(acc.char_on.client_update_value, False) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index fe2a7f6cd02..eea256c134d 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,364 +1,347 @@ """Test different accessory types: Thermostats.""" -import unittest +from collections import namedtuple + +import pytest -from homeassistant.core import callback from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, + DOMAIN, ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, EVENT_CALL_SERVICE, + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service from tests.components.homekit.test_accessories import patch_debounce -class TestHomekitThermostats(unittest.TestCase): - """Test class for all accessory types regarding thermostats.""" +@pytest.fixture(scope='module') +def cls(request): + """Patch debounce decorator during import of type_thermostats.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_thermostats', + fromlist=['Thermostat']) + request.addfinalizer(patcher.stop) + patcher_tuple = namedtuple('Cls', ['thermostat']) + return patcher_tuple(thermostat=_import.Thermostat) - @classmethod - def setUpClass(cls): - """Setup Thermostat class import and debounce patcher.""" - cls.patcher = patch_debounce() - cls.patcher.start() - _import = __import__( - 'homeassistant.components.homekit.type_thermostats', - fromlist=['Thermostat']) - cls.thermostat_cls = _import.Thermostat - @classmethod - def tearDownClass(cls): - """Stop debounce patcher.""" - cls.patcher.stop() +async def test_default_thermostat(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + assert acc.aid == 2 + assert acc.category == 9 # Thermostat - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 + assert acc.char_current_temp.value == 21.0 + assert acc.char_target_temp.value == 21.0 + assert acc.char_display_units.value == 0 + assert acc.char_cooling_thresh_temp is None + assert acc.char_heating_thresh_temp is None - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - def test_default_thermostat(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 23.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 1 + assert acc.char_current_temp.value == 23.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 20.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 2 + assert acc.char_current_temp.value == 25.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.aid, 2) - self.assertEqual(acc.category, 9) # Thermostat + hass.states.async_set(entity_id, STATE_COOL, + {ATTR_OPERATION_MODE: STATE_COOL, + ATTR_TEMPERATURE: 20.0, + ATTR_CURRENT_TEMPERATURE: 19.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 20.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 2 + assert acc.char_current_temp.value == 19.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) - self.assertEqual(acc.char_current_temp.value, 21.0) - self.assertEqual(acc.char_target_temp.value, 21.0) - self.assertEqual(acc.char_display_units.value, 0) - self.assertEqual(acc.char_cooling_thresh_temp, None) - self.assertEqual(acc.char_heating_thresh_temp, None) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_HEAT, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 1) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_HEAT, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 23.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 1) - self.assertEqual(acc.char_current_temp.value, 23.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 25.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 25.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_COOL, - {ATTR_OPERATION_MODE: STATE_COOL, - ATTR_TEMPERATURE: 20.0, - ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 20.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 2) - self.assertEqual(acc.char_current_temp.value, 25.0) - self.assertEqual(acc.char_display_units.value, 0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], + ATTR_TEMPERATURE: 22.0, + ATTR_CURRENT_TEMPERATURE: 22.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_target_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 22.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_COOL, - {ATTR_OPERATION_MODE: STATE_COOL, - ATTR_TEMPERATURE: 20.0, - ATTR_CURRENT_TEMPERATURE: 19.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 20.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 2) - self.assertEqual(acc.char_current_temp.value, 19.0) - self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') + call_set_operation_mode = async_mock_service(hass, DOMAIN, + 'set_operation_mode') - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_OFF, - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job(acc.char_target_temp.client_update_value, 19.0) + await hass.async_block_till_done() + assert call_set_temperature + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 19.0 + assert acc.char_target_temp.value == 19.0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_block_till_done() + assert call_set_operation_mode + assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT + assert acc.char_target_heat_cool.value == 1 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 25.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 25.0) - self.assertEqual(acc.char_display_units.value, 0) - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_OPERATION_LIST: [STATE_HEAT, STATE_COOL], - ATTR_TEMPERATURE: 22.0, - ATTR_CURRENT_TEMPERATURE: 22.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 22.0) - self.assertEqual(acc.char_display_units.value, 0) +async def test_auto_thermostat(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - # Set from HomeKit - acc.char_target_temp.client_update_value(19.0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_TEMPERATURE], 19.0) - self.assertEqual(acc.char_target_temp.value, 19.0) + # support_auto = True + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - acc.char_target_heat_cool.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_operation_mode') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], - STATE_HEAT) - self.assertEqual(acc.char_target_heat_cool.value, 1) + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 - def test_auto_thermostat(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 22.0, + ATTR_TARGET_TEMP_LOW: 20.0, + ATTR_CURRENT_TEMPERATURE: 18.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 22.0 + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 18.0 + assert acc.char_display_units.value == 0 - # support_auto = True - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 24.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == 2 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 24.0 + assert acc.char_display_units.value == 0 - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 21.0, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 3 + assert acc.char_current_temp.value == 21.0 + assert acc.char_display_units.value == 0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 22.0, - ATTR_TARGET_TEMP_LOW: 20.0, - ATTR_CURRENT_TEMPERATURE: 18.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 22.0) - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 18.0) - self.assertEqual(acc.char_display_units.value, 0) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 23.0, - ATTR_TARGET_TEMP_LOW: 19.0, - ATTR_CURRENT_TEMPERATURE: 24.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_current_heat_cool.value, 2) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 24.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job( + acc.char_heating_thresh_temp.client_update_value, 20.0) + await hass.async_block_till_done() + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 + assert acc.char_heating_thresh_temp.value == 20.0 - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 23.0, - ATTR_TARGET_TEMP_LOW: 19.0, - ATTR_CURRENT_TEMPERATURE: 21.0, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 19.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 23.0) - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 3) - self.assertEqual(acc.char_current_temp.value, 21.0) - self.assertEqual(acc.char_display_units.value, 0) + await hass.async_add_job( + acc.char_cooling_thresh_temp.client_update_value, 25.0) + await hass.async_block_till_done() + assert call_set_temperature[1] + assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 25.0 + assert acc.char_cooling_thresh_temp.value == 25.0 - # Set from HomeKit - acc.char_heating_thresh_temp.client_update_value(20.0) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_LOW], 20.0) - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - acc.char_cooling_thresh_temp.client_update_value(25.0) - self.hass.block_till_done() - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_temperature') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_TARGET_TEMP_HIGH], - 25.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 25.0) +async def test_power_state(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - def test_power_state(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' + # SUPPORT_ON_OFF = True + hass.states.async_set(entity_id, STATE_HEAT, + {ATTR_SUPPORTED_FEATURES: 4096, + ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) + assert acc.support_power_state is True - # SUPPORT_ON_OFF = True - self.hass.states.set(climate, STATE_HEAT, - {ATTR_SUPPORTED_FEATURES: 4096, - ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() - self.assertTrue(acc.support_power_state) + assert acc.char_current_heat_cool.value == 1 + assert acc.char_target_heat_cool.value == 1 - self.assertEqual(acc.char_current_heat_cool.value, 1) - self.assertEqual(acc.char_target_heat_cool.value, 1) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_HEAT, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_HEAT, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_OPERATION_MODE: STATE_OFF, + ATTR_TEMPERATURE: 23.0, + ATTR_CURRENT_TEMPERATURE: 18.0}) + await hass.async_block_till_done() + assert acc.char_current_heat_cool.value == 0 + assert acc.char_target_heat_cool.value == 0 - self.hass.states.set(climate, STATE_OFF, - {ATTR_OPERATION_MODE: STATE_OFF, - ATTR_TEMPERATURE: 23.0, - ATTR_CURRENT_TEMPERATURE: 18.0}) - self.hass.block_till_done() - self.assertEqual(acc.char_current_heat_cool.value, 0) - self.assertEqual(acc.char_target_heat_cool.value, 0) + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, 'turn_on') + call_turn_off = async_mock_service(hass, DOMAIN, 'turn_off') + call_set_operation_mode = async_mock_service(hass, DOMAIN, + 'set_operation_mode') - # Set from HomeKit - acc.char_target_heat_cool.client_update_value(1) - self.hass.block_till_done() - self.assertEqual( - self.events[0].data[ATTR_SERVICE], 'turn_on') - self.assertEqual( - self.events[0].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], - climate) - self.assertEqual( - self.events[1].data[ATTR_SERVICE], 'set_operation_mode') - self.assertEqual( - self.events[1].data[ATTR_SERVICE_DATA][ATTR_OPERATION_MODE], - STATE_HEAT) - self.assertEqual(acc.char_target_heat_cool.value, 1) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode + assert call_set_operation_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_operation_mode[0].data[ATTR_OPERATION_MODE] == STATE_HEAT + assert acc.char_target_heat_cool.value == 1 - acc.char_target_heat_cool.client_update_value(0) - self.hass.block_till_done() - self.assertEqual( - self.events[2].data[ATTR_SERVICE], 'turn_off') - self.assertEqual( - self.events[2].data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID], - climate) - self.assertEqual(acc.char_target_heat_cool.value, 0) + await hass.async_add_job(acc.char_target_heat_cool.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert acc.char_target_heat_cool.value == 0 - def test_thermostat_fahrenheit(self): - """Test if accessory and HA are updated accordingly.""" - climate = 'climate.test' - # support_auto = True - self.hass.states.set(climate, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) - self.hass.block_till_done() - acc = self.thermostat_cls(self.hass, 'Climate', climate, - 2, config=None) - acc.run() +async def test_thermostat_fahrenheit(hass, cls): + """Test if accessory and HA are updated accordingly.""" + entity_id = 'climate.test' - self.hass.states.set(climate, STATE_AUTO, - {ATTR_OPERATION_MODE: STATE_AUTO, - ATTR_TARGET_TEMP_HIGH: 75.2, - ATTR_TARGET_TEMP_LOW: 68, - ATTR_TEMPERATURE: 71.6, - ATTR_CURRENT_TEMPERATURE: 73.4, - ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) - self.hass.block_till_done() - self.assertEqual(acc.char_heating_thresh_temp.value, 20.0) - self.assertEqual(acc.char_cooling_thresh_temp.value, 24.0) - self.assertEqual(acc.char_current_temp.value, 23.0) - self.assertEqual(acc.char_target_temp.value, 22.0) - self.assertEqual(acc.char_display_units.value, 1) + # support_auto = True + hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + await hass.async_add_job(acc.run) - # Set from HomeKit - acc.char_cooling_thresh_temp.client_update_value(23) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) - self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 68) + hass.states.async_set(entity_id, STATE_AUTO, + {ATTR_OPERATION_MODE: STATE_AUTO, + ATTR_TARGET_TEMP_HIGH: 75.2, + ATTR_TARGET_TEMP_LOW: 68, + ATTR_TEMPERATURE: 71.6, + ATTR_CURRENT_TEMPERATURE: 73.4, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 24.0 + assert acc.char_current_temp.value == 23.0 + assert acc.char_target_temp.value == 22.0 + assert acc.char_display_units.value == 1 - acc.char_heating_thresh_temp.client_update_value(22) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TARGET_TEMP_HIGH], 73.4) - self.assertEqual(service_data[ATTR_TARGET_TEMP_LOW], 71.6) + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN, 'set_temperature') - acc.char_target_temp.client_update_value(24.0) - self.hass.block_till_done() - service_data = self.events[-1].data[ATTR_SERVICE_DATA] - self.assertEqual(service_data[ATTR_TEMPERATURE], 75.2) + await hass.async_add_job( + acc.char_cooling_thresh_temp.client_update_value, 23) + await hass.async_block_till_done() + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 + + await hass.async_add_job( + acc.char_heating_thresh_temp.client_update_value, 22) + await hass.async_block_till_done() + assert call_set_temperature[1] + assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6 + + await hass.async_add_job(acc.char_target_temp.client_update_value, 24.0) + await hass.async_block_till_done() + assert call_set_temperature[2] + assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 4a9521384bd..2ec35975618 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -1,25 +1,20 @@ """Test HomeKit util module.""" -import unittest - -import voluptuous as vol import pytest +import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, ATTR_CODE, - density_to_air_quality) + temperature_to_homekit, temperature_to_states, density_to_air_quality) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - SERVICE_CREATE, SERVICE_DISMISS, ATTR_NOTIFICATION_ID) + DOMAIN, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - EVENT_CALL_SERVICE, ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA, - TEMP_CELSIUS, TEMP_FAHRENHEIT, STATE_UNKNOWN) + ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) -from tests.common import get_test_home_assistant +from tests.common import async_mock_service def test_validate_entity_config(): @@ -68,51 +63,27 @@ def test_density_to_air_quality(): assert density_to_air_quality(300) == 5 -class TestUtil(unittest.TestCase): - """Test all HomeKit util methods.""" +async def test_show_setup_msg(hass): + """Test show setup message as persistence notification.""" + bridge = HomeBridge(hass) - def setUp(self): - """Setup things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.events = [] + call_create_notification = async_mock_service(hass, DOMAIN, 'create') - @callback - def record_event(event): - """Track called event.""" - self.events.append(event) + await hass.async_add_job(show_setup_message, hass, bridge) + await hass.async_block_till_done() - self.hass.bus.listen(EVENT_CALL_SERVICE, record_event) + assert call_create_notification + assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == \ + HOMEKIT_NOTIFY_ID - def tearDown(self): - """Stop down everything that was started.""" - self.hass.stop() - def test_show_setup_msg(self): - """Test show setup message as persistence notification.""" - bridge = HomeBridge(self.hass) +async def test_dismiss_setup_msg(hass): + """Test dismiss setup message.""" + call_dismiss_notification = async_mock_service(hass, DOMAIN, 'dismiss') - show_setup_message(self.hass, bridge) - self.hass.block_till_done() + await hass.async_add_job(dismiss_setup_message, hass) + await hass.async_block_till_done() - data = self.events[0].data - self.assertEqual( - data.get(ATTR_DOMAIN, None), 'persistent_notification') - self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_CREATE) - self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) - self.assertEqual( - data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), - HOMEKIT_NOTIFY_ID) - - def test_dismiss_setup_msg(self): - """Test dismiss setup message.""" - dismiss_setup_message(self.hass) - self.hass.block_till_done() - - data = self.events[0].data - self.assertEqual( - data.get(ATTR_DOMAIN, None), 'persistent_notification') - self.assertEqual(data.get(ATTR_SERVICE, None), SERVICE_DISMISS) - self.assertNotEqual(data.get(ATTR_SERVICE_DATA, None), None) - self.assertEqual( - data[ATTR_SERVICE_DATA].get(ATTR_NOTIFICATION_ID, None), - HOMEKIT_NOTIFY_ID) + assert call_dismiss_notification + assert call_dismiss_notification[0].data[ATTR_NOTIFICATION_ID] == \ + HOMEKIT_NOTIFY_ID From ef8fc1f2018412749fdb29d109245fe06bcbfc10 Mon Sep 17 00:00:00 2001 From: damarco Date: Fri, 11 May 2018 07:32:16 +0200 Subject: [PATCH 044/144] Update sensor state before adding device (#14357) --- homeassistant/components/sensor/zha.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 41dab282997..6979690708d 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -25,7 +25,7 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): return sensor = yield from make_sensor(discovery_info) - async_add_devices([sensor]) + async_add_devices([sensor], update_before_add=True) @asyncio.coroutine @@ -61,6 +61,11 @@ class Sensor(zha.Entity): value_attribute = 0 min_reportable_change = 1 + @property + def should_poll(self) -> bool: + """State gets pushed from device.""" + return False + @property def state(self) -> str: """Return the state of the entity.""" @@ -75,6 +80,14 @@ class Sensor(zha.Entity): self._state = value self.async_schedule_update_ha_state() + async def async_update(self): + """Retrieve latest state.""" + result = await zha.safe_read( + list(self._in_clusters.values())[0], + [self.value_attribute] + ) + self._state = result.get(self.value_attribute, self._state) + class TemperatureSensor(Sensor): """ZHA temperature sensor.""" From be3b227a878b8f570bf320b776d861301967b20b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 11 May 2018 09:39:18 +0200 Subject: [PATCH 045/144] Make mysensors component async (#13641) * Make mysensors component async * Use async dispatcher and discovery. * Run I/O in executor. * Make mysensors actuator methods async. * Upgrade pymysensors to 0.13.0. * Use async serial gateway. * Use async TCP gateway. * Use async mqtt gateway. * Start gateway before hass start event * Make sure gateway is started after discovery of persistent devices and after corresponding platforms have been loaded. * Don't wait to start gateway until after hass start. * Bump pymysensors to 0.14.0 --- homeassistant/components/climate/mysensors.py | 12 +- homeassistant/components/cover/mysensors.py | 14 +- homeassistant/components/light/mysensors.py | 16 +-- homeassistant/components/mysensors.py | 120 +++++++++++------- homeassistant/components/notify/mysensors.py | 2 +- homeassistant/components/switch/mysensors.py | 24 ++-- requirements_all.txt | 2 +- 7 files changed, 110 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/climate/mysensors.py b/homeassistant/components/climate/mysensors.py index 2545094ceec..9fab56c61ac 100644 --- a/homeassistant/components/climate/mysensors.py +++ b/homeassistant/components/climate/mysensors.py @@ -115,7 +115,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): """List of available fan modes.""" return ['Auto', 'Min', 'Normal', 'Max'] - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" set_req = self.gateway.const.SetReq temp = kwargs.get(ATTR_TEMPERATURE) @@ -143,9 +143,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[value_type] = value - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -153,9 +153,9 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[set_req.V_HVAC_SPEED] = fan_mode - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode): """Set new target temperature.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, @@ -163,7 +163,7 @@ class MySensorsHVAC(mysensors.MySensorsEntity, ClimateDevice): if self.gateway.optimistic: # Optimistically assume that device has changed state self._values[self.value_type] = operation_mode - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/homeassistant/components/cover/mysensors.py b/homeassistant/components/cover/mysensors.py index 669a7ce6723..3f8eb054710 100644 --- a/homeassistant/components/cover/mysensors.py +++ b/homeassistant/components/cover/mysensors.py @@ -42,7 +42,7 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_DIMMER) - def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Move the cover up.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -53,9 +53,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): self._values[set_req.V_DIMMER] = 100 else: self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Move the cover down.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -66,9 +66,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): self._values[set_req.V_DIMMER] = 0 else: self._values[set_req.V_LIGHT] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) set_req = self.gateway.const.SetReq @@ -77,9 +77,9 @@ class MySensorsCover(mysensors.MySensorsEntity, CoverDevice): if self.gateway.optimistic: # Optimistically assume that cover has changed state. self._values[set_req.V_DIMMER] = position - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the device.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( diff --git a/homeassistant/components/light/mysensors.py b/homeassistant/components/light/mysensors.py index 6e41e0f5693..55387288d7f 100644 --- a/homeassistant/components/light/mysensors.py +++ b/homeassistant/components/light/mysensors.py @@ -130,7 +130,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): self._white = white self._values[self.value_type] = hex_color - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off.""" value_type = self.gateway.const.SetReq.V_LIGHT self.gateway.set_child_value( @@ -139,7 +139,7 @@ class MySensorsLight(mysensors.MySensorsEntity, Light): # optimistically assume that light has changed state self._state = False self._values[value_type] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() def _async_update_light(self): """Update the controller with values from light child.""" @@ -171,12 +171,12 @@ class MySensorsLightDimmer(MySensorsLight): """Flag supported features.""" return SUPPORT_BRIGHTNESS - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -196,13 +196,13 @@ class MySensorsLightRGB(MySensorsLight): return SUPPORT_BRIGHTNESS | SUPPORT_COLOR return SUPPORT_COLOR - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x', **kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" @@ -225,10 +225,10 @@ class MySensorsLightRGBW(MySensorsLightRGB): return SUPPORT_BRIGHTNESS | SUPPORT_MYSENSORS_RGBW return SUPPORT_MYSENSORS_RGBW - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on.""" self._turn_on_light() self._turn_on_dimmer(**kwargs) self._turn_on_rgb_and_w('%02x%02x%02x%02x', **kwargs) if self.gateway.optimistic: - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 9b394457973..f5ad59095dc 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -4,6 +4,7 @@ Connect to a MySensors gateway via pymysensors API. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mysensors/ """ +import asyncio from collections import defaultdict import logging import os @@ -16,17 +17,17 @@ import voluptuous as vol from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_START, + ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) + async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component -REQUIREMENTS = ['pymysensors==0.11.1'] +REQUIREMENTS = ['pymysensors==0.14.0'] _LOGGER = logging.getLogger(__name__) @@ -280,67 +281,62 @@ MYSENSORS_CONST_SCHEMA = { } -def setup(hass, config): +async def async_setup(hass, config): """Set up the MySensors component.""" import mysensors.mysensors as mysensors version = config[DOMAIN].get(CONF_VERSION) persistence = config[DOMAIN].get(CONF_PERSISTENCE) - def setup_gateway(device, persistence_file, baud_rate, tcp_port, in_prefix, - out_prefix): + async def setup_gateway( + device, persistence_file, baud_rate, tcp_port, in_prefix, + out_prefix): """Return gateway after setup of the gateway.""" if device == MQTT_COMPONENT: - if not setup_component(hass, MQTT_COMPONENT, config): - return + if not await async_setup_component(hass, MQTT_COMPONENT, config): + return None mqtt = hass.components.mqtt retain = config[DOMAIN].get(CONF_RETAIN) def pub_callback(topic, payload, qos, retain): """Call MQTT publish function.""" - mqtt.publish(topic, payload, qos, retain) + mqtt.async_publish(topic, payload, qos, retain) def sub_callback(topic, sub_cb, qos): """Call MQTT subscribe function.""" - mqtt.subscribe(topic, sub_cb, qos) - gateway = mysensors.MQTTGateway( - pub_callback, sub_callback, + @callback + def internal_callback(*args): + """Call callback.""" + sub_cb(*args) + + hass.async_add_job( + mqtt.async_subscribe(topic, internal_callback, qos)) + + gateway = mysensors.AsyncMQTTGateway( + pub_callback, sub_callback, in_prefix=in_prefix, + out_prefix=out_prefix, retain=retain, loop=hass.loop, event_callback=None, persistence=persistence, persistence_file=persistence_file, - protocol_version=version, in_prefix=in_prefix, - out_prefix=out_prefix, retain=retain) + protocol_version=version) else: try: - is_serial_port(device) - gateway = mysensors.SerialGateway( - device, event_callback=None, persistence=persistence, + await hass.async_add_job(is_serial_port, device) + gateway = mysensors.AsyncSerialGateway( + device, baud=baud_rate, loop=hass.loop, + event_callback=None, persistence=persistence, persistence_file=persistence_file, - protocol_version=version, baud=baud_rate) + protocol_version=version) except vol.Invalid: - try: - socket.getaddrinfo(device, None) - # valid ip address - gateway = mysensors.TCPGateway( - device, event_callback=None, persistence=persistence, - persistence_file=persistence_file, - protocol_version=version, port=tcp_port) - except OSError: - # invalid ip address - return + gateway = mysensors.AsyncTCPGateway( + device, port=tcp_port, loop=hass.loop, event_callback=None, + persistence=persistence, persistence_file=persistence_file, + protocol_version=version) gateway.metric = hass.config.units.is_metric gateway.optimistic = config[DOMAIN].get(CONF_OPTIMISTIC) gateway.device = device gateway.event_callback = gw_callback_factory(hass) - - def gw_start(event): - """Trigger to start of the gateway and any persistence.""" - if persistence: - discover_persistent_devices(hass, gateway) - gateway.start() - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, - lambda event: gateway.stop()) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, gw_start) + if persistence: + await gateway.start_persistence() return gateway @@ -357,7 +353,7 @@ def setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = setup_gateway( + ready_gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) if ready_gateway is not None: @@ -371,9 +367,36 @@ def setup(hass, config): hass.data[MYSENSORS_GATEWAYS] = gateways + hass.async_add_job(finish_setup(hass, gateways)) + return True +async def finish_setup(hass, gateways): + """Load any persistent devices and platforms and start gateway.""" + discover_tasks = [] + start_tasks = [] + for gateway in gateways.values(): + discover_tasks.append(discover_persistent_devices(hass, gateway)) + start_tasks.append(gw_start(hass, gateway)) + if discover_tasks: + # Make sure all devices and platforms are loaded before gateway start. + await asyncio.wait(discover_tasks, loop=hass.loop) + if start_tasks: + await asyncio.wait(start_tasks, loop=hass.loop) + + +async def gw_start(hass, gateway): + """Start the gateway.""" + @callback + def gw_stop(event): + """Trigger to stop the gateway.""" + hass.async_add_job(gateway.stop()) + + await gateway.start() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + + def validate_child(gateway, node_id, child): """Validate that a child has the correct values according to schema. @@ -431,14 +454,18 @@ def validate_child(gateway, node_id, child): return validated +@callback def discover_mysensors_platform(hass, platform, new_devices): """Discover a MySensors platform.""" - discovery.load_platform( - hass, platform, DOMAIN, {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN}) + task = hass.async_add_job(discovery.async_load_platform( + hass, platform, DOMAIN, + {ATTR_DEVICES: new_devices, CONF_NAME: DOMAIN})) + return task -def discover_persistent_devices(hass, gateway): +async def discover_persistent_devices(hass, gateway): """Discover platforms for devices loaded via persistence file.""" + tasks = [] new_devices = defaultdict(list) for node_id in gateway.sensors: node = gateway.sensors[node_id] @@ -447,7 +474,9 @@ def discover_persistent_devices(hass, gateway): for platform, dev_ids in validated.items(): new_devices[platform].extend(dev_ids) for platform, dev_ids in new_devices.items(): - discover_mysensors_platform(hass, platform, dev_ids) + tasks.append(discover_mysensors_platform(hass, platform, dev_ids)) + if tasks: + await asyncio.wait(tasks, loop=hass.loop) def get_mysensors_devices(hass, domain): @@ -459,6 +488,7 @@ def get_mysensors_devices(hass, domain): def gw_callback_factory(hass): """Return a new callback for the gateway.""" + @callback def mysensors_callback(msg): """Handle messages from a MySensors gateway.""" start = timer() @@ -489,7 +519,7 @@ def gw_callback_factory(hass): # Only one signal per device is needed. # A device can have multiple platforms, ie multiple schemas. # FOR LATER: Add timer to not signal if another update comes in. - dispatcher_send(hass, signal) + async_dispatcher_send(hass, signal) end = timer() if end - start > 0.1: _LOGGER.debug( diff --git a/homeassistant/components/notify/mysensors.py b/homeassistant/components/notify/mysensors.py index 257b5995446..1374779c5f0 100644 --- a/homeassistant/components/notify/mysensors.py +++ b/homeassistant/components/notify/mysensors.py @@ -42,7 +42,7 @@ class MySensorsNotificationService(BaseNotificationService): """Initialize the service.""" self.devices = mysensors.get_mysensors_devices(hass, DOMAIN) - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" target_devices = kwargs.get(ATTR_TARGET) devices = [device for device in self.devices.values() diff --git a/homeassistant/components/switch/mysensors.py b/homeassistant/components/switch/mysensors.py index c0f45cad861..a91ca6d11e7 100644 --- a/homeassistant/components/switch/mysensors.py +++ b/homeassistant/components/switch/mysensors.py @@ -42,7 +42,7 @@ async def async_setup_platform( hass, DOMAIN, discovery_info, device_class_map, async_add_devices=async_add_devices) - def send_ir_code_service(service): + async def async_send_ir_code_service(service): """Set IR code as device state attribute.""" entity_ids = service.data.get(ATTR_ENTITY_ID) ir_code = service.data.get(ATTR_IR_CODE) @@ -58,10 +58,10 @@ async def async_setup_platform( kwargs = {ATTR_IR_CODE: ir_code} for device in _devices: - device.turn_on(**kwargs) + await device.async_turn_on(**kwargs) hass.services.async_register( - DOMAIN, SERVICE_SEND_IR_CODE, send_ir_code_service, + DOMAIN, SERVICE_SEND_IR_CODE, async_send_ir_code_service, schema=SEND_IR_CODE_SERVICE_SCHEMA) @@ -84,23 +84,23 @@ class MySensorsSwitch(mysensors.MySensorsEntity, SwitchDevice): """Return True if switch is on.""" return self._values.get(self.value_type) == STATE_ON - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the switch on.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 1) if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the switch off.""" self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, 0) if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[self.value_type] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() class MySensorsIRSwitch(MySensorsSwitch): @@ -117,7 +117,7 @@ class MySensorsIRSwitch(MySensorsSwitch): set_req = self.gateway.const.SetReq return self._values.get(set_req.V_LIGHT) == STATE_ON - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the IR switch on.""" set_req = self.gateway.const.SetReq if ATTR_IR_CODE in kwargs: @@ -130,11 +130,11 @@ class MySensorsIRSwitch(MySensorsSwitch): # optimistically assume that switch has changed state self._values[self.value_type] = self._ir_code self._values[set_req.V_LIGHT] = STATE_ON - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() # turn off switch after switch was turned on - self.turn_off() + await self.async_turn_off() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the IR switch off.""" set_req = self.gateway.const.SetReq self.gateway.set_child_value( @@ -142,7 +142,7 @@ class MySensorsIRSwitch(MySensorsSwitch): if self.gateway.optimistic: # optimistically assume that switch has changed state self._values[set_req.V_LIGHT] = STATE_OFF - self.schedule_update_ha_state() + self.async_schedule_update_ha_state() async def async_update(self): """Update the controller with the latest value from a sensor.""" diff --git a/requirements_all.txt b/requirements_all.txt index cbc5cce0590..bb822934a1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -869,7 +869,7 @@ pymusiccast==0.1.6 pymyq==0.0.8 # homeassistant.components.mysensors -pymysensors==0.11.1 +pymysensors==0.14.0 # homeassistant.components.lock.nello pynello==1.5.1 From 528ad56530484a7f450fe153a7873e12089a4fde Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 11 May 2018 08:57:00 +0100 Subject: [PATCH 046/144] Adds facebox (#14356) * Adds facebox * Update .coveragerc * Remove facebox * Add test of faces attribute * Add event test * Adds more tests * Adds tests to increase coverage * Rename MOCK_FACES to MOCK_FACE * Adds STATE_UNKNOWN --- .../components/image_processing/facebox.py | 110 ++++++++++++++ .../image_processing/test_facebox.py | 139 ++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 homeassistant/components/image_processing/facebox.py create mode 100644 tests/components/image_processing/test_facebox.py diff --git a/homeassistant/components/image_processing/facebox.py b/homeassistant/components/image_processing/facebox.py new file mode 100644 index 00000000000..81b43c1f8e0 --- /dev/null +++ b/homeassistant/components/image_processing/facebox.py @@ -0,0 +1,110 @@ +""" +Component that will perform facial detection and identification via facebox. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/image_processing.facebox +""" +import base64 +import logging + +import requests +import voluptuous as vol + +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv +from homeassistant.components.image_processing import ( + PLATFORM_SCHEMA, ImageProcessingFaceEntity, CONF_SOURCE, CONF_ENTITY_ID, + CONF_NAME) +from homeassistant.const import (CONF_IP_ADDRESS, CONF_PORT) + +_LOGGER = logging.getLogger(__name__) + +CLASSIFIER = 'facebox' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Required(CONF_PORT): cv.port, +}) + + +def encode_image(image): + """base64 encode an image stream.""" + base64_img = base64.b64encode(image).decode('ascii') + return {"base64": base64_img} + + +def get_matched_faces(faces): + """Return the name and rounded confidence of matched faces.""" + return {face['name']: round(face['confidence'], 2) + for face in faces if face['matched']} + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the classifier.""" + entities = [] + for camera in config[CONF_SOURCE]: + entities.append(FaceClassifyEntity( + config[CONF_IP_ADDRESS], + config[CONF_PORT], + camera[CONF_ENTITY_ID], + camera.get(CONF_NAME) + )) + add_devices(entities) + + +class FaceClassifyEntity(ImageProcessingFaceEntity): + """Perform a face classification.""" + + def __init__(self, ip, port, camera_entity, name=None): + """Init with the API key and model id.""" + super().__init__() + self._url = "http://{}:{}/{}/check".format(ip, port, CLASSIFIER) + self._camera = camera_entity + if name: + self._name = name + else: + camera_name = split_entity_id(camera_entity)[1] + self._name = "{} {}".format( + CLASSIFIER, camera_name) + self._matched = {} + + def process_image(self, image): + """Process an image.""" + response = {} + try: + response = requests.post( + self._url, + json=encode_image(image), + timeout=9 + ).json() + except requests.exceptions.ConnectionError: + _LOGGER.error("ConnectionError: Is %s running?", CLASSIFIER) + response['success'] = False + + if response['success']: + faces = response['faces'] + total = response['facesCount'] + self.process_faces(faces, total) + self._matched = get_matched_faces(faces) + + else: + self.total_faces = None + self.faces = [] + self._matched = {} + + @property + def camera_entity(self): + """Return camera entity id from process pictures.""" + return self._camera + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the classifier attributes.""" + return { + 'matched_faces': self._matched, + } diff --git a/tests/components/image_processing/test_facebox.py b/tests/components/image_processing/test_facebox.py new file mode 100644 index 00000000000..cdc19a3d8d1 --- /dev/null +++ b/tests/components/image_processing/test_facebox.py @@ -0,0 +1,139 @@ +"""The tests for the facebox component.""" +from unittest.mock import patch + +import pytest +import requests +import requests_mock + +from homeassistant.core import callback +from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, + CONF_IP_ADDRESS, CONF_PORT, STATE_UNKNOWN) +from homeassistant.setup import async_setup_component +import homeassistant.components.image_processing as ip +import homeassistant.components.image_processing.facebox as fb + +MOCK_IP = '192.168.0.1' +MOCK_PORT = '8080' + +MOCK_FACE = {'confidence': 0.5812028911604818, + 'id': 'john.jpg', + 'matched': True, + 'name': 'John Lennon', + 'rect': {'height': 75, 'left': 63, 'top': 262, 'width': 74} + } + +MOCK_JSON = {"facesCount": 1, + "success": True, + "faces": [MOCK_FACE] + } + +VALID_ENTITY_ID = 'image_processing.facebox_demo_camera' +VALID_CONFIG = { + ip.DOMAIN: { + 'platform': 'facebox', + CONF_IP_ADDRESS: MOCK_IP, + CONF_PORT: MOCK_PORT, + ip.CONF_SOURCE: { + ip.CONF_ENTITY_ID: 'camera.demo_camera'} + }, + 'camera': { + 'platform': 'demo' + } + } + + +def test_encode_image(): + """Test that binary data is encoded correctly.""" + assert fb.encode_image(b'test')["base64"] == 'dGVzdA==' + + +def test_get_matched_faces(): + """Test that matched faces are parsed correctly.""" + assert fb.get_matched_faces([MOCK_FACE]) == {MOCK_FACE['name']: 0.58} + + +@pytest.fixture +def mock_image(): + """Return a mock camera image.""" + with patch('homeassistant.components.camera.demo.DemoCamera.camera_image', + return_value=b'Test') as image: + yield image + + +async def test_setup_platform(hass): + """Setup platform with one entity.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + +async def test_process_image(hass, mock_image): + """Test processing of an image.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + face_events = [] + + @callback + def mock_face_event(event): + """Mock event.""" + face_events.append(event) + + hass.bus.async_listen('image_processing.detect_face', mock_face_event) + + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.post(url, json=MOCK_JSON) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == '1' + assert state.attributes.get('matched_faces') == {MOCK_FACE['name']: 0.58} + + MOCK_FACE[ATTR_ENTITY_ID] = VALID_ENTITY_ID # Update. + assert state.attributes.get('faces') == [MOCK_FACE] + assert state.attributes.get(CONF_FRIENDLY_NAME) == 'facebox demo_camera' + + assert len(face_events) == 1 + assert face_events[0].data['name'] == MOCK_FACE['name'] + assert face_events[0].data['confidence'] == MOCK_FACE['confidence'] + assert face_events[0].data['entity_id'] == VALID_ENTITY_ID + + +async def test_connection_error(hass, mock_image): + """Test connection error.""" + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG) + assert hass.states.get(VALID_ENTITY_ID) + + with requests_mock.Mocker() as mock_req: + url = "http://{}:{}/facebox/check".format(MOCK_IP, MOCK_PORT) + mock_req.register_uri( + 'POST', url, exc=requests.exceptions.ConnectTimeout) + data = {ATTR_ENTITY_ID: VALID_ENTITY_ID} + await hass.services.async_call(ip.DOMAIN, + ip.SERVICE_SCAN, + service_data=data) + await hass.async_block_till_done() + + state = hass.states.get(VALID_ENTITY_ID) + assert state.state == STATE_UNKNOWN + assert state.attributes.get('faces') == [] + assert state.attributes.get('matched_faces') == {} + + +async def test_setup_platform_with_name(hass): + """Setup platform with one entity and a name.""" + MOCK_NAME = 'mock_name' + NAMED_ENTITY_ID = 'image_processing.{}'.format(MOCK_NAME) + + VALID_CONFIG_NAMED = VALID_CONFIG.copy() + VALID_CONFIG_NAMED[ip.DOMAIN][ip.CONF_SOURCE][ip.CONF_NAME] = MOCK_NAME + + await async_setup_component(hass, ip.DOMAIN, VALID_CONFIG_NAMED) + assert hass.states.get(NAMED_ENTITY_ID) + state = hass.states.get(NAMED_ENTITY_ID) + assert state.attributes.get(CONF_FRIENDLY_NAME) == MOCK_NAME From 48d70e520f21338d3f5e698158094e7cf30183aa Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 11 May 2018 20:28:28 +1000 Subject: [PATCH 047/144] more detailed error message (#14385) --- homeassistant/components/sensor/rest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 74bfaa38f02..75235bedaab 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -176,6 +176,7 @@ class RestData(object): self._request, timeout=10, verify=self._verify_ssl) self.data = response.text - except requests.exceptions.RequestException: - _LOGGER.error("Error fetching data: %s", self._request) + except requests.exceptions.RequestException as ex: + _LOGGER.error("Error fetching data: %s from %s failed with %s", + self._request, self._request.url, ex) self.data = None From 621c653fed587508f0052156b2a25bb7111037c7 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Fri, 11 May 2018 08:22:45 -0400 Subject: [PATCH 048/144] Allow HomeKit name to be customized (#14159) --- homeassistant/components/homekit/__init__.py | 17 ++++---- .../components/homekit/accessories.py | 12 +++--- .../components/homekit/type_covers.py | 6 +-- .../components/homekit/type_lights.py | 2 +- .../components/homekit/type_locks.py | 2 +- .../homekit/type_security_systems.py | 4 +- .../components/homekit/type_sensors.py | 12 +++--- .../components/homekit/type_switches.py | 2 +- .../components/homekit/type_thermostats.py | 2 +- homeassistant/components/homekit/util.py | 11 +++-- tests/components/homekit/test_accessories.py | 5 ++- .../homekit/test_get_accessories.py | 43 +++++++++++-------- tests/components/homekit/test_type_covers.py | 8 ++-- tests/components/homekit/test_type_lights.py | 8 ++-- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 10 ++--- tests/components/homekit/test_type_sensors.py | 14 +++--- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 8 ++-- tests/components/homekit/test_util.py | 6 ++- 20 files changed, 97 insertions(+), 79 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c31093a5eb8..028155593fb 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -12,20 +12,19 @@ import voluptuous as vol from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONF_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS, - TEMP_FAHRENHEIT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, - DEFAULT_PORT, DEFAULT_AUTO_START, SERVICE_HOMEKIT_START, + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, + DEFAULT_AUTO_START, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) -from .util import ( - validate_entity_config, show_setup_message) +from .util import show_setup_message, validate_entity_config TYPES = Registry() _LOGGER = logging.getLogger(__name__) @@ -93,7 +92,7 @@ def get_accessory(hass, state, aid, config): return None a_type = None - config = config or {} + name = config.get(CONF_NAME, state.name) if state.domain == 'alarm_control_panel': a_type = 'SecuritySystem' @@ -147,7 +146,7 @@ def get_accessory(hass, state, aid, config): return None _LOGGER.debug('Add "%s" as "%s"', state.entity_id, a_type) - return TYPES[a_type](hass, state.name, state.entity_id, aid, config=config) + return TYPES[a_type](hass, name, state.entity_id, aid, config) def generate_aid(entity_id): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index c47c3f8fbe7..7ec1fb542c9 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -16,8 +16,8 @@ from homeassistant.helpers.event import ( from homeassistant.util import dt as dt_util from .const import ( - DEBOUNCE_TIMEOUT, BRIDGE_MODEL, BRIDGE_NAME, - BRIDGE_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, + DEBOUNCE_TIMEOUT, MANUFACTURER) from .util import ( show_setup_message, dismiss_setup_message) @@ -64,14 +64,16 @@ def debounce(func): class HomeAccessory(Accessory): """Adapter class for Accessory.""" - def __init__(self, hass, name, entity_id, aid, category=CATEGORY_OTHER): + def __init__(self, hass, name, entity_id, aid, config, + category=CATEGORY_OTHER): """Initialize a Accessory object.""" super().__init__(name, aid=aid) - domain = split_entity_id(entity_id)[0].replace("_", " ").title() + model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, manufacturer=MANUFACTURER, - model=domain, serial_number=entity_id) + model=model, serial_number=entity_id) self.category = category + self.config = config self.entity_id = entity_id self.hass = hass diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3de87cf63e8..a32ba0370ec 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -28,7 +28,7 @@ class GarageDoorOpener(HomeAccessory): and support no more than open, close, and stop. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a GarageDoorOpener accessory object.""" super().__init__(*args, category=CATEGORY_GARAGE_DOOR_OPENER) self.flag_target_state = False @@ -69,7 +69,7 @@ class WindowCovering(HomeAccessory): The cover entity must support: set_cover_position. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) self.homekit_target = None @@ -108,7 +108,7 @@ class WindowCoveringBasic(HomeAccessory): stop_cover (optional). """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) features = self.hass.states.get(self.entity_id) \ diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 3efb0e99df6..d8a205d7026 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -26,7 +26,7 @@ class Light(HomeAccessory): Currently supports: state, brightness, color temperature, rgb_color. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) self._flag = {CHAR_ON: False, CHAR_BRIGHTNESS: False, diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index e7f18d44805..b08ac5930bd 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -29,7 +29,7 @@ class Lock(HomeAccessory): The lock entity must support: unlock and lock. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) self.flag_target_state = False diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index ab16f921e99..bd29453e10a 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -32,10 +32,10 @@ STATE_TO_SERVICE = {STATE_ALARM_ARMED_HOME: 'alarm_arm_home', class SecuritySystem(HomeAccessory): """Generate an SecuritySystem accessory for an alarm control panel.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a SecuritySystem accessory object.""" super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) - self._alarm_code = config.get(ATTR_CODE) + self._alarm_code = self.config.get(ATTR_CODE) self.flag_target_state = False serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 393b6beffd6..0005c6184ee 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -51,7 +51,7 @@ class TemperatureSensor(HomeAccessory): Sensor entity must return temperature in °C, °F. """ - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a TemperatureSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) @@ -74,7 +74,7 @@ class TemperatureSensor(HomeAccessory): class HumiditySensor(HomeAccessory): """Generate a HumiditySensor accessory as humidity sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a HumiditySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) @@ -94,7 +94,7 @@ class HumiditySensor(HomeAccessory): class AirQualitySensor(HomeAccessory): """Generate a AirQualitySensor accessory as air quality sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -118,7 +118,7 @@ class AirQualitySensor(HomeAccessory): class CarbonDioxideSensor(HomeAccessory): """Generate a CarbonDioxideSensor accessory as CO2 sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a CarbonDioxideSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -146,7 +146,7 @@ class CarbonDioxideSensor(HomeAccessory): class LightSensor(HomeAccessory): """Generate a LightSensor accessory as light sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a LightSensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) @@ -166,7 +166,7 @@ class LightSensor(HomeAccessory): class BinarySensor(HomeAccessory): """Generate a BinarySensor accessory as binary sensor.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a BinarySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) device_class = self.hass.states.get(self.entity_id).attributes \ diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 68a4fcdab0a..ff4bf1611b8 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) class Switch(HomeAccessory): """Generate a Switch accessory.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Switch accessory object to represent a remote.""" super().__init__(*args, category=CATEGORY_SWITCH) self._domain = split_entity_id(self.entity_id)[0] diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 15fd8160a7e..ab4d7faf875 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -38,7 +38,7 @@ SUPPORT_TEMP_RANGE = SUPPORT_TARGET_TEMPERATURE_LOW | \ class Thermostat(HomeAccessory): """Generate a Thermostat accessory for a climate.""" - def __init__(self, *args, config): + def __init__(self, *args): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = TEMP_CELSIUS diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 29fe3c8f265..c201d884a75 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.core import split_entity_id from homeassistant.const import ( - ATTR_CODE, TEMP_CELSIUS) + ATTR_CODE, CONF_NAME, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv import homeassistant.util.temperature as temp_util from .const import HOMEKIT_NOTIFY_ID @@ -16,13 +16,18 @@ _LOGGER = logging.getLogger(__name__) def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" entities = {} - for key, config in values.items(): - entity = cv.entity_id(key) + for entity_id, config in values.items(): + entity = cv.entity_id(entity_id) params = {} if not isinstance(config, dict): raise vol.Invalid('The configuration for "{}" must be ' ' an dictionary.'.format(entity)) + for key in (CONF_NAME, ): + value = config.get(key, -1) + if value != -1: + params[key] = cv.string(value) + domain, _ = split_entity_id(entity) if domain == 'alarm_control_panel': diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 48c6357c28d..799a831b745 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -56,9 +56,10 @@ async def test_debounce(hass): async def test_home_accessory(hass): """Test HomeAccessory class.""" - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2) + acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' + assert acc.aid == 2 assert acc.category == 1 # Category.OTHER assert len(acc.services) == 1 serv = acc.services[0] # SERV_ACCESSORY_INFO @@ -75,7 +76,7 @@ async def test_home_accessory(hass): hass.states.async_set('homekit.accessory', 'off') await hass.async_block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2) + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) assert acc.display_name == 'test_name' assert acc.aid == 2 assert len(acc.services) == 1 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 2ff591983c6..a6827300862 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -11,33 +11,42 @@ from homeassistant.components.climate import ( from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) _LOGGER = logging.getLogger(__name__) -def test_get_accessory_invalid_aid(caplog): - """Test with unsupported component.""" - assert get_accessory(None, State('light.demo', 'on'), - None, config=None) is None +def test_not_supported(caplog): + """Test if none is returned if entity isn't supported.""" + # not supported entity + assert get_accessory(None, State('demo.demo', 'on'), 2, {}) is None + + # invalid aid + assert get_accessory(None, State('light.demo', 'on'), None, None) is None assert caplog.records[0].levelname == 'WARNING' assert 'invalid aid' in caplog.records[0].msg -def test_not_supported(): - """Test if none is returned if entity isn't supported.""" - assert get_accessory(None, State('demo.demo', 'on'), 2, config=None) \ - is None +@pytest.mark.parametrize('config, name', [ + ({CONF_NAME: 'Customize Name'}, 'Customize Name'), +]) +def test_customize_options(config, name): + """Test with customized options.""" + mock_type = Mock() + with patch.dict(TYPES, {'Light': mock_type}): + entity_state = State('light.demo', 'on') + get_accessory(None, entity_state, 2, config) + mock_type.assert_called_with(None, name, 'light.demo', 2, config) @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ - ('Light', 'light.test', 'on', {}, None), - ('Lock', 'lock.test', 'locked', {}, None), + ('Light', 'light.test', 'on', {}, {}), + ('Lock', 'lock.test', 'locked', {}, {}), - ('Thermostat', 'climate.test', 'auto', {}, None), + ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | - SUPPORT_TARGET_TEMPERATURE_HIGH}, None), + SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, {ATTR_CODE: '1234'}), @@ -51,7 +60,7 @@ def test_types(type_name, entity_id, state, attrs, config): assert mock_type.called if config: - assert mock_type.call_args[1]['config'] == config + assert mock_type.call_args[0][-1] == config @pytest.mark.parametrize('type_name, entity_id, state, attrs', [ @@ -68,7 +77,7 @@ def test_type_covers(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, None) + get_accessory(None, entity_state, 2, {}) assert mock_type.called @@ -104,7 +113,7 @@ def test_type_sensors(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, None) + get_accessory(None, entity_state, 2, {}) assert mock_type.called @@ -118,5 +127,5 @@ def test_type_switches(type_name, entity_id, state, attrs): mock_type = Mock() with patch.dict(TYPES, {type_name: mock_type}): entity_state = State(entity_id, state, attrs) - get_accessory(None, entity_state, 2, None) + get_accessory(None, entity_state, 2, {}) assert mock_type.called diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index b833e1a03c9..fcc807338a9 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -32,7 +32,7 @@ async def test_garage_door_open_close(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' - acc = cls.garage(hass, 'Garage Door', entity_id, 2, config=None) + acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -87,7 +87,7 @@ async def test_window_set_cover_position(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' - acc = cls.window(hass, 'Cover', entity_id, 2, config=None) + acc = cls.window(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -135,7 +135,7 @@ async def test_window_open_close(hass, cls): hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: 0}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -198,7 +198,7 @@ async def test_window_open_close_stop(hass, cls): hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_STOP}) - acc = cls.window_basic(hass, 'Cover', entity_id, 2, config=None) + acc = cls.window_basic(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) # Set from HomeKit diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index b4965fc5ab8..d9602a6e41f 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -33,7 +33,7 @@ async def test_light_basic(hass, cls): hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.aid == 2 assert acc.category == 5 # Lightbulb @@ -81,7 +81,7 @@ async def test_light_brightness(hass, cls): hass.states.async_set(entity_id, STATE_ON, { ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.char_brightness.value == 0 @@ -126,7 +126,7 @@ async def test_light_color_temperature(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.char_color_temperature.value == 153 @@ -153,7 +153,7 @@ async def test_light_rgb_color(hass, cls): ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}) await hass.async_block_till_done() - acc = cls.light(hass, 'Light', entity_id, 2, config=None) + acc = cls.light(hass, 'Light', entity_id, 2, None) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3442c0da6c8..343fce288ac 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -11,7 +11,7 @@ async def test_lock_unlock(hass): """Test if accessory and HA are updated accordingly.""" entity_id = 'lock.kitchen_door' - acc = Lock(hass, 'Lock', entity_id, 2, config=None) + acc = Lock(hass, 'Lock', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 8c3d9474f26..59a700f73ee 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -5,9 +5,9 @@ from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.components.homekit.type_security_systems import ( SecuritySystem) from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED) from tests.common import async_mock_service @@ -18,7 +18,7 @@ async def test_switch_set_state(hass): config = {ATTR_CODE: code} entity_id = 'alarm_control_panel.test' - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -97,7 +97,7 @@ async def test_no_alarm_code(hass, config): """Test accessory if security_system doesn't require a alarm_code.""" entity_id = 'alarm_control_panel.test' - acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config=config) + acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) # Set from HomeKit call_arm_home = async_mock_service(hass, DOMAIN, 'alarm_arm_home') diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 39f48abd60e..a422116014d 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -12,7 +12,7 @@ async def test_temperature(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' - acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, config=None) + acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -42,7 +42,7 @@ async def test_humidity(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' - acc = HumiditySensor(hass, 'Humidity', entity_id, 2, config=None) + acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -63,7 +63,7 @@ async def test_air_quality(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' - acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, config=None) + acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -92,7 +92,7 @@ async def test_co2(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' - acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, config=None) + acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -125,7 +125,7 @@ async def test_light(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' - acc = LightSensor(hass, 'Light', entity_id, 2, config=None) + acc = LightSensor(hass, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -150,7 +150,7 @@ async def test_binary(hass): {ATTR_DEVICE_CLASS: 'opening'}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Window Opening', entity_id, 2, config=None) + acc = BinarySensor(hass, 'Window Opening', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -192,6 +192,6 @@ async def test_binary_device_classes(hass): {ATTR_DEVICE_CLASS: device_class}) await hass.async_block_till_done() - acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, config=None) + acc = BinarySensor(hass, 'Binary Sensor', entity_id, 2, None) assert acc.get_service(service).display_name == service assert acc.char_detected.display_name == char diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 7368179f232..00c1966305f 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -14,7 +14,7 @@ async def test_switch_set_state(hass, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] - acc = Switch(hass, 'Switch', entity_id, 2, config=None) + acc = Switch(hass, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index eea256c134d..ea592bd63dd 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -33,7 +33,7 @@ async def test_default_thermostat(hass, cls): hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -173,7 +173,7 @@ async def test_auto_thermostat(hass, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.char_cooling_thresh_temp.value == 23.0 @@ -252,7 +252,7 @@ async def test_power_state(hass, cls): ATTR_TEMPERATURE: 23.0, ATTR_CURRENT_TEMPERATURE: 18.0}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) assert acc.support_power_state is True @@ -304,7 +304,7 @@ async def test_thermostat_fahrenheit(hass, cls): # support_auto = True hass.states.async_set(entity_id, STATE_OFF, {ATTR_SUPPORTED_FEATURES: 6}) await hass.async_block_till_done() - acc = cls.thermostat(hass, 'Climate', entity_id, 2, config=None) + acc = cls.thermostat(hass, 'Climate', entity_id, 2, None) await hass.async_add_job(acc.run) hass.states.async_set(entity_id, STATE_AUTO, diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 2ec35975618..0b3a5475f7e 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -12,7 +12,7 @@ from homeassistant.components.homekit.util import validate_entity_config \ from homeassistant.components.persistent_notification import ( DOMAIN, ATTR_NOTIFICATION_ID) from homeassistant.const import ( - ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) from tests.common import async_mock_service @@ -21,13 +21,15 @@ def test_validate_entity_config(): """Test validate entities.""" configs = [{'invalid_entity_id': {}}, {'demo.test': 1}, {'demo.test': 'test'}, {'demo.test': [1, 2]}, - {'demo.test': None}] + {'demo.test': None}, {'demo.test': {CONF_NAME: None}}] for conf in configs: with pytest.raises(vol.Invalid): vec(conf) assert vec({}) == {} + assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ + {'demo.test': {CONF_NAME: 'Name'}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} From d6b81fb3459582854e09c6ec4d1adb3c9af14774 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Fri, 11 May 2018 22:40:32 +0200 Subject: [PATCH 049/144] Xiaomi Aqara: Add new cube model (sensor_cube.aqgl01) (#14393) --- homeassistant/components/binary_sensor/xiaomi_aqara.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/xiaomi_aqara.py b/homeassistant/components/binary_sensor/xiaomi_aqara.py index 49f716b9eb7..1c0b903d868 100644 --- a/homeassistant/components/binary_sensor/xiaomi_aqara.py +++ b/homeassistant/components/binary_sensor/xiaomi_aqara.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 'channel_1', hass, gateway)) devices.append(XiaomiButton(device, 'Wall Switch (Both)', 'dual_channel', hass, gateway)) - elif model in ['cube', 'sensor_cube']: + elif model in ['cube', 'sensor_cube', 'sensor_cube.aqgl01']: devices.append(XiaomiCube(device, hass, gateway)) add_devices(devices) From e80628d45bd77121a03b3177cd85ec8db3bd5f1f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Sat, 12 May 2018 01:51:48 -0400 Subject: [PATCH 050/144] Bump pycmus version (#14395) This commit bumps the pycmus version used by the cmus component. There was a bug in the previous version used, 1.0.0, when running in local mode. This was caused by a mtreinish/pycmus#1 and also was reported in the home-assistant forums (but not as an issue): https://community.home-assistant.io/t/cant-install-cmus-component/7961 Version 0.1.1 of pycmus fixes this issue so it should work properly for users running cmus and home-assistant on the same machine. --- homeassistant/components/media_player/cmus.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/cmus.py b/homeassistant/components/media_player/cmus.py index bcbee5c4ff7..0758b5f3058 100644 --- a/homeassistant/components/media_player/cmus.py +++ b/homeassistant/components/media_player/cmus.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_PASSWORD) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pycmus==0.1.0'] +REQUIREMENTS = ['pycmus==0.1.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index bb822934a1a..1ebc27cf248 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -729,7 +729,7 @@ pychannels==1.0.0 pychromecast==2.1.0 # homeassistant.components.media_player.cmus -pycmus==0.1.0 +pycmus==0.1.1 # homeassistant.components.comfoconnect pycomfoconnect==0.3 From 304137e7ff84ba504c6ebdae451b046f80f46131 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 12 May 2018 10:07:10 +0200 Subject: [PATCH 051/144] Fix name of tox pylint env (#14402) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 86acefe9b3f..fb1f7c8bda3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35, py36, lint, requirements, typing +envlist = py35, py36, lint, pylint, typing skip_missing_interpreters = True [testenv] From b903bbc04212a4cdfcdac5288827fa405a6020ca Mon Sep 17 00:00:00 2001 From: Lukas Barth Date: Sat, 12 May 2018 10:30:21 +0200 Subject: [PATCH 052/144] Fix waiting for setup that never happens (#14346) --- homeassistant/components/matrix.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/matrix.py b/homeassistant/components/matrix.py index 569b012b484..b2805c994e8 100644 --- a/homeassistant/components/matrix.py +++ b/homeassistant/components/matrix.py @@ -114,9 +114,6 @@ class MatrixBot(object): self._listening_rooms = listening_rooms - # Logging in is deferred b/c it does I/O - self._setup_done = False - # We have to fetch the aliases for every room to make sure we don't # join it twice by accident. However, fetching aliases is costly, # so we only do it once per room. @@ -343,9 +340,5 @@ class MatrixBot(object): def handle_send_message(self, service): """Handle the send_message service.""" - if not self._setup_done: - _LOGGER.warning("Could not send message: setup is not done!") - return - self._send_message(service.data[ATTR_MESSAGE], service.data[ATTR_TARGET]) From 01ce43ec7cc6007713ec32b8edfbe8d3a9c54665 Mon Sep 17 00:00:00 2001 From: damarco Date: Sat, 12 May 2018 14:41:44 +0200 Subject: [PATCH 053/144] Use None as initial state in zha component (#14389) * Return None if state is unknown * Use None as initial state --- homeassistant/components/binary_sensor/zha.py | 2 +- homeassistant/components/fan/zha.py | 5 ++--- homeassistant/components/light/zha.py | 3 +-- homeassistant/components/sensor/zha.py | 12 ++++++------ homeassistant/components/switch/zha.py | 2 +- homeassistant/components/zha/__init__.py | 2 +- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index 4f3f824c8f9..d3b31188760 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -108,7 +108,7 @@ class BinarySensor(zha.Entity, BinarySensorDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - if self._state == 'unknown': + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/fan/zha.py b/homeassistant/components/fan/zha.py index 3288a788e1f..01b1d0a92cf 100644 --- a/homeassistant/components/fan/zha.py +++ b/homeassistant/components/fan/zha.py @@ -10,7 +10,6 @@ from homeassistant.components import zha from homeassistant.components.fan import ( DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED) -from homeassistant.const import STATE_UNKNOWN DEPENDENCIES = ['zha'] @@ -72,7 +71,7 @@ class ZhaFan(zha.Entity, FanEntity): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return False return self._state != SPEED_OFF @@ -103,7 +102,7 @@ class ZhaFan(zha.Entity, FanEntity): """Retrieve latest state.""" result = yield from zha.safe_read(self._endpoint.fan, ['fan_mode']) new_value = result.get('fan_mode', None) - self._state = VALUE_TO_SPEED.get(new_value, STATE_UNKNOWN) + self._state = VALUE_TO_SPEED.get(new_value, None) @property def should_poll(self) -> bool: diff --git a/homeassistant/components/light/zha.py b/homeassistant/components/light/zha.py index 8eb1b3dc9b6..b44bf820b23 100644 --- a/homeassistant/components/light/zha.py +++ b/homeassistant/components/light/zha.py @@ -6,7 +6,6 @@ at https://home-assistant.io/components/light.zha/ """ import logging from homeassistant.components import light, zha -from homeassistant.const import STATE_UNKNOWN import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -76,7 +75,7 @@ class Light(zha.Entity, light.Light): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state == STATE_UNKNOWN: + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/sensor/zha.py b/homeassistant/components/sensor/zha.py index 6979690708d..3ca908a679d 100644 --- a/homeassistant/components/sensor/zha.py +++ b/homeassistant/components/sensor/zha.py @@ -102,8 +102,8 @@ class TemperatureSensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None celsius = round(float(self._state) / 100, 1) return convert_temperature( celsius, TEMP_CELSIUS, self.unit_of_measurement) @@ -122,8 +122,8 @@ class RelativeHumiditySensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None return round(float(self._state) / 100, 1) @@ -139,7 +139,7 @@ class PressureSensor(Sensor): @property def state(self): """Return the state of the entity.""" - if self._state == 'unknown': - return 'unknown' + if self._state is None: + return None return round(float(self._state)) diff --git a/homeassistant/components/switch/zha.py b/homeassistant/components/switch/zha.py index 22eb50be86b..6109dc192f3 100644 --- a/homeassistant/components/switch/zha.py +++ b/homeassistant/components/switch/zha.py @@ -51,7 +51,7 @@ class Switch(zha.Entity, SwitchDevice): @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" - if self._state == 'unknown': + if self._state is None: return False return bool(self._state) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index d293d4d07cd..238e89c07f0 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -319,7 +319,7 @@ class Entity(entity.Entity): self._endpoint = endpoint self._in_clusters = in_clusters self._out_clusters = out_clusters - self._state = ha_const.STATE_UNKNOWN + self._state = None self._unique_id = unique_id # Normally the entity itself is the listener. Sub-classes may set this From b371bf700f761c40a621acf7285d362a1a5bb68e Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 12 May 2018 15:09:48 +0200 Subject: [PATCH 054/144] Bump PyXiaomiGateway version (#14412) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index 48c54cdecff..cc7f3c8139d 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.0'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 1ebc27cf248..269e9f22a6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.0 +PyXiaomiGateway==0.9.1 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 990f476ac9188c1728ea1d69ce6cb2b52c51bcca Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sat, 12 May 2018 17:10:19 +0200 Subject: [PATCH 055/144] Homekit test cleanup (#14416) --- tests/components/homekit/test_accessories.py | 12 ++++++++++-- tests/components/homekit/test_get_accessories.py | 3 --- tests/components/homekit/test_type_covers.py | 4 ++++ tests/components/homekit/test_type_locks.py | 2 ++ .../components/homekit/test_type_security_systems.py | 4 ++++ tests/components/homekit/test_type_sensors.py | 10 ++++++++++ tests/components/homekit/test_type_switches.py | 2 ++ 7 files changed, 32 insertions(+), 5 deletions(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 799a831b745..f12b80632b6 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -56,7 +56,11 @@ async def test_debounce(hass): async def test_home_accessory(hass): """Test HomeAccessory class.""" - acc = HomeAccessory(hass, 'Home Accessory', 'homekit.accessory', 2, None) + entity_id = 'homekit.accessory' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = HomeAccessory(hass, 'Home Accessory', entity_id, 2, None) assert acc.hass == hass assert acc.display_name == 'Home Accessory' assert acc.aid == 2 @@ -76,7 +80,11 @@ async def test_home_accessory(hass): hass.states.async_set('homekit.accessory', 'off') await hass.async_block_till_done() - acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) + entity_id = 'test_model.demo' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = HomeAccessory('hass', 'test_name', entity_id, 2, None) assert acc.display_name == 'test_name' assert acc.aid == 2 assert len(acc.services) == 1 diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index a6827300862..cdfb858b727 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,5 +1,4 @@ """Package to test the get_accessory method.""" -import logging from unittest.mock import patch, Mock import pytest @@ -13,8 +12,6 @@ from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) -_LOGGER = logging.getLogger(__name__) - def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index fcc807338a9..dc4caeb35a6 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -32,6 +32,8 @@ async def test_garage_door_open_close(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.garage_door' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = cls.garage(hass, 'Garage Door', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -87,6 +89,8 @@ async def test_window_set_cover_position(hass, cls): """Test if accessory and HA are updated accordingly.""" entity_id = 'cover.window' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = cls.window(hass, 'Cover', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 343fce288ac..984d032a1d9 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -11,6 +11,8 @@ async def test_lock_unlock(hass): """Test if accessory and HA are updated accordingly.""" entity_id = 'lock.kitchen_door' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = Lock(hass, 'Lock', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 59a700f73ee..da5dac2d81b 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -18,6 +18,8 @@ async def test_switch_set_state(hass): config = {ATTR_CODE: code} entity_id = 'alarm_control_panel.test' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) await hass.async_add_job(acc.run) @@ -97,6 +99,8 @@ async def test_no_alarm_code(hass, config): """Test accessory if security_system doesn't require a alarm_code.""" entity_id = 'alarm_control_panel.test' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = SecuritySystem(hass, 'SecuritySystem', entity_id, 2, config) # Set from HomeKit diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index a422116014d..56742bada92 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -12,6 +12,8 @@ async def test_temperature(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.temperature' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = TemperatureSensor(hass, 'Temperature', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -42,6 +44,8 @@ async def test_humidity(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.humidity' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = HumiditySensor(hass, 'Humidity', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -63,6 +67,8 @@ async def test_air_quality(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.air_quality' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = AirQualitySensor(hass, 'Air Quality', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -92,6 +98,8 @@ async def test_co2(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.co2' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = CarbonDioxideSensor(hass, 'CO2', entity_id, 2, None) await hass.async_add_job(acc.run) @@ -125,6 +133,8 @@ async def test_light(hass): """Test if accessory is updated after state change.""" entity_id = 'sensor.light' + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = LightSensor(hass, 'Light', entity_id, 2, None) await hass.async_add_job(acc.run) diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 00c1966305f..399a8bd84c8 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -14,6 +14,8 @@ async def test_switch_set_state(hass, entity_id): """Test if accessory and HA are updated accordingly.""" domain = split_entity_id(entity_id)[0] + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() acc = Switch(hass, 'Switch', entity_id, 2, None) await hass.async_add_job(acc.run) From 99e272fc8d37de29e7660dfee6b062007201c66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sat, 12 May 2018 21:12:53 +0200 Subject: [PATCH 056/144] Upgrade PyXiaomiGatewa to 0.9.3 (#14420) (Closes: #14417) --- homeassistant/components/xiaomi_aqara.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara.py b/homeassistant/components/xiaomi_aqara.py index cc7f3c8139d..2cbf977443c 100644 --- a/homeassistant/components/xiaomi_aqara.py +++ b/homeassistant/components/xiaomi_aqara.py @@ -23,7 +23,7 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from homeassistant.util import slugify -REQUIREMENTS = ['PyXiaomiGateway==0.9.1'] +REQUIREMENTS = ['PyXiaomiGateway==0.9.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 269e9f22a6c..d81dcc8280d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -46,7 +46,7 @@ PyMVGLive==1.1.4 PyMata==2.14 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.9.1 +PyXiaomiGateway==0.9.3 # homeassistant.components.rpi_gpio # RPi.GPIO==0.6.1 From 70af7e5fad509d91db4e7900a3f3863f5cbb3f86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 12 May 2018 22:22:20 +0200 Subject: [PATCH 057/144] Update pylint to 1.8.4 (#14421) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6d5f68615be..9dcccd0d1da 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.3 +pylint==1.8.4 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 630ed06580c..e2157389a16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -9,7 +9,7 @@ flake8==3.5 mock-open==1.3.1 mypy==0.590 pydocstyle==1.1.1 -pylint==1.8.3 +pylint==1.8.4 pytest-aiohttp==0.3.0 pytest-cov==2.5.1 pytest-sugar==0.9.1 From 7aec098a05bf424f06988aa2d4fbeac0eb41269f Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 13 May 2018 00:44:53 +0300 Subject: [PATCH 058/144] Bring back typing check. Meanwhile just for homeassistant/*.py (#14410) * Bring back typing check. Meanwhile just for homeassistant/.py * Change follow-imports to silent. Add a few more checks. --- .travis.yml | 4 ++-- homeassistant/__main__.py | 5 +++-- homeassistant/auth.py | 2 +- homeassistant/bootstrap.py | 5 +++-- homeassistant/config.py | 6 +++--- homeassistant/core.py | 25 +++++++++++++++---------- homeassistant/exceptions.py | 3 ++- homeassistant/loader.py | 2 +- homeassistant/setup.py | 10 ++++++---- tests/conftest.py | 6 +++--- tox.ini | 3 ++- 11 files changed, 41 insertions(+), 30 deletions(-) diff --git a/.travis.yml b/.travis.yml index bf2d05bb185..b089d3f89be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,8 @@ matrix: env: TOXENV=lint - python: "3.5.3" env: TOXENV=pylint - # - python: "3.5" - # env: TOXENV=typing + - python: "3.5.3" + env: TOXENV=typing - python: "3.5.3" env: TOXENV=py35 - python: "3.6" diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index deb1746c167..7d3d2d2af88 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -8,7 +8,8 @@ import subprocess import sys import threading -from typing import Optional, List +from typing import Optional, List, Dict, Any # noqa #pylint: disable=unused-import + from homeassistant import monkey_patch from homeassistant.const import ( @@ -259,7 +260,7 @@ def setup_and_run_hass(config_dir: str, config = { 'frontend': {}, 'demo': {} - } + } # type: Dict[str, Any] hass = bootstrap.from_config_dict( config, config_dir=config_dir, verbose=args.verbose, skip_pip=args.skip_pip, log_rotate_days=args.log_rotate_days, diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 2c6c95f9b42..7c01776b7b1 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -35,7 +35,7 @@ ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30) DATA_REQS = 'auth_reqs_processed' -def generate_secret(entropy=32): +def generate_secret(entropy: int = 32) -> str: """Generate a secret. Backport of secrets.token_hex from Python 3.6 diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 826cc563e82..a405362d368 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -278,7 +278,8 @@ def async_enable_logging(hass: core.HomeAssistant, if log_rotate_days: err_handler = logging.handlers.TimedRotatingFileHandler( - err_log_path, when='midnight', backupCount=log_rotate_days) + err_log_path, when='midnight', + backupCount=log_rotate_days) # type: logging.FileHandler else: err_handler = logging.FileHandler( err_log_path, mode='w', delay=True) @@ -297,7 +298,7 @@ def async_enable_logging(hass: core.HomeAssistant, EVENT_HOMEASSISTANT_CLOSE, async_stop_async_handler) logger = logging.getLogger('') - logger.addHandler(async_handler) + logger.addHandler(async_handler) # type: ignore logger.setLevel(logging.INFO) # Save the log file location for access by other components. diff --git a/homeassistant/config.py b/homeassistant/config.py index 5c432490f6a..2f916e69b76 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,7 +7,7 @@ import os import re import shutil # pylint: disable=unused-import -from typing import Any, List, Tuple # NOQA +from typing import Any, List, Tuple, Optional # NOQA import voluptuous as vol from voluptuous.humanize import humanize_error @@ -60,7 +60,7 @@ DEFAULT_CORE_CONFIG = ( (CONF_TIME_ZONE, 'UTC', 'time_zone', 'Pick yours from here: http://en.wiki' 'pedia.org/wiki/List_of_tz_database_time_zones'), (CONF_CUSTOMIZE, '!include customize.yaml', None, 'Customization file'), -) # type: Tuple[Tuple[str, Any, Any, str], ...] +) # type: Tuple[Tuple[str, Any, Any, Optional[str]], ...] DEFAULT_CONFIG = """ # Show links to resources in log and frontend introduction: @@ -167,7 +167,7 @@ def get_default_config_dir() -> str: """Put together the default configuration directory based on the OS.""" data_dir = os.getenv('APPDATA') if os.name == "nt" \ else os.path.expanduser('~') - return os.path.join(data_dir, CONFIG_DIR_NAME) + return os.path.join(data_dir, CONFIG_DIR_NAME) # type: ignore def ensure_config_exists(config_dir: str, detect_location: bool = True) -> str: diff --git a/homeassistant/core.py b/homeassistant/core.py index feb8d331ae8..bc3b598180c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,7 @@ import threading from time import monotonic from types import MappingProxyType -from typing import Optional, Any, Callable, List # NOQA +from typing import Optional, Any, Callable, List, TypeVar, Dict # NOQA from async_timeout import timeout import voluptuous as vol @@ -41,6 +41,8 @@ import homeassistant.util.dt as dt_util import homeassistant.util.location as location from homeassistant.util.unit_system import UnitSystem, METRIC_SYSTEM # NOQA +T = TypeVar('T') + DOMAIN = 'homeassistant' # How long we wait for the result of a service call @@ -70,16 +72,15 @@ def valid_state(state: str) -> bool: return len(state) < 256 -def callback(func: Callable[..., None]) -> Callable[..., None]: +def callback(func: Callable[..., T]) -> Callable[..., T]: """Annotation to mark method as safe to call from within the event loop.""" - # pylint: disable=protected-access - func._hass_callback = True + setattr(func, '_hass_callback', True) return func def is_callback(func: Callable[..., Any]) -> bool: """Check if function is safe to be called in the event loop.""" - return '_hass_callback' in getattr(func, '__dict__', {}) + return getattr(func, '_hass_callback', False) is True @callback @@ -136,13 +137,14 @@ class HomeAssistant(object): self.data = {} self.state = CoreState.not_running self.exit_code = None + self.config_entries = None @property def is_running(self) -> bool: """Return if Home Assistant is running.""" return self.state in (CoreState.starting, CoreState.running) - def start(self) -> None: + def start(self) -> int: """Start home assistant.""" # Register the async start fire_coroutine_threadsafe(self.async_start(), self.loop) @@ -152,13 +154,13 @@ class HomeAssistant(object): # Block until stopped _LOGGER.info("Starting Home Assistant core loop") self.loop.run_forever() - return self.exit_code except KeyboardInterrupt: self.loop.call_soon_threadsafe( self.loop.create_task, self.async_stop()) self.loop.run_forever() finally: self.loop.close() + return self.exit_code async def async_start(self): """Finalize startup from inside the event loop. @@ -200,7 +202,10 @@ class HomeAssistant(object): self.loop.call_soon_threadsafe(self.async_add_job, target, *args) @callback - def async_add_job(self, target: Callable[..., None], *args: Any) -> None: + def async_add_job( + self, + target: Callable[..., Any], + *args: Any) -> Optional[asyncio.tasks.Task]: """Add a job from within the eventloop. This method must be run in the event loop. @@ -354,7 +359,7 @@ class EventBus(object): def __init__(self, hass: HomeAssistant) -> None: """Initialize a new event bus.""" - self._listeners = {} + self._listeners = {} # type: Dict[str, List[Callable]] self._hass = hass @callback @@ -1039,7 +1044,7 @@ class Config(object): # List of allowed external dirs to access self.whitelist_external_dirs = set() - def distance(self: object, lat: float, lon: float) -> float: + def distance(self, lat: float, lon: float) -> float: """Calculate distance from Home Assistant. Async friendly. diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index cb8a3c87820..73bd2377950 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,4 +1,5 @@ """The exceptions used by Home Assistant.""" +import jinja2 class HomeAssistantError(Exception): @@ -22,7 +23,7 @@ class NoEntitySpecifiedError(HomeAssistantError): class TemplateError(HomeAssistantError): """Error during template rendering.""" - def __init__(self, exception): + def __init__(self, exception: jinja2.TemplateError) -> None: """Init the error.""" super().__init__('{}: {}'.format(exception.__class__.__name__, exception)) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 67647a323c9..ce93c8705b5 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -93,7 +93,7 @@ def get_component(hass, comp_or_platform) -> Optional[ModuleType]: # This prevents that when only # custom_components/switch/some_platform.py exists, # the import custom_components.switch would succeed. - if module.__spec__.origin == 'namespace': + if module.__spec__ and module.__spec__.origin == 'namespace': continue _LOGGER.info("Loaded %s from %s", comp_or_platform, path) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f26aa9b61f1..1664653f2a7 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -139,10 +139,11 @@ async def _async_setup_component(hass: core.HomeAssistant, try: if hasattr(component, 'async_setup'): - result = await component.async_setup(hass, processed_config) + result = await component.async_setup( # type: ignore + hass, processed_config) else: result = await hass.async_add_job( - component.setup, hass, processed_config) + component.setup, hass, processed_config) # type: ignore except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, True) @@ -165,14 +166,15 @@ async def _async_setup_component(hass: core.HomeAssistant, for entry in hass.config_entries.async_entries(domain): await entry.async_setup(hass, component=component) - hass.config.components.add(component.DOMAIN) + hass.config.components.add(component.DOMAIN) # type: ignore # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) hass.bus.async_fire( - EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: component.DOMAIN} + EVENT_COMPONENT_LOADED, + {ATTR_COMPONENT: component.DOMAIN} # type: ignore ) return True diff --git a/tests/conftest.py b/tests/conftest.py index 73e69605eae..4d619c5ef61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ logging.basicConfig(level=logging.INFO) logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) -def test_real(func): +def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" @functools.wraps(func) def guard_func(*args, **kwargs): @@ -40,8 +40,8 @@ def test_real(func): # Guard a few functions that would make network connections -location.detect_location_info = test_real(location.detect_location_info) -location.elevation = test_real(location.elevation) +location.detect_location_info = check_real(location.detect_location_info) +location.elevation = check_real(location.elevation) util.get_local_ip = lambda: '127.0.0.1' diff --git a/tox.ini b/tox.ini index fb1f7c8bda3..d4bea81a2f5 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,8 @@ commands = [testenv:typing] basepython = {env:PYTHON3_PATH:python3} +whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt commands = - mypy --ignore-missing-imports --follow-imports=skip homeassistant + /bin/bash -c 'mypy --ignore-missing-imports --follow-imports=silent homeassistant/*.py' From d1228d5cf4fc4296c58c9417dde4b2ca3f3203a6 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sun, 13 May 2018 00:45:36 +0300 Subject: [PATCH 059/144] Look at registry before pulling zwave config values (#14408) * Look at registry before deciding on ID for zwave values * Reuse the new function --- homeassistant/components/zwave/__init__.py | 35 ++++++++++++------ homeassistant/helpers/entity_registry.py | 16 ++++++-- tests/components/zwave/test_init.py | 43 +++++++++++++++++++++- tests/helpers/test_entity_registry.py | 10 +++++ tests/mock/zwave.py | 1 + 5 files changed, 87 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 7562ac0ff14..a8ba5e4a6d3 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -16,10 +16,11 @@ from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers.event import track_time_change +from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util import homeassistant.helpers.config_validation as cv @@ -218,7 +219,7 @@ async def async_setup_platform(hass, config, async_add_devices, # pylint: disable=R0914 -def setup(hass, config): +async def async_setup(hass, config): """Set up Z-Wave. Will automatically load components to support devices found on the network. @@ -286,7 +287,7 @@ def setup(hass, config): continue values = ZWaveDeviceEntityValues( - hass, schema, value, config, device_config) + hass, schema, value, config, device_config, registry) # We create a new list and update the reference here so that # the list can be safely iterated over in the main thread @@ -294,6 +295,7 @@ def setup(hass, config): hass.data[DATA_ENTITY_VALUES] = new_values component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = await async_get_registry(hass) def node_added(node): """Handle a new node on the network.""" @@ -702,9 +704,9 @@ def setup(hass, config): # Setup autoheal if autoheal: _LOGGER.info("Z-Wave network autoheal is enabled") - track_time_change(hass, heal_network, hour=0, minute=0, second=0) + async_track_time_change(hass, heal_network, hour=0, minute=0, second=0) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_zwave) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_zwave) return True @@ -713,7 +715,7 @@ class ZWaveDeviceEntityValues(): """Manages entity access to the underlying zwave value objects.""" def __init__(self, hass, schema, primary_value, zwave_config, - device_config): + device_config, registry): """Initialize the values object with the passed entity schema.""" self._hass = hass self._zwave_config = zwave_config @@ -722,6 +724,7 @@ class ZWaveDeviceEntityValues(): self._values = {} self._entity = None self._workaround_ignore = False + self._registry = registry for name in self._schema[const.DISC_VALUES].keys(): self._values[name] = None @@ -794,9 +797,13 @@ class ZWaveDeviceEntityValues(): workaround_component, component) component = workaround_component - value_name = _value_name(self.primary) - generated_id = generate_entity_id(component + '.{}', value_name, []) - node_config = self._device_config.get(generated_id) + entity_id = self._registry.async_get_entity_id( + component, DOMAIN, + compute_value_unique_id(self._node, self.primary)) + if entity_id is None: + value_name = _value_name(self.primary) + entity_id = generate_entity_id(component + '.{}', value_name, []) + node_config = self._device_config.get(entity_id) # Configure node _LOGGER.debug("Adding Node_id=%s Generic_command_class=%s, " @@ -809,7 +816,7 @@ class ZWaveDeviceEntityValues(): if node_config.get(CONF_IGNORED): _LOGGER.info( - "Ignoring entity %s due to device settings", generated_id) + "Ignoring entity %s due to device settings", entity_id) # No entity will be created for this value self._workaround_ignore = True return @@ -964,6 +971,10 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): if (is_node_parsed(self.node) and self.values.primary.label != "Unknown") or \ self.node.is_ready: - return "{}-{}".format(self.node.node_id, - self.values.primary.object_id) + return compute_value_unique_id(self.node, self.values.primary) return None + + +def compute_value_unique_id(node, value): + """Compute unique_id a value would get if it were to get one.""" + return "{}-{}".format(node.node_id, value.object_id) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b5a9c309119..35cc1015aaf 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -83,6 +83,15 @@ class EntityRegistry: """Check if an entity_id is currently registered.""" return entity_id in self.entities + @callback + def async_get_entity_id(self, domain: str, platform: str, unique_id: str): + """Check if an entity_id is currently registered.""" + for entity in self.entities.values(): + if entity.domain == domain and entity.platform == platform and \ + entity.unique_id == unique_id: + return entity.entity_id + return None + @callback def async_generate_entity_id(self, domain, suggested_object_id): """Generate an entity ID that does not conflict. @@ -99,10 +108,9 @@ class EntityRegistry: def async_get_or_create(self, domain, platform, unique_id, *, suggested_object_id=None): """Get entity. Create if it doesn't exist.""" - for entity in self.entities.values(): - if entity.domain == domain and entity.platform == platform and \ - entity.unique_id == unique_id: - return entity + entity_id = self.async_get_entity_id(domain, platform, unique_id) + if entity_id: + return self.entities[entity_id] entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 0eba19f03a4..a25b725e500 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor.zwave import get_device from homeassistant.components.zwave import ( const, CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB, DATA_NETWORK) from homeassistant.setup import setup_component +from tests.common import mock_registry import pytest @@ -468,6 +469,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): """Initialize values for this testcase class.""" self.hass = get_test_home_assistant() self.hass.start() + self.registry = mock_registry(self.hass) setup_component(self.hass, 'zwave', {'zwave': {}}) self.hass.block_till_done() @@ -487,7 +489,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): const.DISC_OPTIONAL: True, }}} self.primary = MockValue( - command_class='mock_primary_class', node=self.node) + command_class='mock_primary_class', node=self.node, value_id=1000) self.secondary = MockValue( command_class='mock_secondary_class', node=self.node) self.duplicate_secondary = MockValue( @@ -521,6 +523,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) assert values.primary is self.primary @@ -592,6 +595,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) self.hass.block_till_done() @@ -630,6 +634,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -639,7 +644,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') def test_entity_workaround_component(self, discovery, get_platform): - """Test ignore workaround.""" + """Test component workaround.""" discovery.async_load_platform.return_value = mock_coro() mock_platform = MagicMock() get_platform.return_value = mock_platform @@ -666,6 +671,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -697,6 +703,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() @@ -720,12 +727,42 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() assert not discovery.async_load_platform.called + @patch.object(zwave, 'get_platform') + @patch.object(zwave, 'discovery') + def test_entity_config_ignore_with_registry(self, discovery, get_platform): + """Test ignore config. + + The case when the device is in entity registry. + """ + self.node.values = { + self.primary.value_id: self.primary, + self.secondary.value_id: self.secondary, + } + self.device_config = {'mock_component.registry_id': { + zwave.CONF_IGNORED: True + }} + self.registry.async_get_or_create( + 'mock_component', zwave.DOMAIN, '567-1000', + suggested_object_id='registry_id') + zwave.ZWaveDeviceEntityValues( + hass=self.hass, + schema=self.mock_schema, + primary_value=self.primary, + zwave_config=self.zwave_config, + device_config=self.device_config, + registry=self.registry + ) + self.hass.block_till_done() + + assert not discovery.async_load_platform.called + @patch.object(zwave, 'get_platform') @patch.object(zwave, 'discovery') def test_entity_platform_ignore(self, discovery, get_platform): @@ -743,6 +780,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) self.hass.block_till_done() @@ -770,6 +808,7 @@ class TestZWaveDeviceEntityValues(unittest.TestCase): primary_value=self.primary, zwave_config=self.zwave_config, device_config=self.device_config, + registry=self.registry ) values._check_entity_ready() self.hass.block_till_done() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index cb8703d1fe6..492b97f6387 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -180,3 +180,13 @@ test.disabled_hass: assert entry_disabled_hass.disabled_by == entity_registry.DISABLED_HASS assert entry_disabled_user.disabled assert entry_disabled_user.disabled_by == entity_registry.DISABLED_USER + + +@asyncio.coroutine +def test_async_get_entity_id(registry): + """Test that entity_id is returned.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234' + assert registry.async_get_entity_id( + 'light', 'hue', '1234') == 'light.hue_1234' + assert registry.async_get_entity_id('light', 'hue', '123') is None diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 67bfb590c3f..59d97ddb621 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -178,6 +178,7 @@ class MockValue(MagicMock): MockValue._mock_value_id += 1 value_id = MockValue._mock_value_id self.value_id = value_id + self.object_id = value_id for attr_name in kwargs: setattr(self, attr_name, kwargs[attr_name]) From ea2c0736123f3445c7183cf7f0a097da6374d302 Mon Sep 17 00:00:00 2001 From: Krasimir Chariyski Date: Sun, 13 May 2018 00:46:00 +0300 Subject: [PATCH 060/144] Add Bulgarian to Google TTS (#14422) --- homeassistant/components/tts/google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tts/google.py b/homeassistant/components/tts/google.py index bf03ec1adad..cb05795c445 100644 --- a/homeassistant/components/tts/google.py +++ b/homeassistant/components/tts/google.py @@ -29,7 +29,7 @@ SUPPORT_LANGUAGES = [ 'hr', 'cs', 'da', 'nl', 'en', 'en-au', 'en-uk', 'en-us', 'eo', 'fi', 'fr', 'de', 'el', 'hi', 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', 'mk', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru', 'sr', 'sk', 'es', 'es-es', - 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', + 'es-us', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy', 'uk', 'bg-BG' ] DEFAULT_LANG = 'en' From 843789528ed800268362782782d7c857bc68c6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 11:06:15 +0200 Subject: [PATCH 061/144] Remove extra quotes from docstrings (#14431) --- homeassistant/components/folder_watcher.py | 2 +- homeassistant/components/sensor/sigfox.py | 2 +- homeassistant/components/sensor/simulated.py | 2 +- tests/components/automation/test_event.py | 2 +- .../automation/test_numeric_state.py | 54 +++++++++---------- tests/components/binary_sensor/test_nx584.py | 2 +- .../components/binary_sensor/test_template.py | 16 +++--- tests/components/camera/test_uvc.py | 26 ++++----- .../device_tracker/test_unifi_direct.py | 6 +-- .../components/device_tracker/test_xiaomi.py | 6 +-- tests/components/fan/test_mqtt.py | 2 +- tests/components/light/test_template.py | 2 +- tests/components/notify/test_demo.py | 2 +- tests/components/notify/test_file.py | 2 +- tests/components/notify/test_group.py | 2 +- tests/components/notify/test_smtp.py | 2 +- tests/components/sensor/test_template.py | 4 +- tests/components/switch/test_mqtt.py | 2 +- tests/components/switch/test_template.py | 2 +- tests/components/test_mqtt_eventstream.py | 16 +++--- tests/components/test_mqtt_statestream.py | 22 ++++---- 21 files changed, 88 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/folder_watcher.py b/homeassistant/components/folder_watcher.py index 44110647632..098b34ac948 100644 --- a/homeassistant/components/folder_watcher.py +++ b/homeassistant/components/folder_watcher.py @@ -43,7 +43,7 @@ def setup(hass, config): def create_event_handler(patterns, hass): - """"Return the Watchdog EventHandler object.""" + """Return the Watchdog EventHandler object.""" from watchdog.events import PatternMatchingEventHandler class EventHandler(PatternMatchingEventHandler): diff --git a/homeassistant/components/sensor/sigfox.py b/homeassistant/components/sensor/sigfox.py index ef47132eefc..da8f3fcc639 100644 --- a/homeassistant/components/sensor/sigfox.py +++ b/homeassistant/components/sensor/sigfox.py @@ -66,7 +66,7 @@ class SigfoxAPI(object): self._devices = self.get_devices(device_types) def check_credentials(self): - """"Check API credentials are valid.""" + """Check API credentials are valid.""" url = urljoin(API_URL, 'devicetypes') response = requests.get(url, auth=self._auth, timeout=10) if response.status_code != 200: diff --git a/homeassistant/components/sensor/simulated.py b/homeassistant/components/sensor/simulated.py index 7091146e3ac..ae2d4939eab 100644 --- a/homeassistant/components/sensor/simulated.py +++ b/homeassistant/components/sensor/simulated.py @@ -87,7 +87,7 @@ class SimulatedSensor(Entity): self._state = None def time_delta(self): - """"Return the time delta.""" + """Return the time delta.""" dt0 = self._start_time dt1 = dt_util.utcnow() return dt1 - dt0 diff --git a/tests/components/automation/test_event.py b/tests/components/automation/test_event.py index df9ab69e7e8..aea6e517e38 100644 --- a/tests/components/automation/test_event.py +++ b/tests/components/automation/test_event.py @@ -26,7 +26,7 @@ class TestAutomationEvent(unittest.TestCase): self.hass.services.register('test', 'automation', record_call) def tearDown(self): - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_if_fires_on_event(self): diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 63ca4b5cd1a..de453675a57 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -35,7 +35,7 @@ class TestAutomationNumericState(unittest.TestCase): self.hass.stop() def test_if_fires_on_entity_change_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -62,7 +62,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_over_to_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -85,7 +85,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entities_change_over_to_below(self): - """"Test the firing with changed entities.""" + """Test the firing with changed entities.""" self.hass.states.set('test.entity_1', 11) self.hass.states.set('test.entity_2', 11) self.hass.block_till_done() @@ -115,7 +115,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(2, len(self.calls)) def test_if_not_fires_on_entity_change_below_to_below(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -148,7 +148,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_below_fires_on_entity_change_to_equal(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -171,7 +171,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_initial_entity_below(self): - """"Test the firing when starting with a match.""" + """Test the firing when starting with a match.""" self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -194,7 +194,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_initial_entity_above(self): - """"Test the firing when starting with a match.""" + """Test the firing when starting with a match.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -217,7 +217,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -236,7 +236,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_below_to_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -260,7 +260,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_entity_change_above_to_above(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -289,7 +289,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_above_fires_on_entity_change_to_equal(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" # set initial state self.hass.states.set('test.entity', 9) self.hass.block_till_done() @@ -313,7 +313,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_below_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -333,7 +333,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_below_above_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -353,7 +353,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_over_to_below_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -377,7 +377,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_fires_on_entity_change_over_to_below_above_range(self): - """"Test the firing with changed entity.""" + """Test the firing with changed entity.""" self.hass.states.set('test.entity', 11) self.hass.block_till_done() @@ -401,7 +401,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_if_entity_not_match(self): - """"Test if not fired with non matching entity.""" + """Test if not fired with non matching entity.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -420,7 +420,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_entity_change_below_with_attribute(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -439,7 +439,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_entity_change_not_below_with_attribute(self): - """"Test attributes.""" + """Test attributes.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -458,7 +458,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_fires_on_attribute_change_with_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -478,7 +478,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_if_not_fires_on_attribute_change_with_attribute_not_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -498,7 +498,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_on_entity_change_with_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -518,7 +518,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_not_fires_on_entity_change_with_not_attribute_below(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -538,7 +538,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(self): - """"Test attributes change.""" + """Test attributes change.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -559,7 +559,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_template_list(self): - """"Test template list.""" + """Test template list.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -581,7 +581,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(1, len(self.calls)) def test_template_string(self): - """"Test template string.""" + """Test template string.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -614,7 +614,7 @@ class TestAutomationNumericState(unittest.TestCase): self.calls[0].data['some']) def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(self): - """"Test if not fired changed attributes.""" + """Test if not fired changed attributes.""" assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { 'trigger': { @@ -635,7 +635,7 @@ class TestAutomationNumericState(unittest.TestCase): self.assertEqual(0, len(self.calls)) def test_if_action(self): - """"Test if action.""" + """Test if action.""" entity_id = 'domain.test_entity' assert setup_component(self.hass, automation.DOMAIN, { automation.DOMAIN: { diff --git a/tests/components/binary_sensor/test_nx584.py b/tests/components/binary_sensor/test_nx584.py index d94d887c641..4d1d85d30fb 100644 --- a/tests/components/binary_sensor/test_nx584.py +++ b/tests/components/binary_sensor/test_nx584.py @@ -113,7 +113,7 @@ class TestNX584SensorSetup(unittest.TestCase): self._test_assert_graceful_fail({}) def test_setup_version_too_old(self): - """"Test if version is too old.""" + """Test if version is too old.""" nx584_client.Client.return_value.get_version.return_value = '1.0' self._test_assert_graceful_fail({}) diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 18c095f4bc1..62623a04f3c 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -31,7 +31,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass.stop() def test_setup(self): - """"Test the setup.""" + """Test the setup.""" config = { 'binary_sensor': { 'platform': 'template', @@ -49,7 +49,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.hass, 'binary_sensor', config) def test_setup_no_sensors(self): - """"Test setup with no sensors.""" + """Test setup with no sensors.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -58,7 +58,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_device(self): - """"Test the setup with invalid devices.""" + """Test the setup with invalid devices.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -70,7 +70,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_device_class(self): - """"Test setup with invalid sensor class.""" + """Test setup with invalid sensor class.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -85,7 +85,7 @@ class TestBinarySensorTemplate(unittest.TestCase): }) def test_setup_invalid_missing_template(self): - """"Test setup with invalid and missing template.""" + """Test setup with invalid and missing template.""" with assert_setup_component(0): assert setup.setup_component(self.hass, 'binary_sensor', { 'binary_sensor': { @@ -161,7 +161,7 @@ class TestBinarySensorTemplate(unittest.TestCase): assert state.attributes['entity_picture'] == '/local/sensor.png' def test_attributes(self): - """"Test the attributes.""" + """Test the attributes.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', @@ -182,7 +182,7 @@ class TestBinarySensorTemplate(unittest.TestCase): self.assertTrue(vs.is_on) def test_event(self): - """"Test the event.""" + """Test the event.""" config = { 'binary_sensor': { 'platform': 'template', @@ -214,7 +214,7 @@ class TestBinarySensorTemplate(unittest.TestCase): @mock.patch('homeassistant.helpers.template.Template.render') def test_update_template_error(self, mock_render): - """"Test the template update error.""" + """Test the template update error.""" vs = run_callback_threadsafe( self.hass.loop, template.BinarySensorTemplate, self.hass, 'parent', 'Parent', 'motion', diff --git a/tests/components/camera/test_uvc.py b/tests/components/camera/test_uvc.py index 40b4fb2d8e2..dabad953bea 100644 --- a/tests/components/camera/test_uvc.py +++ b/tests/components/camera/test_uvc.py @@ -26,7 +26,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_full_config(self, mock_uvc, mock_remote): - """"Test the setup with full configuration.""" + """Test the setup with full configuration.""" config = { 'platform': 'uvc', 'nvr': 'foo', @@ -41,7 +41,7 @@ class TestUVCSetup(unittest.TestCase): ] def fake_get_camera(uuid): - """"Create a fake camera.""" + """Create a fake camera.""" if uuid == 'id3': return {'model': 'airCam'} else: @@ -65,7 +65,7 @@ class TestUVCSetup(unittest.TestCase): @mock.patch('uvcclient.nvr.UVCRemote') @mock.patch.object(uvc, 'UnifiVideoCamera') def test_setup_partial_config(self, mock_uvc, mock_remote): - """"Test the setup with partial configuration.""" + """Test the setup with partial configuration.""" config = { 'platform': 'uvc', 'nvr': 'foo', @@ -152,7 +152,7 @@ class TestUVC(unittest.TestCase): """Test class for UVC.""" def setup_method(self, method): - """"Setup the mock camera.""" + """Setup the mock camera.""" self.nvr = mock.MagicMock() self.uuid = 'uuid' self.name = 'name' @@ -171,7 +171,7 @@ class TestUVC(unittest.TestCase): self.nvr.server_version = (3, 2, 0) def test_properties(self): - """"Test the properties.""" + """Test the properties.""" self.assertEqual(self.name, self.uvc.name) self.assertTrue(self.uvc.is_recording) self.assertEqual('Ubiquiti', self.uvc.brand) @@ -180,7 +180,7 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login(self, mock_camera, mock_store): - """"Test the login.""" + """Test the login.""" self.uvc._login() self.assertEqual(mock_camera.call_count, 1) self.assertEqual( @@ -205,7 +205,7 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_tries_both_addrs_and_caches(self, mock_camera, mock_store): - """"Test the login tries.""" + """Test the login tries.""" responses = [0] def fake_login(*a): @@ -234,13 +234,13 @@ class TestUVC(unittest.TestCase): @mock.patch('uvcclient.store.get_info_store') @mock.patch('uvcclient.camera.UVCCameraClientV320') def test_login_fails_both_properly(self, mock_camera, mock_store): - """"Test if login fails properly.""" + """Test if login fails properly.""" mock_camera.return_value.login.side_effect = socket.error self.assertEqual(None, self.uvc._login()) self.assertEqual(None, self.uvc._connect_addr) def test_camera_image_tries_login_bails_on_failure(self): - """"Test retrieving failure.""" + """Test retrieving failure.""" with mock.patch.object(self.uvc, '_login') as mock_login: mock_login.return_value = False self.assertEqual(None, self.uvc.camera_image()) @@ -248,19 +248,19 @@ class TestUVC(unittest.TestCase): self.assertEqual(mock_login.call_args, mock.call()) def test_camera_image_logged_in(self): - """"Test the login state.""" + """Test the login state.""" self.uvc._camera = mock.MagicMock() self.assertEqual(self.uvc._camera.get_snapshot.return_value, self.uvc.camera_image()) def test_camera_image_error(self): - """"Test the camera image error.""" + """Test the camera image error.""" self.uvc._camera = mock.MagicMock() self.uvc._camera.get_snapshot.side_effect = camera.CameraConnectError self.assertEqual(None, self.uvc.camera_image()) def test_camera_image_reauths(self): - """"Test the re-authentication.""" + """Test the re-authentication.""" responses = [0] def fake_snapshot(): @@ -281,7 +281,7 @@ class TestUVC(unittest.TestCase): self.assertEqual([], responses) def test_camera_image_reauths_only_once(self): - """"Test if the re-authentication only happens once.""" + """Test if the re-authentication only happens once.""" self.uvc._camera = mock.MagicMock() self.uvc._camera.get_snapshot.side_effect = camera.CameraAuthError with mock.patch.object(self.uvc, '_login') as mock_login: diff --git a/tests/components/device_tracker/test_unifi_direct.py b/tests/components/device_tracker/test_unifi_direct.py index 8bc3a60146c..ccfa59404a1 100644 --- a/tests/components/device_tracker/test_unifi_direct.py +++ b/tests/components/device_tracker/test_unifi_direct.py @@ -71,7 +71,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh') def test_get_device_name(self, mock_ssh): - """"Testing MAC matching.""" + """Testing MAC matching.""" conf_dict = { DOMAIN: { CONF_PLATFORM: 'unifi_direct', @@ -95,7 +95,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh.logout') @patch('pexpect.pxssh.pxssh.login') def test_failed_to_log_in(self, mock_login, mock_logout): - """"Testing exception at login results in False.""" + """Testing exception at login results in False.""" from pexpect import exceptions conf_dict = { @@ -120,7 +120,7 @@ class TestComponentsDeviceTrackerUnifiDirect(unittest.TestCase): @patch('pexpect.pxssh.pxssh.sendline') def test_to_get_update(self, mock_sendline, mock_prompt, mock_login, mock_logout): - """"Testing exception in get_update matching.""" + """Testing exception in get_update matching.""" conf_dict = { DOMAIN: { CONF_PLATFORM: 'unifi_direct', diff --git a/tests/components/device_tracker/test_xiaomi.py b/tests/components/device_tracker/test_xiaomi.py index 19f25b514db..bdd921f395f 100644 --- a/tests/components/device_tracker/test_xiaomi.py +++ b/tests/components/device_tracker/test_xiaomi.py @@ -210,7 +210,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_invalid_credential(self, mock_get, mock_post): - """"Testing invalid credential handling.""" + """Testing invalid credential handling.""" config = { DOMAIN: xiaomi.PLATFORM_SCHEMA({ CONF_PLATFORM: xiaomi.DOMAIN, @@ -224,7 +224,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_valid_credential(self, mock_get, mock_post): - """"Testing valid refresh.""" + """Testing valid refresh.""" config = { DOMAIN: xiaomi.PLATFORM_SCHEMA({ CONF_PLATFORM: xiaomi.DOMAIN, @@ -244,7 +244,7 @@ class TestXiaomiDeviceScanner(unittest.TestCase): @patch('requests.get', side_effect=mocked_requests) @patch('requests.post', side_effect=mocked_requests) def test_token_timed_out(self, mock_get, mock_post): - """"Testing refresh with a timed out token. + """Testing refresh with a timed out token. New token is requested and list is downloaded a second time. """ diff --git a/tests/components/fan/test_mqtt.py b/tests/components/fan/test_mqtt.py index ec68492ed1e..9060d7b9986 100644 --- a/tests/components/fan/test_mqtt.py +++ b/tests/components/fan/test_mqtt.py @@ -18,7 +18,7 @@ class TestMqttFan(unittest.TestCase): self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_default_availability_payload(self): diff --git a/tests/components/light/test_template.py b/tests/components/light/test_template.py index 2d45ad1bf94..962760672f1 100644 --- a/tests/components/light/test_template.py +++ b/tests/components/light/test_template.py @@ -36,7 +36,7 @@ class TestTemplateLight: self.hass.stop() def test_template_state_text(self): - """"Test the state text of a template.""" + """Test the state text of a template.""" with assert_setup_component(1, 'light'): assert setup.setup_component(self.hass, 'light', { 'light': { diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py index 5bd3270b922..71b472afe74 100644 --- a/tests/components/notify/test_demo.py +++ b/tests/components/notify/test_demo.py @@ -33,7 +33,7 @@ class TestNotifyDemo(unittest.TestCase): self.hass.bus.listen(demo.EVENT_NOTIFY, record_event) def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() def _setup_notify(self): diff --git a/tests/components/notify/test_file.py b/tests/components/notify/test_file.py index c5064fca851..d59bbe4d720 100644 --- a/tests/components/notify/test_file.py +++ b/tests/components/notify/test_file.py @@ -20,7 +20,7 @@ class TestNotifyFile(unittest.TestCase): self.hass = get_test_home_assistant() def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() def test_bad_config(self): diff --git a/tests/components/notify/test_group.py b/tests/components/notify/test_group.py index c96a49d7cb3..a847de51142 100644 --- a/tests/components/notify/test_group.py +++ b/tests/components/notify/test_group.py @@ -53,7 +53,7 @@ class TestNotifyGroup(unittest.TestCase): assert self.service is not None def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_send_message_with_data(self): diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 127eecae2b7..29e34974c6c 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -27,7 +27,7 @@ class TestNotifySmtp(unittest.TestCase): 'HomeAssistant', 0) def tearDown(self): # pylint: disable=invalid-name - """"Stop down everything that was started.""" + """Stop down everything that was started.""" self.hass.stop() @patch('email.utils.make_msgid', return_value='') diff --git a/tests/components/sensor/test_template.py b/tests/components/sensor/test_template.py index f8d912f24dd..6861d3a5070 100644 --- a/tests/components/sensor/test_template.py +++ b/tests/components/sensor/test_template.py @@ -269,7 +269,7 @@ class TestTemplateSensor: assert self.hass.states.all() == [] def test_setup_invalid_device_class(self): - """"Test setup with invalid device_class.""" + """Test setup with invalid device_class.""" with assert_setup_component(0): assert setup_component(self.hass, 'sensor', { 'sensor': { @@ -284,7 +284,7 @@ class TestTemplateSensor: }) def test_setup_valid_device_class(self): - """"Test setup with valid device_class.""" + """Test setup with valid device_class.""" with assert_setup_component(1): assert setup_component(self.hass, 'sensor', { 'sensor': { diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 24db0540012..31f9a729c53 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -20,7 +20,7 @@ class TestSwitchMQTT(unittest.TestCase): self.mock_publish = mock_mqtt_component(self.hass) def tearDown(self): # pylint: disable=invalid-name - """"Stop everything that was started.""" + """Stop everything that was started.""" self.hass.stop() def test_controlling_state_via_topic(self): diff --git a/tests/components/switch/test_template.py b/tests/components/switch/test_template.py index 7456ae11a0d..8f7bbda8e98 100644 --- a/tests/components/switch/test_template.py +++ b/tests/components/switch/test_template.py @@ -32,7 +32,7 @@ class TestTemplateSwitch: self.hass.stop() def test_template_state_text(self): - """"Test the state text of a template.""" + """Test the state text of a template.""" with assert_setup_component(1, 'switch'): assert setup.setup_component(self.hass, 'switch', { 'switch': { diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py index f4fc3e89ee0..48bc04d46ed 100644 --- a/tests/components/test_mqtt_eventstream.py +++ b/tests/components/test_mqtt_eventstream.py @@ -44,11 +44,11 @@ class TestMqttEventStream(object): eventstream.DOMAIN: config}) def test_setup_succeeds(self): - """"Test the success of the setup.""" + """Test the success of the setup.""" assert self.add_eventstream() def test_setup_with_pub(self): - """"Test the setup with subscription.""" + """Test the setup with subscription.""" # Should start off with no listeners for all events assert self.hass.bus.listeners.get('*') is None @@ -60,7 +60,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_subscribe') def test_subscribe(self, mock_sub): - """"Test the subscription.""" + """Test the subscription.""" sub_topic = 'foo' assert self.add_eventstream(sub_topic=sub_topic) self.hass.block_till_done() @@ -71,7 +71,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if event changed.""" + """Test the sending of a new message if event changed.""" now = dt_util.as_utc(dt_util.now()) e_id = 'fake.entity' pub_topic = 'bar' @@ -113,7 +113,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_time_event_does_not_send_message(self, mock_pub): - """"Test the sending of a new message if time event.""" + """Test the sending of a new message if time event.""" assert self.add_eventstream(pub_topic='bar') self.hass.block_till_done() @@ -125,7 +125,7 @@ class TestMqttEventStream(object): assert not mock_pub.called def test_receiving_remote_event_fires_hass_event(self): - """"Test the receiving of the remotely fired event.""" + """Test the receiving of the remotely fired event.""" sub_topic = 'foo' assert self.add_eventstream(sub_topic=sub_topic) self.hass.block_till_done() @@ -150,7 +150,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_ignored_event_doesnt_send_over_stream(self, mock_pub): - """"Test the ignoring of sending events if defined.""" + """Test the ignoring of sending events if defined.""" assert self.add_eventstream(pub_topic='bar', ignore_event=['state_changed']) self.hass.block_till_done() @@ -177,7 +177,7 @@ class TestMqttEventStream(object): @patch('homeassistant.components.mqtt.async_publish') def test_wrong_ignored_event_sends_over_stream(self, mock_pub): - """"Test the ignoring of sending events if defined.""" + """Test the ignoring of sending events if defined.""" assert self.add_eventstream(pub_topic='bar', ignore_event=['statee_changed']) self.hass.block_till_done() diff --git a/tests/components/test_mqtt_statestream.py b/tests/components/test_mqtt_statestream.py index e120c3a7dd2..2ed2f4487ea 100644 --- a/tests/components/test_mqtt_statestream.py +++ b/tests/components/test_mqtt_statestream.py @@ -47,17 +47,17 @@ class TestMqttStateStream(object): assert self.add_statestream() is False def test_setup_succeeds_without_attributes(self): - """"Test the success of the setup with a valid base_topic.""" + """Test the success of the setup with a valid base_topic.""" assert self.add_statestream(base_topic='pub') def test_setup_succeeds_with_attributes(self): - """"Test setup with a valid base_topic and publish_attributes.""" + """Test setup with a valid base_topic and publish_attributes.""" assert self.add_statestream(base_topic='pub', publish_attributes=True) @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if event changed.""" + """Test the sending of a new message if event changed.""" e_id = 'fake.entity' base_topic = 'pub' @@ -84,7 +84,7 @@ class TestMqttStateStream(object): self, mock_utcnow, mock_pub): - """"Test the sending of a message and timestamps if event changed.""" + """Test the sending of a message and timestamps if event changed.""" e_id = 'another.entity' base_topic = 'pub' @@ -118,7 +118,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_attr_sends_message(self, mock_utcnow, mock_pub): - """"Test the sending of a new message if attribute changed.""" + """Test the sending of a new message if attribute changed.""" e_id = 'fake.entity' base_topic = 'pub' @@ -160,7 +160,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_domain(self, mock_utcnow, mock_pub): - """"Test that filtering on included domain works as expected.""" + """Test that filtering on included domain works as expected.""" base_topic = 'pub' incl = { @@ -198,7 +198,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_entity(self, mock_utcnow, mock_pub): - """"Test that filtering on included entity works as expected.""" + """Test that filtering on included entity works as expected.""" base_topic = 'pub' incl = { @@ -236,7 +236,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_domain(self, mock_utcnow, mock_pub): - """"Test that filtering on excluded domain works as expected.""" + """Test that filtering on excluded domain works as expected.""" base_topic = 'pub' incl = {} @@ -274,7 +274,7 @@ class TestMqttStateStream(object): @patch('homeassistant.components.mqtt.async_publish') @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_entity(self, mock_utcnow, mock_pub): - """"Test that filtering on excluded entity works as expected.""" + """Test that filtering on excluded entity works as expected.""" base_topic = 'pub' incl = {} @@ -313,7 +313,7 @@ class TestMqttStateStream(object): @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_exclude_domain_include_entity( self, mock_utcnow, mock_pub): - """"Test filtering with excluded domain and included entity.""" + """Test filtering with excluded domain and included entity.""" base_topic = 'pub' incl = { @@ -354,7 +354,7 @@ class TestMqttStateStream(object): @patch('homeassistant.core.dt_util.utcnow') def test_state_changed_event_include_domain_exclude_entity( self, mock_utcnow, mock_pub): - """"Test filtering with included domain and excluded entity.""" + """Test filtering with included domain and excluded entity.""" base_topic = 'pub' incl = { From 234bf1f0ead4edeb2b522e40a72ec92cdc409c0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 12:09:28 +0200 Subject: [PATCH 062/144] Spelling, grammar etc fixes (#14432) * Spelling, grammar etc fixes * s/an api data/data of an api/ --- homeassistant/components/bmw_connected_drive/services.yaml | 4 ++-- homeassistant/components/hassio/handler.py | 2 +- homeassistant/components/homekit/util.py | 2 +- homeassistant/components/insteon_plm/services.yaml | 2 +- homeassistant/components/media_player/yamaha.py | 2 +- homeassistant/components/sensor/buienradar.py | 2 +- homeassistant/components/sensor/hive.py | 2 +- homeassistant/components/sensor/statistics.py | 2 +- homeassistant/components/switch/tahoma.py | 4 ++-- tests/components/homekit/test_type_security_systems.py | 2 +- tests/components/media_player/test_blackbird.py | 2 +- tests/components/sensor/test_sigfox.py | 4 ++-- tests/components/test_folder_watcher.py | 2 +- tests/components/test_prometheus.py | 2 +- tests/test_core.py | 6 +++--- 15 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/services.yaml b/homeassistant/components/bmw_connected_drive/services.yaml index 3c180271919..b9605429a8e 100644 --- a/homeassistant/components/bmw_connected_drive/services.yaml +++ b/homeassistant/components/bmw_connected_drive/services.yaml @@ -27,7 +27,7 @@ activate_air_conditioning: description: > Start the air conditioning of the vehicle. What exactly is started here depends on the type of vehicle. It might range from just ventilation over - auxilary heating to real air conditioning. The vehicle is identified via + auxiliary heating to real air conditioning. The vehicle is identified via the vin (see below). fields: vin: @@ -39,4 +39,4 @@ update_state: description: > Fetch the last state of the vehicles of all your accounts from the BMW server. This does *not* trigger an update from the vehicle, it just gets - the data from the BMW servers. This service does not require any attributes. \ No newline at end of file + the data from the BMW servers. This service does not require any attributes. diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index a954aaccbd4..c3caf40ba62 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -33,7 +33,7 @@ def _api_bool(funct): def _api_data(funct): - """Return a api data.""" + """Return data of an api.""" @asyncio.coroutine def _wrapper(*argv, **kwargs): """Wrap function.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index c201d884a75..5ddef534202 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -21,7 +21,7 @@ def validate_entity_config(values): params = {} if not isinstance(config, dict): raise vol.Invalid('The configuration for "{}" must be ' - ' an dictionary.'.format(entity)) + ' a dictionary.'.format(entity)) for key in (CONF_NAME, ): value = config.get(key, -1) diff --git a/homeassistant/components/insteon_plm/services.yaml b/homeassistant/components/insteon_plm/services.yaml index a0e250fef1f..9ea53c10fbf 100644 --- a/homeassistant/components/insteon_plm/services.yaml +++ b/homeassistant/components/insteon_plm/services.yaml @@ -14,7 +14,7 @@ delete_all_link: description: All-Link group number. example: 1 load_all_link_database: - description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistant. This may take a LONG time and may need to be repeated to obtain all records. + description: Load the All-Link Database for a device. WARNING - Loading a device All-LInk database is very time consuming and inconsistent. This may take a LONG time and may need to be repeated to obtain all records. fields: entity_id: description: Name of the device to print diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 5b8ac2ad236..bb7942a2545 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -222,7 +222,7 @@ class YamahaDevice(MediaPlayerDevice): @property def zone_id(self): - """Return an zone_id to ensure 1 media player per zone.""" + """Return a zone_id to ensure 1 media player per zone.""" return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) @property diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 6eb67f7cbd8..590d5a8f1ce 100644 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -197,7 +197,7 @@ class BrSensor(Entity): def uid(self, coordinates): """Generate a unique id using coordinates and sensor type.""" - # The combination of the location, name an sensor type is unique + # The combination of the location, name and sensor type is unique return "%2.6f%2.6f%s" % (coordinates[CONF_LATITUDE], coordinates[CONF_LONGITUDE], self.type) diff --git a/homeassistant/components/sensor/hive.py b/homeassistant/components/sensor/hive.py index 8f8ce2d1681..82816c83404 100644 --- a/homeassistant/components/sensor/hive.py +++ b/homeassistant/components/sensor/hive.py @@ -70,7 +70,7 @@ class HiveSensorEntity(Entity): return DEVICETYPE_ICONS.get(self.device_type) def update(self): - """Update all Node data frome Hive.""" + """Update all Node data from Hive.""" if self.session.core.update_data(self.node_id): for entity in self.session.entities: entity.handle_update(self.data_updatesource) diff --git a/homeassistant/components/sensor/statistics.py b/homeassistant/components/sensor/statistics.py index 7b2ae537d4b..a77509c18d4 100644 --- a/homeassistant/components/sensor/statistics.py +++ b/homeassistant/components/sensor/statistics.py @@ -156,7 +156,7 @@ class StatisticsSensor(Entity): ATTR_CHANGE: self.change, ATTR_AVERAGE_CHANGE: self.average_change, } - # Only return min/max age if we have a age span + # Only return min/max age if we have an age span if self._max_age: state.update({ ATTR_MAX_AGE: self.max_age, diff --git a/homeassistant/components/switch/tahoma.py b/homeassistant/components/switch/tahoma.py index 339a0c39386..aa3554a494c 100644 --- a/homeassistant/components/switch/tahoma.py +++ b/homeassistant/components/switch/tahoma.py @@ -1,7 +1,7 @@ """ Support for Tahoma Switch - those are push buttons for garage door etc. -Those buttons are implemented as switchs that are never on. They only +Those buttons are implemented as switches that are never on. They only receive the turn_on action, perform the relay click, and stay in OFF state For more details about this platform, please refer to the documentation at @@ -19,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_devices, discovery_info=None): - """Set up Tahoma switchs.""" + """Set up Tahoma switches.""" controller = hass.data[TAHOMA_DOMAIN]['controller'] devices = [] for switch in hass.data[TAHOMA_DOMAIN]['devices']['switch']: diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index da5dac2d81b..577d2f2175d 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -96,7 +96,7 @@ async def test_switch_set_state(hass): @pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) async def test_no_alarm_code(hass, config): - """Test accessory if security_system doesn't require a alarm_code.""" + """Test accessory if security_system doesn't require an alarm_code.""" entity_id = 'alarm_control_panel.test' hass.states.async_set(entity_id, None) diff --git a/tests/components/media_player/test_blackbird.py b/tests/components/media_player/test_blackbird.py index eea6295b79e..7c85775949c 100644 --- a/tests/components/media_player/test_blackbird.py +++ b/tests/components/media_player/test_blackbird.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player.blackbird import ( class AttrDict(dict): - """Helper clas for mocking attributes.""" + """Helper class for mocking attributes.""" def __setattr__(self, name, value): """Set attribute.""" diff --git a/tests/components/sensor/test_sigfox.py b/tests/components/sensor/test_sigfox.py index dcdeef56b98..569fab584ad 100644 --- a/tests/components/sensor/test_sigfox.py +++ b/tests/components/sensor/test_sigfox.py @@ -38,7 +38,7 @@ class TestSigfoxSensor(unittest.TestCase): self.hass.stop() def test_invalid_credentials(self): - """Test for a invalid credentials.""" + """Test for invalid credentials.""" with requests_mock.Mocker() as mock_req: url = re.compile(API_URL + 'devicetypes') mock_req.get(url, text='{}', status_code=401) @@ -47,7 +47,7 @@ class TestSigfoxSensor(unittest.TestCase): assert len(self.hass.states.entity_ids()) == 0 def test_valid_credentials(self): - """Test for a valid credentials.""" + """Test for valid credentials.""" with requests_mock.Mocker() as mock_req: url1 = re.compile(API_URL + 'devicetypes') mock_req.get(url1, text='{"data":[{"id":"fake_type"}]}', diff --git a/tests/components/test_folder_watcher.py b/tests/components/test_folder_watcher.py index 16ec7a58a02..b5ac9cca9d9 100644 --- a/tests/components/test_folder_watcher.py +++ b/tests/components/test_folder_watcher.py @@ -8,7 +8,7 @@ from tests.common import MockDependency async def test_invalid_path_setup(hass): - """Test that a invalid path is not setup.""" + """Test that an invalid path is not setup.""" assert not await async_setup_component( hass, folder_watcher.DOMAIN, { folder_watcher.DOMAIN: { diff --git a/tests/components/test_prometheus.py b/tests/components/test_prometheus.py index 6cc0e4fcada..e336a28eb03 100644 --- a/tests/components/test_prometheus.py +++ b/tests/components/test_prometheus.py @@ -8,7 +8,7 @@ import homeassistant.components.prometheus as prometheus @pytest.fixture def prometheus_client(loop, hass, aiohttp_client): - """Initialize a aiohttp_client with Prometheus component.""" + """Initialize an aiohttp_client with Prometheus component.""" assert loop.run_until_complete(async_setup_component( hass, prometheus.DOMAIN, diff --git a/tests/test_core.py b/tests/test_core.py index 1fcd9416f36..4abce180093 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -375,7 +375,7 @@ class TestEventBus(unittest.TestCase): self.assertEqual(1, len(runs)) def test_thread_event_listener(self): - """Test a event listener listeners.""" + """Test thread event listener.""" thread_calls = [] def thread_listener(event): @@ -387,7 +387,7 @@ class TestEventBus(unittest.TestCase): assert len(thread_calls) == 1 def test_callback_event_listener(self): - """Test a event listener listeners.""" + """Test callback event listener.""" callback_calls = [] @ha.callback @@ -400,7 +400,7 @@ class TestEventBus(unittest.TestCase): assert len(callback_calls) == 1 def test_coroutine_event_listener(self): - """Test a event listener listeners.""" + """Test coroutine event listener.""" coroutine_calls = [] @asyncio.coroutine From 4d63baf705750de3a17bec4429db47338d2b5445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 12:11:55 +0200 Subject: [PATCH 063/144] Invoke pytest instead of py.test per upstream recommendation, #dropthedot (#14434) http://blog.pytest.org/2016/whats-new-in-pytest-30/ https://twitter.com/hashtag/dropthedot --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d4bea81a2f5..8b034346475 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ setenv = whitelist_externals = /usr/bin/env install_command = /usr/bin/env LANG=C.UTF-8 pip install {opts} {packages} commands = - py.test --timeout=9 --duration=10 --cov --cov-report= {posargs} + pytest --timeout=9 --duration=10 --cov --cov-report= {posargs} deps = -r{toxinidir}/requirements_test_all.txt -c{toxinidir}/homeassistant/package_constraints.txt From e5d714ef528bff1a874cd17ecc3d1e1fd3043724 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 May 2018 14:41:42 +0200 Subject: [PATCH 064/144] Fix fan service description (#14423) --- homeassistant/components/fan/services.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index a74f67b83fb..039cc33f748 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -51,8 +51,8 @@ set_direction: description: Name(s) of the entities to toggle example: 'fan.living_room' direction: - description: The direction to rotate - example: 'left' + description: The direction to rotate. Either 'forward' or 'reverse' + example: 'forward' dyson_set_night_mode: description: Set the fan in night mode. From 146a9492ecbf1bbf4184a9945447182b2847bd7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 May 2018 17:56:42 +0200 Subject: [PATCH 065/144] Clean up some Python 3.4 remnants (#14433) --- homeassistant/components/system_log/__init__.py | 6 +----- tests/components/camera/test_local_file.py | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 5994184d815..2a2a19aa2f5 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -126,11 +126,7 @@ class LogErrorHandler(logging.Handler): if record.levelno >= logging.WARN: stack = [] if not record.exc_info: - try: - stack = [f for f, _, _, _ in traceback.extract_stack()] - except ValueError: - # On Python 3.4 under py.test getting the stack might fail. - pass + stack = [f for f, _, _, _ in traceback.extract_stack()] entry = self._create_entry(record, stack) self.records.appendleft(entry) diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 40517ea1298..0a57512aabd 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -2,10 +2,6 @@ import asyncio from unittest import mock -# Using third party package because of a bug reading binary data in Python 3.4 -# https://bugs.python.org/issue23004 -from mock_open import MockOpen - from homeassistant.components.camera import DOMAIN from homeassistant.components.camera.local_file import ( SERVICE_UPDATE_FILE_PATH) @@ -30,7 +26,7 @@ def test_loading_file(hass, aiohttp_client): client = yield from aiohttp_client(hass.http.app) - m_open = MockOpen(read_data=b'hello') + m_open = mock.mock_open(read_data=b'hello') with mock.patch( 'homeassistant.components.camera.local_file.open', m_open, create=True @@ -90,7 +86,7 @@ def test_camera_content_type(hass, aiohttp_client): client = yield from aiohttp_client(hass.http.app) image = 'hello' - m_open = MockOpen(read_data=image.encode()) + m_open = mock.mock_open(read_data=image.encode()) with mock.patch('homeassistant.components.camera.local_file.open', m_open, create=True): resp_1 = yield from client.get('/api/camera_proxy/camera.test_jpg') From b904a4e7709dec3618ff6077979cfe62c5c2c5e8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 13 May 2018 17:57:52 +0200 Subject: [PATCH 066/144] Remove universal wheel setting (#14445) * Home assistant should not build a universal wheel since we don't support Python 2. --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index d6dfdfe0ea5..8b17da455dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[wheel] -universal = 1 - [tool:pytest] testpaths = tests norecursedirs = .git testing_config From 3ec56d55c5f56a857f387047a38de31e9988288d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 17:58:18 +0200 Subject: [PATCH 067/144] Upgrade requests_mock to 1.5 (#14444) --- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9dcccd0d1da..0a4a0bcb5b0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -14,4 +14,4 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout>=1.2.1 pytest==3.4.2 -requests_mock==1.4 +requests_mock==1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2157389a16..b91a6500b07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -15,7 +15,7 @@ pytest-cov==2.5.1 pytest-sugar==0.9.1 pytest-timeout>=1.2.1 pytest==3.4.2 -requests_mock==1.4 +requests_mock==1.5 # homeassistant.components.homekit From e0bc894cbba67282a85de8400fe813d41342f3af Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 17:58:57 +0200 Subject: [PATCH 068/144] Upgrade pyota to 2.0.5 (#14442) * Use constants * Upgrade pyota to 2.0.5 --- homeassistant/components/iota.py | 2 +- homeassistant/components/sensor/iota.py | 26 ++++++++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/iota.py b/homeassistant/components/iota.py index 442be6e22e7..ada70f8a9eb 100644 --- a/homeassistant/components/iota.py +++ b/homeassistant/components/iota.py @@ -13,7 +13,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['pyota==2.0.4'] +REQUIREMENTS = ['pyota==2.0.5'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/iota.py b/homeassistant/components/sensor/iota.py index c973fa83148..2e3e58a18f3 100644 --- a/homeassistant/components/sensor/iota.py +++ b/homeassistant/components/sensor/iota.py @@ -7,10 +7,18 @@ https://home-assistant.io/components/iota import logging from datetime import timedelta -from homeassistant.components.iota import IotaDevice +from homeassistant.components.iota import IotaDevice, CONF_WALLETS +from homeassistant.const import CONF_NAME _LOGGER = logging.getLogger(__name__) +ATTR_TESTNET = 'testnet' +ATTR_URL = 'url' + +CONF_IRI = 'iri' +CONF_SEED = 'seed' +CONF_TESTNET = 'testnet' + DEPENDENCIES = ['iota'] SCAN_INTERVAL = timedelta(minutes=3) @@ -21,7 +29,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Add sensors for wallet balance iota_config = discovery_info sensors = [IotaBalanceSensor(wallet, iota_config) - for wallet in iota_config['wallets']] + for wallet in iota_config[CONF_WALLETS]] # Add sensor for node information sensors.append(IotaNodeSensor(iota_config=iota_config)) @@ -34,10 +42,9 @@ class IotaBalanceSensor(IotaDevice): def __init__(self, wallet_config, iota_config): """Initialize the sensor.""" - super().__init__(name=wallet_config['name'], - seed=wallet_config['seed'], - iri=iota_config['iri'], - is_testnet=iota_config['testnet']) + super().__init__( + name=wallet_config[CONF_NAME], seed=wallet_config[CONF_SEED], + iri=iota_config[CONF_IRI], is_testnet=iota_config[CONF_TESTNET]) self._state = None @property @@ -65,10 +72,11 @@ class IotaNodeSensor(IotaDevice): def __init__(self, iota_config): """Initialize the sensor.""" - super().__init__(name='Node Info', seed=None, iri=iota_config['iri'], - is_testnet=iota_config['testnet']) + super().__init__( + name='Node Info', seed=None, iri=iota_config[CONF_IRI], + is_testnet=iota_config[CONF_TESTNET]) self._state = None - self._attr = {'url': self.iri, 'testnet': self.is_testnet} + self._attr = {ATTR_URL: self.iri, ATTR_TESTNET: self.is_testnet} @property def name(self): diff --git a/requirements_all.txt b/requirements_all.txt index d81dcc8280d..281df00312e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -891,7 +891,7 @@ pynut2==2.1.2 pynx584==0.4 # homeassistant.components.iota -pyota==2.0.4 +pyota==2.0.5 # homeassistant.components.sensor.otp pyotp==2.2.6 From a5bff4cd8dd9e3a3f4659f2f570b9f89a6e6a222 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 17:59:25 +0200 Subject: [PATCH 069/144] Upgrade python-telegram-bot to 10.1.0 (#14441) --- homeassistant/components/telegram_bot/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index af0fe5bd572..b9329a46b72 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError from homeassistant.setup import async_prepare_setup_platform -REQUIREMENTS = ['python-telegram-bot==10.0.2'] +REQUIREMENTS = ['python-telegram-bot==10.1.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 281df00312e..d2b77726729 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1030,7 +1030,7 @@ python-synology==0.1.0 python-tado==0.2.3 # homeassistant.components.telegram_bot -python-telegram-bot==10.0.2 +python-telegram-bot==10.1.0 # homeassistant.components.sensor.twitch python-twitch==1.3.0 From a750f8444ef355acac12dbad11e645fbb2c8e059 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 18:00:08 +0200 Subject: [PATCH 070/144] Upgrade Sphinx to 1.7.4 (#14439) --- requirements_docs.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_docs.txt b/requirements_docs.txt index bb0d30462ce..5ef38e1537e 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==1.7.1 -sphinx-autodoc-typehints==1.2.5 +Sphinx==1.7.4 +sphinx-autodoc-typehints==1.3.0 sphinx-autodoc-annotation==1.0.post1 From cb709931e469ab3b1d276055fbe9c1af1190887e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 18:00:37 +0200 Subject: [PATCH 071/144] Upgrade youtube_dl to 2018.05.09 (#14438) --- homeassistant/components/media_extractor.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor.py b/homeassistant/components/media_extractor.py index fe6ebe8e618..89cc296111b 100644 --- a/homeassistant/components/media_extractor.py +++ b/homeassistant/components/media_extractor.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( SERVICE_PLAY_MEDIA) from homeassistant.helpers import config_validation as cv -REQUIREMENTS = ['youtube_dl==2018.04.25'] +REQUIREMENTS = ['youtube_dl==2018.05.09'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index d2b77726729..ad64e4ae736 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1367,7 +1367,7 @@ yeelight==0.4.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2018.04.25 +youtube_dl==2018.05.09 # homeassistant.components.light.zengge zengge==0.2 From 391e3196ea439bf6ef6503bed9982d8871e72ea4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 May 2018 18:01:10 +0200 Subject: [PATCH 072/144] Upgrade distro to 1.3.0 (#14436) --- homeassistant/components/updater.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/updater.py b/homeassistant/components/updater.py index 9ccf280ed04..0cb22bd98dc 100644 --- a/homeassistant/components/updater.py +++ b/homeassistant/components/updater.py @@ -25,7 +25,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -REQUIREMENTS = ['distro==1.2.0'] +REQUIREMENTS = ['distro==1.3.0'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index ad64e4ae736..e293814e661 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -258,7 +258,7 @@ discogs_client==2.2.1 discord.py==0.16.12 # homeassistant.components.updater -distro==1.2.0 +distro==1.3.0 # homeassistant.components.switch.digitalloggers dlipower==0.7.165 From 8ae3caa2928d5b6ae9bb73f4b9fbd288fec5991d Mon Sep 17 00:00:00 2001 From: Philip Rosenberg-Watt Date: Sun, 13 May 2018 10:04:21 -0600 Subject: [PATCH 073/144] Add priority and cycles to LaMetric (#14414) * Add priority and cycles to LaMetric Priority can be "info", "warning" (default), or "critical" and cycles is the number of times the message is displayed. If cycles is set to 0 we get a persistent notification that has to be dismissed manually. * Fix for schema and style * Fix for style --- homeassistant/components/notify/lametric.py | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notify/lametric.py b/homeassistant/components/notify/lametric.py index 895ffd9db10..f6c3e152b0a 100644 --- a/homeassistant/components/notify/lametric.py +++ b/homeassistant/components/notify/lametric.py @@ -23,11 +23,16 @@ _LOGGER = logging.getLogger(__name__) CONF_LIFETIME = "lifetime" CONF_CYCLES = "cycles" +CONF_PRIORITY = "priority" + +AVAILABLE_PRIORITIES = ["info", "warning", "critical"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_ICON, default="i555"): cv.string, vol.Optional(CONF_LIFETIME, default=10): cv.positive_int, vol.Optional(CONF_CYCLES, default=1): cv.positive_int, + vol.Optional(CONF_PRIORITY, default="warning"): + vol.In(AVAILABLE_PRIORITIES) }) @@ -38,18 +43,20 @@ def get_service(hass, config, discovery_info=None): return LaMetricNotificationService(hlmn, config[CONF_ICON], config[CONF_LIFETIME] * 1000, - config[CONF_CYCLES]) + config[CONF_CYCLES], + config[CONF_PRIORITY]) class LaMetricNotificationService(BaseNotificationService): """Implement the notification service for LaMetric.""" - def __init__(self, hasslametricmanager, icon, lifetime, cycles): + def __init__(self, hasslametricmanager, icon, lifetime, cycles, priority): """Initialize the service.""" self.hasslametricmanager = hasslametricmanager self._icon = icon self._lifetime = lifetime self._cycles = cycles + self._priority = priority self._devices = [] # pylint: disable=broad-except @@ -64,6 +71,7 @@ class LaMetricNotificationService(BaseNotificationService): icon = self._icon cycles = self._cycles sound = None + priority = self._priority # Additional data? if data is not None: @@ -78,6 +86,14 @@ class LaMetricNotificationService(BaseNotificationService): except AssertionError: _LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) + if "cycles" in data: + cycles = data['cycles'] + if "priority" in data: + if data['priority'] in AVAILABLE_PRIORITIES: + priority = data['priority'] + else: + _LOGGER.warning("Priority %s invalid, using default %s", + data['priority'], priority) text_frame = SimpleFrame(icon, message) _LOGGER.debug("Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", @@ -100,7 +116,8 @@ class LaMetricNotificationService(BaseNotificationService): if targets is None or dev["name"] in targets: try: lmn.set_device(dev) - lmn.send_notification(model, lifetime=self._lifetime) + lmn.send_notification(model, lifetime=self._lifetime, + priority=priority) _LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) except OSError: From 6b9c65c9ce8523b7f28a4f7b0758c945a75d8668 Mon Sep 17 00:00:00 2001 From: Johann Kellerman Date: Mon, 14 May 2018 08:40:25 +0200 Subject: [PATCH 074/144] Allow qwikswitch sensors as part of devices (#14454) --- homeassistant/components/qwikswitch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qwikswitch.py b/homeassistant/components/qwikswitch.py index f26318fa7a9..63e30a9491e 100644 --- a/homeassistant/components/qwikswitch.py +++ b/homeassistant/components/qwikswitch.py @@ -150,8 +150,10 @@ async def async_setup(hass, config): comps = {'switch': [], 'light': [], 'sensor': [], 'binary_sensor': []} try: + sensor_ids = [] for sens in sensors: _, _type = SENSORS[sens['type']] + sensor_ids.append(sens['id']) if _type is bool: comps['binary_sensor'].append(sens) continue @@ -192,9 +194,7 @@ async def async_setup(hass, config): 'qwikswitch.button.{}'.format(qspacket[QS_ID]), qspacket) return - if qspacket[QS_ID] not in qsusb.devices: - # Not a standard device in, component can handle packet - # i.e. sensors + if qspacket[QS_ID] in sensor_ids: _LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket) hass.helpers.dispatcher.async_dispatcher_send( qspacket[QS_ID], qspacket) From c06351f2a9038e4e128ff89c5392f7c2cfc965ae Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 May 2018 08:41:17 +0200 Subject: [PATCH 075/144] Bump requirement to pydeconz v38 (#14452) --- homeassistant/components/deconz/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 47573be6add..bbab4029d7e 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -22,7 +22,7 @@ from .const import ( CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==37'] +REQUIREMENTS = ['pydeconz==38'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ diff --git a/requirements_all.txt b/requirements_all.txt index e293814e661..a1158d2af3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==37 +pydeconz==38 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b91a6500b07..d3e2e16dc57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==37 +pydeconz==38 # homeassistant.components.zwave pydispatcher==2.0.5 From fb501282ccb61ad89088619eb12613a4d542a759 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 May 2018 09:13:59 +0200 Subject: [PATCH 076/144] Add SpaceAPI support (#14204) * Add SpaceAPI support * Changes according PR comments * Add tests * Remove print * Minor changes --- homeassistant/components/spaceapi.py | 175 +++++++++++++++++++++++++++ tests/components/test_spaceapi.py | 113 +++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 homeassistant/components/spaceapi.py create mode 100644 tests/components/test_spaceapi.py diff --git a/homeassistant/components/spaceapi.py b/homeassistant/components/spaceapi.py new file mode 100644 index 00000000000..eaf1508071a --- /dev/null +++ b/homeassistant/components/spaceapi.py @@ -0,0 +1,175 @@ +""" +Support for the SpaceAPI. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/spaceapi/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_ICON, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, + ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, CONF_ADDRESS, CONF_EMAIL, + CONF_ENTITY_ID, CONF_SENSORS, CONF_STATE, CONF_URL) +import homeassistant.core as ha +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util + +_LOGGER = logging.getLogger(__name__) + +ATTR_ADDRESS = 'address' +ATTR_API = 'api' +ATTR_CLOSE = 'close' +ATTR_CONTACT = 'contact' +ATTR_ISSUE_REPORT_CHANNELS = 'issue_report_channels' +ATTR_LASTCHANGE = 'lastchange' +ATTR_LOGO = 'logo' +ATTR_NAME = 'name' +ATTR_OPEN = 'open' +ATTR_SENSORS = 'sensors' +ATTR_SPACE = 'space' +ATTR_UNIT = 'unit' +ATTR_URL = 'url' +ATTR_VALUE = 'value' + +CONF_CONTACT = 'contact' +CONF_HUMIDITY = 'humidity' +CONF_ICON_CLOSED = 'icon_closed' +CONF_ICON_OPEN = 'icon_open' +CONF_ICONS = 'icons' +CONF_IRC = 'irc' +CONF_ISSUE_REPORT_CHANNELS = 'issue_report_channels' +CONF_LOCATION = 'location' +CONF_LOGO = 'logo' +CONF_MAILING_LIST = 'mailing_list' +CONF_PHONE = 'phone' +CONF_SPACE = 'space' +CONF_TEMPERATURE = 'temperature' +CONF_TWITTER = 'twitter' + +DATA_SPACEAPI = 'data_spaceapi' +DEPENDENCIES = ['http'] +DOMAIN = 'spaceapi' + +ISSUE_REPORT_CHANNELS = [CONF_EMAIL, CONF_IRC, CONF_MAILING_LIST, CONF_TWITTER] + +SENSOR_TYPES = [CONF_HUMIDITY, CONF_TEMPERATURE] +SPACEAPI_VERSION = 0.13 + +URL_API_SPACEAPI = '/api/spaceapi' + +LOCATION_SCHEMA = vol.Schema({ + vol.Optional(CONF_ADDRESS): cv.string, +}, required=True) + +CONTACT_SCHEMA = vol.Schema({ + vol.Optional(CONF_EMAIL): cv.string, + vol.Optional(CONF_IRC): cv.string, + vol.Optional(CONF_MAILING_LIST): cv.string, + vol.Optional(CONF_PHONE): cv.string, + vol.Optional(CONF_TWITTER): cv.string, +}, required=False) + +STATE_SCHEMA = vol.Schema({ + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Inclusive(CONF_ICON_CLOSED, CONF_ICONS): cv.url, + vol.Inclusive(CONF_ICON_OPEN, CONF_ICONS): cv.url, +}, required=False) + +SENSOR_SCHEMA = vol.Schema( + {vol.In(SENSOR_TYPES): [cv.entity_id]} +) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CONTACT): CONTACT_SCHEMA, + vol.Required(CONF_ISSUE_REPORT_CHANNELS): + vol.All(cv.ensure_list, [vol.In(ISSUE_REPORT_CHANNELS)]), + vol.Required(CONF_LOCATION): LOCATION_SCHEMA, + vol.Required(CONF_LOGO): cv.url, + vol.Required(CONF_SPACE): cv.string, + vol.Required(CONF_STATE): STATE_SCHEMA, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_SENSORS): SENSOR_SCHEMA, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Register the SpaceAPI with the HTTP interface.""" + hass.data[DATA_SPACEAPI] = config[DOMAIN] + hass.http.register_view(APISpaceApiView) + + return True + + +class APISpaceApiView(HomeAssistantView): + """View to provide details according to the SpaceAPI.""" + + url = URL_API_SPACEAPI + name = 'api:spaceapi' + + @ha.callback + def get(self, request): + """Get SpaceAPI data.""" + hass = request.app['hass'] + spaceapi = dict(hass.data[DATA_SPACEAPI]) + is_sensors = spaceapi.get('sensors') + + location = { + ATTR_ADDRESS: spaceapi[ATTR_LOCATION][CONF_ADDRESS], + ATTR_LATITUDE: hass.config.latitude, + ATTR_LONGITUDE: hass.config.longitude, + } + + state_entity = spaceapi['state'][ATTR_ENTITY_ID] + space_state = hass.states.get(state_entity) + + if space_state is not None: + state = { + ATTR_OPEN: False if space_state.state == 'off' else True, + ATTR_LASTCHANGE: + dt_util.as_timestamp(space_state.last_updated), + } + else: + state = {ATTR_OPEN: 'null', ATTR_LASTCHANGE: 0} + + try: + state[ATTR_ICON] = { + ATTR_OPEN: spaceapi['state'][CONF_ICON_OPEN], + ATTR_CLOSE: spaceapi['state'][CONF_ICON_CLOSED], + } + except KeyError: + pass + + data = { + ATTR_API: SPACEAPI_VERSION, + ATTR_CONTACT: spaceapi[CONF_CONTACT], + ATTR_ISSUE_REPORT_CHANNELS: spaceapi[CONF_ISSUE_REPORT_CHANNELS], + ATTR_LOCATION: location, + ATTR_LOGO: spaceapi[CONF_LOGO], + ATTR_SPACE: spaceapi[CONF_SPACE], + ATTR_STATE: state, + ATTR_URL: spaceapi[CONF_URL], + } + + if is_sensors is not None: + sensors = {} + for sensor_type in is_sensors: + sensors[sensor_type] = [] + for sensor in spaceapi['sensors'][sensor_type]: + sensor_state = hass.states.get(sensor) + unit = sensor_state.attributes[ATTR_UNIT_OF_MEASUREMENT] + value = sensor_state.state + sensor_data = { + ATTR_LOCATION: spaceapi[CONF_SPACE], + ATTR_NAME: sensor_state.name, + ATTR_UNIT: unit, + ATTR_VALUE: value, + } + sensors[sensor_type].append(sensor_data) + data[ATTR_SENSORS] = sensors + + return self.json(data) diff --git a/tests/components/test_spaceapi.py b/tests/components/test_spaceapi.py new file mode 100644 index 00000000000..e7e7d158a31 --- /dev/null +++ b/tests/components/test_spaceapi.py @@ -0,0 +1,113 @@ +"""The tests for the Home Assistant SpaceAPI component.""" +# pylint: disable=protected-access +from unittest.mock import patch + +import pytest +from tests.common import mock_coro + +from homeassistant.components.spaceapi import ( + DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI) +from homeassistant.setup import async_setup_component + +CONFIG = { + DOMAIN: { + 'space': 'Home', + 'logo': 'https://home-assistant.io/logo.png', + 'url': 'https://home-assistant.io', + 'location': {'address': 'In your Home'}, + 'contact': {'email': 'hello@home-assistant.io'}, + 'issue_report_channels': ['email'], + 'state': { + 'entity_id': 'test.test_door', + 'icon_open': 'https://home-assistant.io/open.png', + 'icon_closed': 'https://home-assistant.io/close.png', + }, + 'sensors': { + 'temperature': ['test.temp1', 'test.temp2'], + 'humidity': ['test.hum1'], + } + } +} + +SENSOR_OUTPUT = { + 'temperature': [ + { + 'location': 'Home', + 'name': 'temp1', + 'unit': '°C', + 'value': '25' + }, + { + 'location': 'Home', + 'name': 'temp2', + 'unit': '°C', + 'value': '23' + }, + ], + 'humidity': [ + { + 'location': 'Home', + 'name': 'hum1', + 'unit': '%', + 'value': '88' + }, + ] +} + + +@pytest.fixture +def mock_client(hass, aiohttp_client): + """Start the Home Assistant HTTP component.""" + with patch('homeassistant.components.spaceapi', + return_value=mock_coro(True)): + hass.loop.run_until_complete( + async_setup_component(hass, 'spaceapi', CONFIG)) + + hass.states.async_set('test.temp1', 25, + attributes={'unit_of_measurement': '°C'}) + hass.states.async_set('test.temp2', 23, + attributes={'unit_of_measurement': '°C'}) + hass.states.async_set('test.hum1', 88, + attributes={'unit_of_measurement': '%'}) + + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + + +async def test_spaceapi_get(hass, mock_client): + """Test response after start-up Home Assistant.""" + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + + assert data['api'] == SPACEAPI_VERSION + assert data['space'] == 'Home' + assert data['contact']['email'] == 'hello@home-assistant.io' + assert data['location']['address'] == 'In your Home' + assert data['location']['latitude'] == 32.87336 + assert data['location']['longitude'] == -117.22743 + assert data['state']['open'] == 'null' + assert data['state']['icon']['open'] == \ + 'https://home-assistant.io/open.png' + assert data['state']['icon']['close'] == \ + 'https://home-assistant.io/close.png' + + +async def test_spaceapi_state_get(hass, mock_client): + """Test response if the state entity was set.""" + hass.states.async_set('test.test_door', True) + + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + assert data['state']['open'] == bool(1) + + +async def test_spaceapi_sensors_get(hass, mock_client): + """Test the response for the sensors.""" + resp = await mock_client.get(URL_API_SPACEAPI) + assert resp.status == 200 + + data = await resp.json() + assert data['sensors'] == SENSOR_OUTPUT From 954e4796b84082a9654cdaee9006fc22dd60f0e0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 May 2018 13:05:52 +0200 Subject: [PATCH 077/144] Use ATTR_NAME from const.py (#14450) --- .../components/binary_sensor/rfxtrx.py | 5 +- .../components/device_tracker/__init__.py | 3 +- homeassistant/components/group/__init__.py | 4 +- homeassistant/components/hassio/__init__.py | 4 +- .../components/homematic/__init__.py | 11 ++-- .../components/image_processing/__init__.py | 13 ++-- .../image_processing/microsoft_face_detect.py | 8 +-- .../microsoft_face_identify.py | 9 +-- homeassistant/components/lock/wink.py | 4 +- homeassistant/components/logbook.py | 50 ++++++++-------- homeassistant/components/microsoft_face.py | 33 +++++------ homeassistant/components/notify/apns.py | 11 ++-- homeassistant/components/rfxtrx.py | 18 ++---- homeassistant/components/sensor/citybikes.py | 46 +++++++-------- .../components/sensor/coinmarketcap.py | 5 +- .../components/sensor/comed_hourly_pricing.py | 20 +++---- .../components/sensor/linux_battery.py | 5 +- homeassistant/components/sensor/qnap.py | 3 +- homeassistant/components/sensor/rfxtrx.py | 6 +- homeassistant/components/sensor/tado.py | 13 ++-- homeassistant/components/sensor/wsdot.py | 59 +++++++++---------- homeassistant/components/switch/rfxtrx.py | 4 +- homeassistant/components/switch/rpi_pfio.py | 3 +- homeassistant/components/wink/__init__.py | 6 +- homeassistant/const.py | 3 + tests/components/sensor/test_wsdot.py | 11 ++-- 26 files changed, 163 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/binary_sensor/rfxtrx.py b/homeassistant/components/binary_sensor/rfxtrx.py index 8c026131fd3..6ac604a4f1e 100644 --- a/homeassistant/components/binary_sensor/rfxtrx.py +++ b/homeassistant/components/binary_sensor/rfxtrx.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, BinarySensorDevice) + DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, BinarySensorDevice) from homeassistant.components.rfxtrx import ( ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY) @@ -29,8 +29,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): - DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, vol.Optional(CONF_OFF_DELAY): vol.Any(cv.time_period, cv.positive_timedelta), diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index e1dd52a28ea..580c0272e46 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_NAME, CONF_MAC, DEVICE_DEFAULT_NAME, STATE_HOME, STATE_NOT_HOME, ATTR_ENTITY_ID, - CONF_ICON, ATTR_ICON) + CONF_ICON, ATTR_ICON, ATTR_NAME) _LOGGER = logging.getLogger(__name__) @@ -71,7 +71,6 @@ ATTR_GPS = 'gps' ATTR_HOST_NAME = 'host_name' ATTR_LOCATION_NAME = 'location_name' ATTR_MAC = 'mac' -ATTR_NAME = 'name' ATTR_SOURCE_TYPE = 'source_type' ATTR_CONSIDER_HOME = 'consider_home' diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index f70a2d29351..a33e91f3aa9 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED, STATE_UNLOCKED, STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, SERVICE_RELOAD) + ATTR_ASSUMED_STATE, SERVICE_RELOAD, ATTR_NAME, ATTR_ICON) from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -35,8 +35,6 @@ ATTR_ADD_ENTITIES = 'add_entities' ATTR_AUTO = 'auto' ATTR_CONTROL = 'control' ATTR_ENTITIES = 'entities' -ATTR_ICON = 'icon' -ATTR_NAME = 'name' ATTR_OBJECT_ID = 'object_id' ATTR_ORDER = 'order' ATTR_VIEW = 'view' diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 87251a2745c..aa24cc61af3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -13,12 +13,13 @@ import voluptuous as vol from homeassistant.components import SERVICE_CHECK_CONFIG from homeassistant.const import ( - SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) + ATTR_NAME, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP) from homeassistant.core import DOMAIN as HASS_DOMAIN from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow + from .handler import HassIO from .http import HassIOView @@ -47,7 +48,6 @@ ATTR_SNAPSHOT = 'snapshot' ATTR_ADDONS = 'addons' ATTR_FOLDERS = 'folders' ATTR_HOMEASSISTANT = 'homeassistant' -ATTR_NAME = 'name' ATTR_PASSWORD = 'password' SCHEMA_NO_DATA = vol.Schema({}) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 0291cc28fed..aa19875d43a 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -13,17 +13,19 @@ import socket import voluptuous as vol from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, - CONF_HOSTS, CONF_HOST, ATTR_ENTITY_ID, STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_NAME, CONF_HOST, CONF_HOSTS, CONF_PASSWORD, + CONF_PLATFORM, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN) from homeassistant.helpers import discovery -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.loader import bind_hass REQUIREMENTS = ['pyhomematic==0.1.42'] -DOMAIN = 'homematic' + _LOGGER = logging.getLogger(__name__) +DOMAIN = 'homematic' + SCAN_INTERVAL_HUB = timedelta(seconds=300) SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) @@ -38,7 +40,6 @@ DISCOVER_LOCKS = 'homematic.locks' ATTR_DISCOVER_DEVICES = 'devices' ATTR_PARAM = 'param' ATTR_CHANNEL = 'channel' -ATTR_NAME = 'name' ATTR_ADDRESS = 'address' ATTR_VALUE = 'value' ATTR_INTERFACE = 'interface' diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index c6100ff701d..29f26cc84e6 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -10,14 +10,14 @@ import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITY_ID) + ATTR_ENTITY_ID, ATTR_NAME, CONF_ENTITY_ID, CONF_NAME) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -42,7 +42,6 @@ ATTR_CONFIDENCE = 'confidence' ATTR_FACES = 'faces' ATTR_GENDER = 'gender' ATTR_GLASSES = 'glasses' -ATTR_NAME = 'name' ATTR_MOTION = 'motion' ATTR_TOTAL_FACES = 'total_faces' @@ -60,7 +59,7 @@ SOURCE_SCHEMA = vol.Schema({ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ vol.Optional(CONF_SOURCE): vol.All(cv.ensure_list, [SOURCE_SCHEMA]), vol.Optional(CONF_CONFIDENCE, default=DEFAULT_CONFIDENCE): - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), }) SERVICE_SCAN_SCHEMA = vol.Schema({ @@ -77,7 +76,7 @@ def scan(hass, entity_id=None): @asyncio.coroutine def async_setup(hass, config): - """Set up image processing.""" + """Set up the image processing.""" component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) yield from component.async_setup(config) diff --git a/homeassistant/components/image_processing/microsoft_face_detect.py b/homeassistant/components/image_processing/microsoft_face_detect.py index cd1e341a218..bda0e1bc550 100644 --- a/homeassistant/components/image_processing/microsoft_face_detect.py +++ b/homeassistant/components/image_processing/microsoft_face_detect.py @@ -9,12 +9,12 @@ import logging import voluptuous as vol +from homeassistant.components.image_processing import ( + ATTR_AGE, ATTR_GENDER, ATTR_GLASSES, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, PLATFORM_SCHEMA, ImageProcessingFaceEntity) +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_AGE, ATTR_GENDER, - ATTR_GLASSES, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/image_processing/microsoft_face_identify.py b/homeassistant/components/image_processing/microsoft_face_identify.py index 32f02e1820e..8984f25cdf2 100644 --- a/homeassistant/components/image_processing/microsoft_face_identify.py +++ b/homeassistant/components/image_processing/microsoft_face_identify.py @@ -9,12 +9,13 @@ import logging import voluptuous as vol +from homeassistant.components.image_processing import ( + ATTR_CONFIDENCE, CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE, + PLATFORM_SCHEMA, ImageProcessingFaceEntity) +from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE +from homeassistant.const import ATTR_NAME from homeassistant.core import split_entity_id from homeassistant.exceptions import HomeAssistantError -from homeassistant.components.microsoft_face import DATA_MICROSOFT_FACE -from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, ImageProcessingFaceEntity, ATTR_NAME, - CONF_CONFIDENCE, CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, ATTR_CONFIDENCE) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['microsoft_face'] diff --git a/homeassistant/components/lock/wink.py b/homeassistant/components/lock/wink.py index a5cd18454df..1c42e427a00 100644 --- a/homeassistant/components/lock/wink.py +++ b/homeassistant/components/lock/wink.py @@ -11,7 +11,8 @@ import voluptuous as vol from homeassistant.components.lock import LockDevice from homeassistant.components.wink import DOMAIN, WinkDevice -from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_CODE, ATTR_ENTITY_ID, ATTR_NAME, STATE_UNKNOWN) import homeassistant.helpers.config_validation as cv DEPENDENCIES = ['wink'] @@ -28,7 +29,6 @@ SERVICE_ADD_KEY = 'wink_add_new_lock_key_code' ATTR_ENABLED = 'enabled' ATTR_SENSITIVITY = 'sensitivity' ATTR_MODE = 'mode' -ATTR_NAME = 'name' ALARM_SENSITIVITY_MAP = { 'low': 0.2, diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 8bab6fe0440..1ea0b586d33 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -4,44 +4,49 @@ Event parser and human readable log generator. For more details about this component, please refer to the documentation at https://home-assistant.io/components/logbook/ """ -import logging from datetime import timedelta from itertools import groupby +import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components import sun from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, - EVENT_LOGBOOK_ENTRY) -from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN - -DOMAIN = 'logbook' -DEPENDENCIES = ['recorder', 'frontend'] + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_HIDDEN, ATTR_NAME, CONF_EXCLUDE, + CONF_INCLUDE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, + EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, + STATE_OFF, STATE_ON) +from homeassistant.core import DOMAIN as HA_DOMAIN +from homeassistant.core import State, callback, split_entity_id +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) -CONF_EXCLUDE = 'exclude' -CONF_INCLUDE = 'include' -CONF_ENTITIES = 'entities' +ATTR_MESSAGE = 'message' + CONF_DOMAINS = 'domains' +CONF_ENTITIES = 'entities' +CONTINUOUS_DOMAINS = ['proximity', 'sensor'] + +DEPENDENCIES = ['recorder', 'frontend'] + +DOMAIN = 'logbook' + +GROUP_BY_MINUTES = 15 CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ CONF_EXCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) }), CONF_INCLUDE: vol.Schema({ vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, - [cv.string]) + vol.Optional(CONF_DOMAINS, default=[]): + vol.All(cv.ensure_list, [cv.string]) }) }), }, extra=vol.ALLOW_EXTRA) @@ -51,15 +56,6 @@ ALL_EVENT_TYPES = [ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP ] -GROUP_BY_MINUTES = 15 - -CONTINUOUS_DOMAINS = ['proximity', 'sensor'] - -ATTR_NAME = 'name' -ATTR_MESSAGE = 'message' -ATTR_DOMAIN = 'domain' -ATTR_ENTITY_ID = 'entity_id' - LOG_MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_MESSAGE): cv.template, diff --git a/homeassistant/components/microsoft_face.py b/homeassistant/components/microsoft_face.py index 7c167f93142..847f4131f43 100644 --- a/homeassistant/components/microsoft_face.py +++ b/homeassistant/components/microsoft_face.py @@ -1,5 +1,5 @@ """ -Support for microsoft face recognition. +Support for Microsoft face recognition. For more details about this component, please refer to the documentation at https://home-assistant.io/components/microsoft_face/ @@ -13,7 +13,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT +from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -22,28 +22,25 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -DOMAIN = 'microsoft_face' -DEPENDENCIES = ['camera'] - -FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" - -DATA_MICROSOFT_FACE = 'microsoft_face' +ATTR_CAMERA_ENTITY = 'camera_entity' +ATTR_GROUP = 'group' +ATTR_PERSON = 'person' CONF_AZURE_REGION = 'azure_region' +DATA_MICROSOFT_FACE = 'microsoft_face' +DEFAULT_TIMEOUT = 10 +DEPENDENCIES = ['camera'] +DOMAIN = 'microsoft_face' + +FACE_API_URL = "api.cognitive.microsoft.com/face/v1.0/{0}" + SERVICE_CREATE_GROUP = 'create_group' -SERVICE_DELETE_GROUP = 'delete_group' -SERVICE_TRAIN_GROUP = 'train_group' SERVICE_CREATE_PERSON = 'create_person' +SERVICE_DELETE_GROUP = 'delete_group' SERVICE_DELETE_PERSON = 'delete_person' SERVICE_FACE_PERSON = 'face_person' - -ATTR_GROUP = 'group' -ATTR_PERSON = 'person' -ATTR_CAMERA_ENTITY = 'camera_entity' -ATTR_NAME = 'name' - -DEFAULT_TIMEOUT = 10 +SERVICE_TRAIN_GROUP = 'train_group' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -111,7 +108,7 @@ def face_person(hass, group, person, camera_entity): @asyncio.coroutine def async_setup(hass, config): - """Set up microsoft face.""" + """Set up Microsoft Face.""" entities = {} face = MicrosoftFace( hass, diff --git a/homeassistant/components/notify/apns.py b/homeassistant/components/notify/apns.py index dcbd1ce1317..9cca81e1485 100644 --- a/homeassistant/components/notify/apns.py +++ b/homeassistant/components/notify/apns.py @@ -12,8 +12,8 @@ import voluptuous as vol from homeassistant.helpers.event import track_state_change from homeassistant.config import load_yaml_config_file from homeassistant.components.notify import ( - ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN) -from homeassistant.const import CONF_NAME, CONF_PLATFORM + ATTR_TARGET, ATTR_DATA, BaseNotificationService, DOMAIN, PLATFORM_SCHEMA) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, ATTR_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers import template as template_helper @@ -27,9 +27,8 @@ DEVICE_TRACKER_DOMAIN = 'device_tracker' SERVICE_REGISTER = 'apns_register' ATTR_PUSH_ID = 'push_id' -ATTR_NAME = 'name' -PLATFORM_SCHEMA = vol.Schema({ +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_PLATFORM): 'apns', vol.Required(CONF_NAME): cv.string, vol.Required(CONF_CERTFILE): cv.isfile, @@ -66,7 +65,7 @@ class ApnsDevice(object): """ def __init__(self, push_id, name, tracking_device_id=None, disabled=False): - """Initialize Apns Device.""" + """Initialize APNS Device.""" self.device_push_id = push_id self.device_name = name self.tracking_id = tracking_device_id @@ -104,7 +103,7 @@ class ApnsDevice(object): @property def disabled(self): - """Return the .""" + """Return the state of the service.""" return self.device_disabled def disable(self): diff --git a/homeassistant/components/rfxtrx.py b/homeassistant/components/rfxtrx.py index 2e96ec64d97..2f170a20646 100644 --- a/homeassistant/components/rfxtrx.py +++ b/homeassistant/components/rfxtrx.py @@ -4,21 +4,18 @@ Support for RFXtrx components. For more details about this component, please refer to the documentation at https://home-assistant.io/components/rfxtrx/ """ - import asyncio -import logging from collections import OrderedDict +import logging + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - ATTR_ENTITY_ID, TEMP_CELSIUS, - CONF_DEVICES -) + ATTR_ENTITY_ID, ATTR_NAME, ATTR_STATE, CONF_DEVICE, CONF_DEVICES, + EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.util import slugify REQUIREMENTS = ['pyRFXtrx==0.22.1'] @@ -29,8 +26,6 @@ DEFAULT_SIGNAL_REPETITIONS = 1 ATTR_AUTOMATIC_ADD = 'automatic_add' ATTR_DEVICE = 'device' ATTR_DEBUG = 'debug' -ATTR_STATE = 'state' -ATTR_NAME = 'name' ATTR_FIRE_EVENT = 'fire_event' ATTR_DATA_TYPE = 'data_type' ATTR_DUMMY = 'dummy' @@ -40,7 +35,6 @@ CONF_DATA_TYPE = 'data_type' CONF_SIGNAL_REPETITIONS = 'signal_repetitions' CONF_FIRE_EVENT = 'fire_event' CONF_DUMMY = 'dummy' -CONF_DEVICE = 'device' CONF_DEBUG = 'debug' CONF_OFF_DELAY = 'off_delay' EVENT_BUTTON_PRESSED = 'button_pressed' diff --git a/homeassistant/components/sensor/citybikes.py b/homeassistant/components/sensor/citybikes.py index b7635f729e2..a8bc441b722 100644 --- a/homeassistant/components/sensor/citybikes.py +++ b/homeassistant/components/sensor/citybikes.py @@ -4,32 +4,31 @@ Sensor for the CityBikes data. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.citybikes/ """ -import logging -from datetime import timedelta - import asyncio +from datetime import timedelta +import logging + import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT +from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, - ATTR_ATTRIBUTION, ATTR_LOCATION, ATTR_LATITUDE, ATTR_LONGITUDE, - STATE_UNKNOWN, LENGTH_METERS, LENGTH_FEET, ATTR_ID) + ATTR_ATTRIBUTION, ATTR_ID, ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, + ATTR_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_RADIUS, + LENGTH_FEET, LENGTH_METERS) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import location, distance +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import distance, location _LOGGER = logging.getLogger(__name__) ATTR_EMPTY_SLOTS = 'empty_slots' ATTR_EXTRA = 'extra' ATTR_FREE_BIKES = 'free_bikes' -ATTR_NAME = 'name' ATTR_NETWORK = 'network' ATTR_NETWORKS_LIST = 'networks' ATTR_STATIONS_LIST = 'stations' @@ -151,8 +150,7 @@ def async_setup_platform(hass, config, async_add_devices, network = CityBikesNetwork(hass, network_id) hass.data[PLATFORM][MONITORED_NETWORKS][network_id] = network hass.async_add_job(network.async_refresh) - async_track_time_interval(hass, network.async_refresh, - SCAN_INTERVAL) + async_track_time_interval(hass, network.async_refresh, SCAN_INTERVAL) else: network = hass.data[PLATFORM][MONITORED_NETWORKS][network_id] @@ -160,14 +158,14 @@ def async_setup_platform(hass, config, async_add_devices, devices = [] for station in network.stations: - dist = location.distance(latitude, longitude, - station[ATTR_LATITUDE], - station[ATTR_LONGITUDE]) + dist = location.distance( + latitude, longitude, station[ATTR_LATITUDE], + station[ATTR_LONGITUDE]) station_id = station[ATTR_ID] station_uid = str(station.get(ATTR_EXTRA, {}).get(ATTR_UID, '')) - if radius > dist or stations_list.intersection((station_id, - station_uid)): + if radius > dist or stations_list.intersection( + (station_id, station_uid)): devices.append(CityBikesStation(hass, network, station_id, name)) async_add_devices(devices, True) @@ -199,8 +197,8 @@ class CityBikesNetwork: for network in networks_list[1:]: network_latitude = network[ATTR_LOCATION][ATTR_LATITUDE] network_longitude = network[ATTR_LOCATION][ATTR_LONGITUDE] - dist = location.distance(latitude, longitude, - network_latitude, network_longitude) + dist = location.distance( + latitude, longitude, network_latitude, network_longitude) if dist < minimum_dist: minimum_dist = dist result = network[ATTR_ID] @@ -246,13 +244,13 @@ class CityBikesStation(Entity): uid = "_".join([network.network_id, base_name, station_id]) else: uid = "_".join([network.network_id, station_id]) - self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, - hass=hass) + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, uid, hass=hass) @property def state(self): """Return the state of the sensor.""" - return self._station_data.get(ATTR_FREE_BIKES, STATE_UNKNOWN) + return self._station_data.get(ATTR_FREE_BIKES, None) @property def name(self): diff --git a/homeassistant/components/sensor/coinmarketcap.py b/homeassistant/components/sensor/coinmarketcap.py index f8ada07eec6..849e21a0901 100644 --- a/homeassistant/components/sensor/coinmarketcap.py +++ b/homeassistant/components/sensor/coinmarketcap.py @@ -23,7 +23,6 @@ _LOGGER = logging.getLogger(__name__) ATTR_24H_VOLUME = '24h_volume' ATTR_AVAILABLE_SUPPLY = 'available_supply' ATTR_MARKET_CAP = 'market_cap' -ATTR_NAME = 'name' ATTR_PERCENT_CHANGE_24H = 'percent_change_24h' ATTR_PERCENT_CHANGE_7D = 'percent_change_7d' ATTR_PERCENT_CHANGE_1H = 'percent_change_1h' @@ -130,6 +129,4 @@ class CoinMarketCapData(object): """Get the latest data from blockchain.info.""" from coinmarketcap import Market self.ticker = Market().ticker( - self.currency, - limit=1, - convert=self.display_currency) + self.currency, limit=1, convert=self.display_currency) diff --git a/homeassistant/components/sensor/comed_hourly_pricing.py b/homeassistant/components/sensor/comed_hourly_pricing.py index 01e9f443e0e..c0c477ade0b 100644 --- a/homeassistant/components/sensor/comed_hourly_pricing.py +++ b/homeassistant/components/sensor/comed_hourly_pricing.py @@ -4,19 +4,21 @@ Support for ComEd Hourly Pricing data. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.comed_hourly_pricing/ """ -from datetime import timedelta -import logging import asyncio +from datetime import timedelta import json -import async_timeout +import logging + import aiohttp +import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_NAME, CONF_OFFSET, STATE_UNKNOWN) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _RESOURCE = 'https://hourlypricing.comed.com/api' @@ -27,8 +29,6 @@ CONF_ATTRIBUTION = "Data provided by ComEd Hourly Pricing service" CONF_CURRENT_HOUR_AVERAGE = 'current_hour_average' CONF_FIVE_MINUTE = 'five_minute' CONF_MONITORED_FEEDS = 'monitored_feeds' -CONF_NAME = 'name' -CONF_OFFSET = 'offset' CONF_SENSOR_TYPE = 'type' SENSOR_TYPES = { @@ -40,12 +40,12 @@ TYPES_SCHEMA = vol.In(SENSOR_TYPES) SENSORS_SCHEMA = vol.Schema({ vol.Required(CONF_SENSOR_TYPE): TYPES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_OFFSET, default=0.0): vol.Coerce(float), - vol.Optional(CONF_NAME): cv.string }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA] + vol.Required(CONF_MONITORED_FEEDS): [SENSORS_SCHEMA], }) diff --git a/homeassistant/components/sensor/linux_battery.py b/homeassistant/components/sensor/linux_battery.py index aad8c2f7a92..e7b8bf600a4 100644 --- a/homeassistant/components/sensor/linux_battery.py +++ b/homeassistant/components/sensor/linux_battery.py @@ -10,15 +10,14 @@ import os import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, DEVICE_CLASS_BATTERY -from homeassistant.helpers.entity import Entity +from homeassistant.const import ATTR_NAME, CONF_NAME, DEVICE_CLASS_BATTERY import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity REQUIREMENTS = ['batinfo==0.4.2'] _LOGGER = logging.getLogger(__name__) -ATTR_NAME = 'name' ATTR_PATH = 'path' ATTR_ALARM = 'alarm' ATTR_CAPACITY = 'capacity' diff --git a/homeassistant/components/sensor/qnap.py b/homeassistant/components/sensor/qnap.py index b3ca054f88f..7dd795d8f8d 100644 --- a/homeassistant/components/sensor/qnap.py +++ b/homeassistant/components/sensor/qnap.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.const import ( - CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_HOST, CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, ATTR_NAME, CONF_VERIFY_SSL, CONF_TIMEOUT, CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -29,7 +29,6 @@ ATTR_MASK = 'Mask' ATTR_MAX_SPEED = 'Max Speed' ATTR_MEMORY_SIZE = 'Memory Size' ATTR_MODEL = 'Model' -ATTR_NAME = 'Name' ATTR_PACKETS_TX = 'Packets (TX)' ATTR_PACKETS_RX = 'Packets (RX)' ATTR_PACKETS_ERR = 'Packets (Err)' diff --git a/homeassistant/components/sensor/rfxtrx.py b/homeassistant/components/sensor/rfxtrx.py index 4a555905d50..a5a6eb5f07b 100644 --- a/homeassistant/components/sensor/rfxtrx.py +++ b/homeassistant/components/sensor/rfxtrx.py @@ -10,10 +10,10 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.rfxtrx import ( - ATTR_DATA_TYPE, ATTR_FIRE_EVENT, ATTR_NAME, CONF_AUTOMATIC_ADD, - CONF_DATA_TYPE, CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) + ATTR_DATA_TYPE, ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, CONF_DATA_TYPE, + CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES) from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify diff --git a/homeassistant/components/sensor/tado.py b/homeassistant/components/sensor/tado.py index 7acdc1a20bd..ff8ad7fe849 100644 --- a/homeassistant/components/sensor/tado.py +++ b/homeassistant/components/sensor/tado.py @@ -6,16 +6,14 @@ https://home-assistant.io/components/sensor.tado/ """ import logging -from homeassistant.const import TEMP_CELSIUS +from homeassistant.components.tado import DATA_TADO +from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from homeassistant.components.tado import (DATA_TADO) -from homeassistant.const import (ATTR_ID) _LOGGER = logging.getLogger(__name__) ATTR_DATA_ID = 'data_id' ATTR_DEVICE = 'device' -ATTR_NAME = 'name' ATTR_ZONE = 'zone' CLIMATE_SENSOR_TYPES = ['temperature', 'humidity', 'power', @@ -39,14 +37,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if zone['type'] == 'HEATING': for variable in CLIMATE_SENSOR_TYPES: sensor_items.append(create_zone_sensor( - tado, zone, zone['name'], zone['id'], - variable)) + tado, zone, zone['name'], zone['id'], variable)) elif zone['type'] == 'HOT_WATER': for variable in HOT_WATER_SENSOR_TYPES: sensor_items.append(create_zone_sensor( - tado, zone, zone['name'], zone['id'], - variable - )) + tado, zone, zone['name'], zone['id'], variable)) me_data = tado.get_me() sensor_items.append(create_device_sensor( diff --git a/homeassistant/components/sensor/wsdot.py b/homeassistant/components/sensor/wsdot.py index fecff260716..0cd5ba44349 100644 --- a/homeassistant/components/sensor/wsdot.py +++ b/homeassistant/components/sensor/wsdot.py @@ -13,24 +13,27 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION, CONF_ID - ) + CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION, CONF_ID, ATTR_NAME) from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +ATTR_ACCESS_CODE = 'AccessCode' +ATTR_AVG_TIME = 'AverageTime' +ATTR_CURRENT_TIME = 'CurrentTime' +ATTR_DESCRIPTION = 'Description' +ATTR_TIME_UPDATED = 'TimeUpdated' +ATTR_TRAVEL_TIME_ID = 'TravelTimeID' + +CONF_ATTRIBUTION = "Data provided by WSDOT" + CONF_TRAVEL_TIMES = 'travel_time' -# API codes for travel time details -ATTR_ACCESS_CODE = 'AccessCode' -ATTR_TRAVEL_TIME_ID = 'TravelTimeID' -ATTR_CURRENT_TIME = 'CurrentTime' -ATTR_AVG_TIME = 'AverageTime' -ATTR_NAME = 'Name' -ATTR_TIME_UPDATED = 'TimeUpdated' -ATTR_DESCRIPTION = 'Description' -ATTRIBUTION = "Data provided by WSDOT" +ICON = 'mdi:car' + +RESOURCE = 'http://www.wsdot.wa.gov/Traffic/api/TravelTimes/' \ + 'TravelTimesREST.svc/GetTravelTimeAsJson' SCAN_INTERVAL = timedelta(minutes=3) @@ -43,16 +46,14 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): - """Get the WSDOT sensor.""" + """Set up the WSDOT sensor.""" sensors = [] for travel_time in config.get(CONF_TRAVEL_TIMES): - name = (travel_time.get(CONF_NAME) or - travel_time.get(CONF_ID)) + name = (travel_time.get(CONF_NAME) or travel_time.get(CONF_ID)) sensors.append( WashingtonStateTravelTimeSensor( - name, - config.get(CONF_API_KEY), - travel_time.get(CONF_ID))) + name, config.get(CONF_API_KEY), travel_time.get(CONF_ID))) + add_devices(sensors, True) @@ -65,8 +66,6 @@ class WashingtonStateTransportSensor(Entity): can read them and make them available. """ - ICON = 'mdi:car' - def __init__(self, name, access_code): """Initialize the sensor.""" self._data = {} @@ -87,16 +86,12 @@ class WashingtonStateTransportSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return self.ICON + return ICON class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" - RESOURCE = ('http://www.wsdot.wa.gov/Traffic/api/TravelTimes/' - 'TravelTimesREST.svc/GetTravelTimeAsJson') - ICON = 'mdi:car' - def __init__(self, name, access_code, travel_time_id): """Construct a travel time sensor.""" self._travel_time_id = travel_time_id @@ -104,10 +99,12 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def update(self): """Get the latest data from WSDOT.""" - params = {ATTR_ACCESS_CODE: self._access_code, - ATTR_TRAVEL_TIME_ID: self._travel_time_id} + params = { + ATTR_ACCESS_CODE: self._access_code, + ATTR_TRAVEL_TIME_ID: self._travel_time_id, + } - response = requests.get(self.RESOURCE, params, timeout=10) + response = requests.get(RESOURCE, params, timeout=10) if response.status_code != 200: _LOGGER.warning("Invalid response from WSDOT API") else: @@ -118,7 +115,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def device_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + attrs = {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} for key in [ATTR_AVG_TIME, ATTR_NAME, ATTR_DESCRIPTION, ATTR_TRAVEL_TIME_ID]: attrs[key] = self._data.get(key) @@ -129,7 +126,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" - return "min" + return 'min' def _parse_wsdot_timestamp(timestamp): @@ -139,5 +136,5 @@ def _parse_wsdot_timestamp(timestamp): # ex: Date(1485040200000-0800) milliseconds, tzone = re.search( r'Date\((\d+)([+-]\d\d)\d\d\)', timestamp).groups() - return datetime.fromtimestamp(int(milliseconds) / 1000, - tz=timezone(timedelta(hours=int(tzone)))) + return datetime.fromtimestamp( + int(milliseconds) / 1000, tz=timezone(timedelta(hours=int(tzone)))) diff --git a/homeassistant/components/switch/rfxtrx.py b/homeassistant/components/switch/rfxtrx.py index 7dd1d25ad94..68e91612008 100644 --- a/homeassistant/components/switch/rfxtrx.py +++ b/homeassistant/components/switch/rfxtrx.py @@ -10,11 +10,11 @@ import voluptuous as vol import homeassistant.components.rfxtrx as rfxtrx from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME from homeassistant.components.rfxtrx import ( CONF_AUTOMATIC_ADD, CONF_FIRE_EVENT, DEFAULT_SIGNAL_REPETITIONS, CONF_SIGNAL_REPETITIONS, CONF_DEVICES) from homeassistant.helpers import config_validation as cv +from homeassistant.const import CONF_NAME DEPENDENCIES = ['rfxtrx'] @@ -24,7 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_DEVICES, default={}): { cv.string: vol.Schema({ vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, }) }, vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, diff --git a/homeassistant/components/switch/rpi_pfio.py b/homeassistant/components/switch/rpi_pfio.py index c10f417ba49..3031b1e0290 100644 --- a/homeassistant/components/switch/rpi_pfio.py +++ b/homeassistant/components/switch/rpi_pfio.py @@ -10,7 +10,7 @@ import voluptuous as vol import homeassistant.components.rpi_pfio as rpi_pfio from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.const import ATTR_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity @@ -19,7 +19,6 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['rpi_pfio'] ATTR_INVERT_LOGIC = 'invert_logic' -ATTR_NAME = 'name' CONF_PORTS = 'ports' diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index eab67c18aed..042943f7a3f 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -15,7 +15,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, CONF_EMAIL, CONF_PASSWORD, + ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_NAME, CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, __version__) from homeassistant.core import callback @@ -45,7 +45,6 @@ ATTR_ACCESS_TOKEN = 'access_token' ATTR_REFRESH_TOKEN = 'refresh_token' ATTR_CLIENT_ID = 'client_id' ATTR_CLIENT_SECRET = 'client_secret' -ATTR_NAME = 'name' ATTR_PAIRING_MODE = 'pairing_mode' ATTR_KIDDE_RADIO_CODE = 'kidde_radio_code' ATTR_HUB_NAME = 'hub_name' @@ -53,7 +52,8 @@ ATTR_HUB_NAME = 'hub_name' WINK_AUTH_CALLBACK_PATH = '/auth/wink/callback' WINK_AUTH_START = '/auth/wink' WINK_CONFIG_FILE = '.wink.conf' -USER_AGENT = "Manufacturer/Home-Assistant%s python/3 Wink/3" % __version__ +USER_AGENT = "Manufacturer/Home-Assistant{} python/3 Wink/3".format( + __version__) DEFAULT_CONFIG = { 'client_id': 'CLIENT_ID_HERE', diff --git a/homeassistant/const.py b/homeassistant/const.py index 37e0c32ca03..acc30bcd57c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -221,6 +221,9 @@ ATTR_SERVICE_DATA = 'service_data' # IDs ATTR_ID = 'id' +# Name +ATTR_NAME = 'name' + # Data for a SERVICE_EXECUTED event ATTR_SERVICE_CALL_ID = 'service_call_id' diff --git a/tests/components/sensor/test_wsdot.py b/tests/components/sensor/test_wsdot.py index ee2cec3bb2a..8eb542b2b68 100644 --- a/tests/components/sensor/test_wsdot.py +++ b/tests/components/sensor/test_wsdot.py @@ -1,17 +1,16 @@ """The tests for the WSDOT platform.""" +from datetime import datetime, timedelta, timezone import re import unittest -from datetime import timedelta, datetime, timezone import requests_mock +from tests.common import get_test_home_assistant, load_fixture from homeassistant.components.sensor import wsdot from homeassistant.components.sensor.wsdot import ( - WashingtonStateTravelTimeSensor, ATTR_DESCRIPTION, - ATTR_TIME_UPDATED, CONF_API_KEY, CONF_NAME, - CONF_ID, CONF_TRAVEL_TIMES, SCAN_INTERVAL) + ATTR_DESCRIPTION, ATTR_TIME_UPDATED, CONF_API_KEY, CONF_ID, CONF_NAME, + CONF_TRAVEL_TIMES, RESOURCE, SCAN_INTERVAL) from homeassistant.setup import setup_component -from tests.common import load_fixture, get_test_home_assistant class TestWSDOT(unittest.TestCase): @@ -50,7 +49,7 @@ class TestWSDOT(unittest.TestCase): @requests_mock.Mocker() def test_setup(self, mock_req): """Test for operational WSDOT sensor with proper attributes.""" - uri = re.compile(WashingtonStateTravelTimeSensor.RESOURCE + '*') + uri = re.compile(RESOURCE + '*') mock_req.get(uri, text=load_fixture('wsdot.json')) wsdot.setup_platform(self.hass, self.config, self.add_entities) self.assertEqual(len(self.entities), 1) From 2f74ffcf81e22a9c038f2591dc008b7938b96d92 Mon Sep 17 00:00:00 2001 From: Russell Cloran Date: Mon, 14 May 2018 07:50:09 -0700 Subject: [PATCH 078/144] zha: Fix cluster class check in single-cluster device type (#14303) zigpy now allows custom devices, which might mean that devices have cluster objects which are not instances of the default, but may be instances of sub-classes of the default. This fixes the check for finding single-cluster device entities to handle sub-classes properly. --- homeassistant/components/zha/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 238e89c07f0..3ea95ff1dd1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -256,11 +256,16 @@ class ApplicationListener: """Try to set up an entity from a "bare" cluster.""" if cluster.cluster_id in profile_clusters: return - # pylint: disable=unidiomatic-typecheck - if type(cluster) not in device_classes: + + component = None + for cluster_type, candidate_component in device_classes.items(): + if isinstance(cluster, cluster_type): + component = candidate_component + break + + if component is None: return - component = device_classes[type(cluster)] cluster_key = "{}-{}".format(device_key, cluster.cluster_id) discovery_info = { 'application_listener': self, From 1b5c02ff67473b17cf0959c5e1779f43a451cfc5 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 May 2018 21:52:54 +0200 Subject: [PATCH 079/144] Upgrade pygatt to 3.2.0 (#14447) --- homeassistant/components/sensor/skybeacon.py | 32 ++++++++++---------- requirements_all.txt | 3 ++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sensor/skybeacon.py b/homeassistant/components/sensor/skybeacon.py index eabc33312b2..61933614a74 100644 --- a/homeassistant/components/sensor/skybeacon.py +++ b/homeassistant/components/sensor/skybeacon.py @@ -10,38 +10,38 @@ from uuid import UUID import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_MAC, TEMP_CELSIUS, STATE_UNKNOWN, EVENT_HOMEASSISTANT_STOP) + CONF_MAC, CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, TEMP_CELSIUS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity -# REQUIREMENTS = ['pygatt==3.1.1'] +REQUIREMENTS = ['pygatt==3.2.0'] _LOGGER = logging.getLogger(__name__) -CONNECT_LOCK = threading.Lock() - ATTR_DEVICE = 'device' ATTR_MODEL = 'model' +BLE_TEMP_HANDLE = 0x24 +BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' + +CONNECT_LOCK = threading.Lock() +CONNECT_TIMEOUT = 30 + +DEFAULT_NAME = 'Skybeacon' + +SKIP_HANDLE_LOOKUP = True + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_NAME, default=""): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, }) -BLE_TEMP_UUID = '0000ff92-0000-1000-8000-00805f9b34fb' -BLE_TEMP_HANDLE = 0x24 -SKIP_HANDLE_LOOKUP = True -CONNECT_TIMEOUT = 30 - # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Skybeacon sensor.""" - _LOGGER.warning("This platform has been disabled due to having a " - "requirement depending on enum34.") - return # pylint: disable=unreachable name = config.get(CONF_NAME) mac = config.get(CONF_MAC) @@ -150,7 +150,7 @@ class Monitor(threading.Thread): adapter = pygatt.backends.GATTToolBackend() while True: try: - _LOGGER.info("Connecting to %s", self.name) + _LOGGER.debug("Connecting to %s", self.name) # We need concurrent connect, so lets not reset the device adapter.start(reset_on_start=False) # Seems only one connection can be initiated at a time diff --git a/requirements_all.txt b/requirements_all.txt index a1158d2af3e..63d9b9dcca2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -783,6 +783,9 @@ pyfritzhome==0.3.7 # homeassistant.components.ifttt pyfttt==0.3 +# homeassistant.components.sensor.skybeacon +pygatt==3.2.0 + # homeassistant.components.cover.gogogate2 pygogogate2==0.1.1 From 44e9783c7c45ff9cbaf3c716932a1a06808632cf Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Mon, 14 May 2018 16:37:49 -0400 Subject: [PATCH 080/144] Add support for direction to fan template (#14371) * Initial commit * Update and add tests --- homeassistant/components/fan/template.py | 93 ++++++++++++--- tests/components/fan/test_template.py | 141 +++++++++++++++++++---- 2 files changed, 197 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/fan/template.py b/homeassistant/components/fan/template.py index 31b335eb2bc..a40437e719b 100644 --- a/homeassistant/components/fan/template.py +++ b/homeassistant/components/fan/template.py @@ -18,11 +18,10 @@ from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import ToggleEntity -from homeassistant.components.fan import (SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, FanEntity, - ATTR_SPEED, ATTR_OSCILLATING, - ENTITY_ID_FORMAT) +from homeassistant.components.fan import ( + SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, + FanEntity, ATTR_SPEED, ATTR_OSCILLATING, ENTITY_ID_FORMAT, + SUPPORT_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE, ATTR_DIRECTION) from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script @@ -33,25 +32,30 @@ CONF_FANS = 'fans' CONF_SPEED_LIST = 'speeds' CONF_SPEED_TEMPLATE = 'speed_template' CONF_OSCILLATING_TEMPLATE = 'oscillating_template' +CONF_DIRECTION_TEMPLATE = 'direction_template' CONF_ON_ACTION = 'turn_on' CONF_OFF_ACTION = 'turn_off' CONF_SET_SPEED_ACTION = 'set_speed' CONF_SET_OSCILLATING_ACTION = 'set_oscillating' +CONF_SET_DIRECTION_ACTION = 'set_direction' _VALID_STATES = [STATE_ON, STATE_OFF] _VALID_OSC = [True, False] +_VALID_DIRECTIONS = [DIRECTION_FORWARD, DIRECTION_REVERSE] FAN_SCHEMA = vol.Schema({ vol.Optional(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SPEED_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATING_TEMPLATE): cv.template, + vol.Optional(CONF_DIRECTION_TEMPLATE): cv.template, vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_SPEED_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_SET_OSCILLATING_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_SET_DIRECTION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional( CONF_SPEED_LIST, @@ -80,18 +84,21 @@ async def async_setup_platform( oscillating_template = device_config.get( CONF_OSCILLATING_TEMPLATE ) + direction_template = device_config.get(CONF_DIRECTION_TEMPLATE) on_action = device_config[CONF_ON_ACTION] off_action = device_config[CONF_OFF_ACTION] set_speed_action = device_config.get(CONF_SET_SPEED_ACTION) set_oscillating_action = device_config.get(CONF_SET_OSCILLATING_ACTION) + set_direction_action = device_config.get(CONF_SET_DIRECTION_ACTION) speed_list = device_config[CONF_SPEED_LIST] entity_ids = set() manual_entity_ids = device_config.get(CONF_ENTITY_ID) - for template in (state_template, speed_template, oscillating_template): + for template in (state_template, speed_template, oscillating_template, + direction_template): if template is None: continue template.hass = hass @@ -114,8 +121,9 @@ async def async_setup_platform( TemplateFan( hass, device, friendly_name, state_template, speed_template, oscillating_template, - on_action, off_action, set_speed_action, - set_oscillating_action, speed_list, entity_ids + direction_template, on_action, off_action, set_speed_action, + set_oscillating_action, set_direction_action, speed_list, + entity_ids ) ) @@ -127,8 +135,9 @@ class TemplateFan(FanEntity): def __init__(self, hass, device_id, friendly_name, state_template, speed_template, oscillating_template, - on_action, off_action, set_speed_action, - set_oscillating_action, speed_list, entity_ids): + direction_template, on_action, off_action, set_speed_action, + set_oscillating_action, set_direction_action, speed_list, + entity_ids): """Initialize the fan.""" self.hass = hass self.entity_id = async_generate_entity_id( @@ -138,6 +147,7 @@ class TemplateFan(FanEntity): self._template = state_template self._speed_template = speed_template self._oscillating_template = oscillating_template + self._direction_template = direction_template self._supported_features = 0 self._on_script = Script(hass, on_action) @@ -151,9 +161,14 @@ class TemplateFan(FanEntity): if set_oscillating_action: self._set_oscillating_script = Script(hass, set_oscillating_action) + self._set_direction_script = None + if set_direction_action: + self._set_direction_script = Script(hass, set_direction_action) + self._state = STATE_OFF self._speed = None self._oscillating = None + self._direction = None self._template.hass = self.hass if self._speed_template: @@ -162,6 +177,9 @@ class TemplateFan(FanEntity): if self._oscillating_template: self._oscillating_template.hass = self.hass self._supported_features |= SUPPORT_OSCILLATE + if self._direction_template: + self._direction_template.hass = self.hass + self._supported_features |= SUPPORT_DIRECTION self._entities = entity_ids # List of valid speeds @@ -197,6 +215,11 @@ class TemplateFan(FanEntity): """Return the oscillation state.""" return self._oscillating + @property + def direction(self): + """Return the oscillation state.""" + return self._direction + @property def should_poll(self): """Return the polling state.""" @@ -236,10 +259,30 @@ class TemplateFan(FanEntity): if self._set_oscillating_script is None: return - await self._set_oscillating_script.async_run( - {ATTR_OSCILLATING: oscillating} - ) - self._oscillating = oscillating + if oscillating in _VALID_OSC: + self._oscillating = oscillating + await self._set_oscillating_script.async_run( + {ATTR_OSCILLATING: oscillating}) + else: + _LOGGER.error( + 'Received invalid oscillating value: %s. ' + + 'Expected: %s.', + oscillating, ', '.join(_VALID_OSC)) + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + if self._set_direction_script is None: + return + + if direction in _VALID_DIRECTIONS: + self._direction = direction + await self._set_direction_script.async_run( + {ATTR_DIRECTION: direction}) + else: + _LOGGER.error( + 'Received invalid direction: %s. ' + + 'Expected: %s.', + direction, ', '.join(_VALID_DIRECTIONS)) async def async_added_to_hass(self): """Register callbacks.""" @@ -308,6 +351,7 @@ class TemplateFan(FanEntity): oscillating = self._oscillating_template.async_render() except TemplateError as ex: _LOGGER.error(ex) + oscillating = None self._state = None # Validate osc @@ -322,3 +366,24 @@ class TemplateFan(FanEntity): 'Received invalid oscillating: %s. ' + 'Expected: True/False.', oscillating) self._oscillating = None + + # Update direction if 'direction_template' is configured + if self._direction_template is not None: + try: + direction = self._direction_template.async_render() + except TemplateError as ex: + _LOGGER.error(ex) + direction = None + self._state = None + + # Validate speed + if direction in _VALID_DIRECTIONS: + self._direction = direction + elif direction == STATE_UNKNOWN: + self._direction = None + else: + _LOGGER.error( + 'Received invalid direction: %s. ' + + 'Expected: %s.', + direction, ', '.join(_VALID_DIRECTIONS)) + self._direction = None diff --git a/tests/components/fan/test_template.py b/tests/components/fan/test_template.py index 719a3f96aed..53eb9e8e2d4 100644 --- a/tests/components/fan/test_template.py +++ b/tests/components/fan/test_template.py @@ -6,7 +6,8 @@ from homeassistant import setup import homeassistant.components as components from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.components.fan import ( - ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH) + ATTR_SPEED, ATTR_OSCILLATING, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH, + ATTR_DIRECTION, DIRECTION_FORWARD, DIRECTION_REVERSE) from tests.common import ( get_test_home_assistant, assert_setup_component) @@ -20,6 +21,8 @@ _STATE_INPUT_BOOLEAN = 'input_boolean.state' _SPEED_INPUT_SELECT = 'input_select.speed' # Represent for fan's oscillating _OSC_INPUT = 'input_select.osc' +# Represent for fan's direction +_DIRECTION_INPUT_SELECT = 'input_select.direction' class TestTemplateFan: @@ -71,7 +74,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_missing_value_template_config(self): """Test: missing 'value_template' will fail.""" @@ -185,6 +188,8 @@ class TestTemplateFan: "{{ states('input_select.speed') }}", 'oscillating_template': "{{ states('input_select.osc') }}", + 'direction_template': + "{{ states('input_select.direction') }}", 'turn_on': { 'service': 'script.fan_on' }, @@ -199,14 +204,15 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) self.hass.states.set(_STATE_INPUT_BOOLEAN, True) self.hass.states.set(_SPEED_INPUT_SELECT, SPEED_MEDIUM) self.hass.states.set(_OSC_INPUT, 'True') + self.hass.states.set(_DIRECTION_INPUT_SELECT, DIRECTION_FORWARD) self.hass.block_till_done() - self._verify(STATE_ON, SPEED_MEDIUM, True) + self._verify(STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) def test_templates_with_valid_values(self): """Test templates with valid values.""" @@ -222,6 +228,8 @@ class TestTemplateFan: "{{ 'medium' }}", 'oscillating_template': "{{ 1 == 1 }}", + 'direction_template': + "{{ 'forward' }}", 'turn_on': { 'service': 'script.fan_on' @@ -237,7 +245,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_ON, SPEED_MEDIUM, True) + self._verify(STATE_ON, SPEED_MEDIUM, True, DIRECTION_FORWARD) def test_templates_invalid_values(self): """Test templates with invalid values.""" @@ -253,6 +261,8 @@ class TestTemplateFan: "{{ '0' }}", 'oscillating_template': "{{ 'xyz' }}", + 'direction_template': + "{{ 'right' }}", 'turn_on': { 'service': 'script.fan_on' @@ -268,7 +278,7 @@ class TestTemplateFan: self.hass.start() self.hass.block_till_done() - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) # End of template tests # @@ -283,7 +293,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) # Turn off fan components.fan.turn_off(self.hass, _TEST_FAN) @@ -291,7 +301,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_OFF - self._verify(STATE_OFF, None, None) + self._verify(STATE_OFF, None, None, None) def test_on_with_speed(self): """Test turn on with speed.""" @@ -304,7 +314,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_STATE_INPUT_BOOLEAN).state == STATE_ON assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) def test_set_speed(self): """Test set valid speed.""" @@ -320,7 +330,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to medium components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) @@ -328,7 +338,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_MEDIUM - self._verify(STATE_ON, SPEED_MEDIUM, None) + self._verify(STATE_ON, SPEED_MEDIUM, None, None) def test_set_invalid_speed_from_initial_stage(self): """Test set invalid speed when fan is in initial state.""" @@ -344,7 +354,7 @@ class TestTemplateFan: # verify speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '' - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_set_invalid_speed(self): """Test set invalid speed when fan has valid speed.""" @@ -360,7 +370,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) # Set fan's speed to 'invalid' components.fan.set_speed(self.hass, _TEST_FAN, 'invalid') @@ -368,7 +378,7 @@ class TestTemplateFan: # verify speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == SPEED_HIGH - self._verify(STATE_ON, SPEED_HIGH, None) + self._verify(STATE_ON, SPEED_HIGH, None, None) def test_custom_speed_list(self): """Test set custom speed list.""" @@ -384,7 +394,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' - self._verify(STATE_ON, '1', None) + self._verify(STATE_ON, '1', None, None) # Set fan's speed to 'medium' which is invalid components.fan.set_speed(self.hass, _TEST_FAN, SPEED_MEDIUM) @@ -392,7 +402,7 @@ class TestTemplateFan: # verify that speed is unchanged assert self.hass.states.get(_SPEED_INPUT_SELECT).state == '1' - self._verify(STATE_ON, '1', None) + self._verify(STATE_ON, '1', None, None) def test_set_osc(self): """Test set oscillating.""" @@ -408,7 +418,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) # Set fan's osc to False components.fan.oscillate(self.hass, _TEST_FAN, False) @@ -416,7 +426,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'False' - self._verify(STATE_ON, None, False) + self._verify(STATE_ON, None, False, None) def test_set_invalid_osc_from_initial_state(self): """Test set invalid oscillating when fan is in initial state.""" @@ -432,7 +442,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == '' - self._verify(STATE_ON, None, None) + self._verify(STATE_ON, None, None, None) def test_set_invalid_osc(self): """Test set invalid oscillating when fan has valid osc.""" @@ -448,7 +458,7 @@ class TestTemplateFan: # verify assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) # Set fan's osc to False components.fan.oscillate(self.hass, _TEST_FAN, None) @@ -456,15 +466,85 @@ class TestTemplateFan: # verify osc is unchanged assert self.hass.states.get(_OSC_INPUT).state == 'True' - self._verify(STATE_ON, None, True) + self._verify(STATE_ON, None, True, None) - def _verify(self, expected_state, expected_speed, expected_oscillating): + def test_set_direction(self): + """Test set valid direction.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to forward + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state \ + == DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + # Set fan's direction to reverse + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_REVERSE) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state \ + == DIRECTION_REVERSE + self._verify(STATE_ON, None, None, DIRECTION_REVERSE) + + def test_set_invalid_direction_from_initial_stage(self): + """Test set invalid direction when fan is in initial state.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to 'invalid' + components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify direction is unchanged + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == '' + self._verify(STATE_ON, None, None, None) + + def test_set_invalid_direction(self): + """Test set invalid direction when fan has valid direction.""" + self._register_components() + + # Turn on fan + components.fan.turn_on(self.hass, _TEST_FAN) + self.hass.block_till_done() + + # Set fan's direction to forward + components.fan.set_direction(self.hass, _TEST_FAN, DIRECTION_FORWARD) + self.hass.block_till_done() + + # verify + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == \ + DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + # Set fan's direction to 'invalid' + components.fan.set_direction(self.hass, _TEST_FAN, 'invalid') + self.hass.block_till_done() + + # verify direction is unchanged + assert self.hass.states.get(_DIRECTION_INPUT_SELECT).state == \ + DIRECTION_FORWARD + self._verify(STATE_ON, None, None, DIRECTION_FORWARD) + + def _verify(self, expected_state, expected_speed, expected_oscillating, + expected_direction): """Verify fan's state, speed and osc.""" state = self.hass.states.get(_TEST_FAN) attributes = state.attributes assert state.state == expected_state assert attributes.get(ATTR_SPEED, None) == expected_speed assert attributes.get(ATTR_OSCILLATING, None) == expected_oscillating + assert attributes.get(ATTR_DIRECTION, None) == expected_direction def _register_components(self, speed_list=None): """Register basic components for testing.""" @@ -475,7 +555,7 @@ class TestTemplateFan: {'input_boolean': {'state': None}} ) - with assert_setup_component(2, 'input_select'): + with assert_setup_component(3, 'input_select'): assert setup.setup_component(self.hass, 'input_select', { 'input_select': { 'speed': { @@ -488,6 +568,11 @@ class TestTemplateFan: 'name': 'oscillating', 'options': ['', 'True', 'False'] }, + + 'direction': { + 'name': 'Direction', + 'options': ['', DIRECTION_FORWARD, DIRECTION_REVERSE] + }, } }) @@ -506,6 +591,8 @@ class TestTemplateFan: "{{ states('input_select.speed') }}", 'oscillating_template': "{{ states('input_select.osc') }}", + 'direction_template': + "{{ states('input_select.direction') }}", 'turn_on': { 'service': 'input_boolean.turn_on', @@ -530,6 +617,14 @@ class TestTemplateFan: 'entity_id': _OSC_INPUT, 'option': '{{ oscillating }}' } + }, + 'set_direction': { + 'service': 'input_select.select_option', + + 'data_template': { + 'entity_id': _DIRECTION_INPUT_SELECT, + 'option': '{{ direction }}' + } } } From cf44b77225387b5e343df2dee77497468d829610 Mon Sep 17 00:00:00 2001 From: Gregory Benner Date: Mon, 14 May 2018 16:52:35 -0400 Subject: [PATCH 081/144] Samsung Family hub camera component (#14458) * add familyhub.py camera * fix import and REQUIREMENTS * add to coveragerc * fix formatting to make houndci-bot happy * ran scripts/gen_requirements_all.py * use CONF_IP_ADDRESS * Revert "ran scripts/gen_requirements_all.py" This reverts commit 3a38681d8a084e6d4811771ae7a18819477885bc. * fix library name * add missing docstrings and enable polling * Sort imports --- .coveragerc | 1 + homeassistant/components/camera/familyhub.py | 63 ++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 67 insertions(+) create mode 100644 homeassistant/components/camera/familyhub.py diff --git a/.coveragerc b/.coveragerc index 28fe39430f3..431c2f6f976 100644 --- a/.coveragerc +++ b/.coveragerc @@ -347,6 +347,7 @@ omit = homeassistant/components/calendar/todoist.py homeassistant/components/camera/bloomsky.py homeassistant/components/camera/canary.py + homeassistant/components/camera/familyhub.py homeassistant/components/camera/ffmpeg.py homeassistant/components/camera/foscam.py homeassistant/components/camera/mjpeg.py diff --git a/homeassistant/components/camera/familyhub.py b/homeassistant/components/camera/familyhub.py new file mode 100644 index 00000000000..1868078c4c7 --- /dev/null +++ b/homeassistant/components/camera/familyhub.py @@ -0,0 +1,63 @@ +""" +Family Hub camera for Samsung Refrigerators. + +For more details about this platform, please refer to the documentation +https://home-assistant.io/components/camera.familyhub/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.camera import Camera +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['python-family-hub-local==0.0.2'] + +DEFAULT_NAME = 'FamilyHub Camera' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): + """Set up the Family Hub Camera.""" + from pyfamilyhublocal import FamilyHubCam + address = config.get(CONF_IP_ADDRESS) + name = config.get(CONF_NAME) + + session = async_get_clientsession(hass) + family_hub_cam = FamilyHubCam(address, hass.loop, session) + + async_add_devices([FamilyHubCamera(name, family_hub_cam)], True) + + +class FamilyHubCamera(Camera): + """The representation of a Family Hub camera.""" + + def __init__(self, name, family_hub_cam): + """Initialize camera component.""" + super().__init__() + self._name = name + self.family_hub_cam = family_hub_cam + + async def camera_image(self): + """Return a still image response.""" + return await self.family_hub_cam.async_get_cam_image() + + @property + def name(self): + """Return the name of this camera.""" + return self._name + + @property + def should_poll(self): + """Camera should poll periodically.""" + return True diff --git a/requirements_all.txt b/requirements_all.txt index 63d9b9dcca2..a5ad5ef6d0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -969,6 +969,9 @@ python-ecobee-api==0.0.18 # homeassistant.components.sensor.etherscan python-etherscan-api==0.0.3 +# homeassistant.components.camera.familyhub +python-family-hub-local==0.0.2 + # homeassistant.components.sensor.darksky # homeassistant.components.weather.darksky python-forecastio==1.4.0 From 7562b4164b0d79f233f6bc967a03ad582ef7b03b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 14 May 2018 22:52:44 +0200 Subject: [PATCH 082/144] Fix key error upon missing node (#14460) * This is needed after gateway ready message generates an update while persistence is off, or while the gateway node hasn't been presented yet. --- homeassistant/components/mysensors.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index f5ad59095dc..6721669a026 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -495,8 +495,9 @@ def gw_callback_factory(hass): _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) - child = msg.gateway.sensors[msg.node_id].children.get(msg.child_id) - if child is None: + try: + child = msg.gateway.sensors[msg.node_id].children[msg.child_id] + except KeyError: _LOGGER.debug("Not a child update for node %s", msg.node_id) return From 11c57f9345e3c8c86174b35bb6b9f7a120a6d680 Mon Sep 17 00:00:00 2001 From: Matthew Garrett Date: Mon, 14 May 2018 22:51:32 -0700 Subject: [PATCH 083/144] Bump lakeside version (#14471) This should fix a couple of issues with T1013 bulbs, and also handle accounts that contain unknown devices. --- homeassistant/components/eufy.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eufy.py b/homeassistant/components/eufy.py index 733aa0adbfe..892c0b9972a 100644 --- a/homeassistant/components/eufy.py +++ b/homeassistant/components/eufy.py @@ -15,7 +15,7 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['lakeside==0.5'] +REQUIREMENTS = ['lakeside==0.6'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index a5ad5ef6d0a..d1da68360be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -465,7 +465,7 @@ keyring==12.2.0 keyrings.alt==3.1 # homeassistant.components.eufy -lakeside==0.5 +lakeside==0.6 # homeassistant.components.device_tracker.owntracks # homeassistant.components.device_tracker.owntracks_http From 710533ae8a51a789ec131ee3944f67e8c1d9cf99 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Tue, 15 May 2018 01:53:12 -0400 Subject: [PATCH 084/144] Minor Wink fixes (#14468) * Updated Wink light supported feature to reflect what features a given light support. * Fix typo in wink climate --- homeassistant/components/climate/wink.py | 2 +- homeassistant/components/light/wink.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/wink.py b/homeassistant/components/climate/wink.py index 8c66567a4aa..c67e032c149 100644 --- a/homeassistant/components/climate/wink.py +++ b/homeassistant/components/climate/wink.py @@ -190,7 +190,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): @property def cool_on(self): """Return whether or not the heat is actually heating.""" - return self.wink.heat_on() + return self.wink.cool_on() @property def current_operation(self): diff --git a/homeassistant/components/light/wink.py b/homeassistant/components/light/wink.py index 04e9c34b0f6..a2cc4fd7aeb 100644 --- a/homeassistant/components/light/wink.py +++ b/homeassistant/components/light/wink.py @@ -16,8 +16,6 @@ from homeassistant.util.color import \ DEPENDENCIES = ['wink'] -SUPPORT_WINK = SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_COLOR - def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the Wink lights.""" @@ -78,7 +76,14 @@ class WinkLight(WinkDevice, Light): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_WINK + supports = SUPPORT_BRIGHTNESS + if self.wink.supports_temperature(): + supports = supports | SUPPORT_COLOR_TEMP + if self.wink.supports_xy_color(): + supports = supports | SUPPORT_COLOR + elif self.wink.supports_hue_saturation(): + supports = supports | SUPPORT_COLOR + return supports def turn_on(self, **kwargs): """Turn the switch on.""" From 16bf10b1a277ed2234395e329743e257f9d1a5ea Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Tue, 15 May 2018 08:50:07 +0200 Subject: [PATCH 085/144] Don't poll the Samsung Family hub camera (#14473) --- homeassistant/components/camera/familyhub.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/camera/familyhub.py b/homeassistant/components/camera/familyhub.py index 1868078c4c7..e78d341713b 100644 --- a/homeassistant/components/camera/familyhub.py +++ b/homeassistant/components/camera/familyhub.py @@ -48,7 +48,7 @@ class FamilyHubCamera(Camera): self._name = name self.family_hub_cam = family_hub_cam - async def camera_image(self): + async def async_camera_image(self): """Return a still image response.""" return await self.family_hub_cam.async_get_cam_image() @@ -56,8 +56,3 @@ class FamilyHubCamera(Camera): def name(self): """Return the name of this camera.""" return self._name - - @property - def should_poll(self): - """Camera should poll periodically.""" - return True From d47006c98f5efa5c199ea60738e2037f92165abf Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 15 May 2018 11:25:50 +0100 Subject: [PATCH 086/144] Optimistic MQTT light (#14401) * Restores light state, case the light is optimistic * lint * hound * hound * Added mqtt_json * hound * added mqtt_template * lint * cleanup * use ATTR --- homeassistant/components/light/mqtt.py | 63 ++++++++++++------- homeassistant/components/light/mqtt_json.py | 18 +++++- .../components/light/mqtt_template.py | 34 ++++++---- tests/components/light/test_mqtt.py | 23 +++++-- tests/components/light/test_mqtt_json.py | 46 +++++++++----- tests/components/light/test_mqtt_template.py | 54 ++++++++++------ 6 files changed, 165 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index a0534ba4e95..97a4cc8c137 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -4,7 +4,6 @@ Support for MQTT lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt/ """ -import asyncio import logging import voluptuous as vol @@ -17,12 +16,13 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, SUPPORT_COLOR, SUPPORT_WHITE_VALUE) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, CONF_NAME, - CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, STATE_ON, CONF_RGB, CONF_STATE, CONF_VALUE_TEMPLATE, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_COMMAND_TOPIC, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, MqttAvailability) +from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -100,8 +100,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a MQTT Light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -213,10 +213,9 @@ class MqttLight(MqttAvailability, Light): self._supported_features |= ( topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR) - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() templates = {} for key, tpl in list(self._templates.items()): @@ -226,6 +225,8 @@ class MqttLight(MqttAvailability, Light): tpl.hass = self.hass templates[key] = tpl.async_render_with_possible_json_value + last_state = await async_get_last_state(self.hass, self.entity_id) + @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -237,9 +238,11 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) + elif self._optimistic and last_state: + self._state = last_state.state == STATE_ON @callback def brightness_received(topic, payload, qos): @@ -250,10 +253,13 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_BRIGHTNESS_STATE_TOPIC], brightness_received, self._qos) self._brightness = 255 + elif self._optimistic_brightness and last_state\ + and last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) elif self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None: self._brightness = 255 else: @@ -268,11 +274,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_RGB_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_RGB_STATE_TOPIC], rgb_received, self._qos) self._hs = (0, 0) - if self._topic[CONF_RGB_COMMAND_TOPIC] is not None: + if self._optimistic_rgb and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_RGB_COMMAND_TOPIC] is not None: self._hs = (0, 0) @callback @@ -282,11 +291,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_COLOR_TEMP_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_COLOR_TEMP_STATE_TOPIC], color_temp_received, self._qos) self._color_temp = 150 - if self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: + if self._optimistic_color_temp and last_state\ + and last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + elif self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: self._color_temp = 150 else: self._color_temp = None @@ -298,11 +310,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_EFFECT_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_EFFECT_STATE_TOPIC], effect_received, self._qos) self._effect = 'none' - if self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: + if self._optimistic_effect and last_state\ + and last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + elif self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None: self._effect = 'none' else: self._effect = None @@ -316,10 +331,13 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_WHITE_VALUE_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_WHITE_VALUE_STATE_TOPIC], white_value_received, self._qos) self._white_value = 255 + elif self._optimistic_white_value and last_state\ + and last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) elif self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: self._white_value = 255 else: @@ -334,11 +352,14 @@ class MqttLight(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topic[CONF_XY_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topic[CONF_XY_STATE_TOPIC], xy_received, self._qos) self._hs = (0, 0) - if self._topic[CONF_XY_COMMAND_TOPIC] is not None: + if self._optimistic_xy and last_state\ + and last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + elif self._topic[CONF_XY_COMMAND_TOPIC] is not None: self._hs = (0, 0) @property @@ -396,8 +417,7 @@ class MqttLight(MqttAvailability, Light): """Flag supported features.""" return self._supported_features - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -517,8 +537,7 @@ class MqttLight(MqttAvailability, Light): if should_update: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the device off. This method is a coroutine. diff --git a/homeassistant/components/light/mqtt_json.py b/homeassistant/components/light/mqtt_json.py index ca5c76e905f..14f5ee7a9b9 100644 --- a/homeassistant/components/light/mqtt_json.py +++ b/homeassistant/components/light/mqtt_json.py @@ -18,7 +18,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE) from homeassistant.components.light.mqtt import CONF_BRIGHTNESS_SCALE from homeassistant.const import ( - CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, + CONF_BRIGHTNESS, CONF_COLOR_TEMP, CONF_EFFECT, STATE_ON, CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, CONF_WHITE_VALUE, CONF_XY) from homeassistant.components.mqtt import ( CONF_AVAILABILITY_TOPIC, CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, @@ -26,6 +26,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.restore_state import async_get_last_state import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -177,6 +178,8 @@ class MqttJson(MqttAvailability, Light): """Subscribe to MQTT events.""" await super().async_added_to_hass() + last_state = await async_get_last_state(self.hass, self.entity_id) + @callback def state_received(topic, payload, qos): """Handle new MQTT messages.""" @@ -260,6 +263,19 @@ class MqttJson(MqttAvailability, Light): self.hass, self._topic[CONF_STATE_TOPIC], state_received, self._qos) + if self._optimistic and last_state: + self._state = last_state.state == STATE_ON + if last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + if last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + if last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + if last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + if last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/light/mqtt_template.py b/homeassistant/components/light/mqtt_template.py index 06a94cd23b4..e32c13fc5b6 100644 --- a/homeassistant/components/light/mqtt_template.py +++ b/homeassistant/components/light/mqtt_template.py @@ -4,7 +4,6 @@ Support for MQTT Template lights. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_template/ """ -import asyncio import logging import voluptuous as vol @@ -22,6 +21,7 @@ from homeassistant.components.mqtt import ( MqttAvailability) import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util +from homeassistant.helpers.restore_state import async_get_last_state _LOGGER = logging.getLogger(__name__) @@ -66,8 +66,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ }).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): """Set up a MQTT Template light.""" if discovery_info is not None: config = PLATFORM_SCHEMA(discovery_info) @@ -152,10 +152,11 @@ class MqttTemplate(MqttAvailability, Light): if tpl is not None: tpl.hass = hass - @asyncio.coroutine - def async_added_to_hass(self): + async def async_added_to_hass(self): """Subscribe to MQTT events.""" - yield from super().async_added_to_hass() + await super().async_added_to_hass() + + last_state = await async_get_last_state(self.hass, self.entity_id) @callback def state_received(topic, payload, qos): @@ -223,10 +224,23 @@ class MqttTemplate(MqttAvailability, Light): self.async_schedule_update_ha_state() if self._topics[CONF_STATE_TOPIC] is not None: - yield from mqtt.async_subscribe( + await mqtt.async_subscribe( self.hass, self._topics[CONF_STATE_TOPIC], state_received, self._qos) + if self._optimistic and last_state: + self._state = last_state.state == STATE_ON + if last_state.attributes.get(ATTR_BRIGHTNESS): + self._brightness = last_state.attributes.get(ATTR_BRIGHTNESS) + if last_state.attributes.get(ATTR_HS_COLOR): + self._hs = last_state.attributes.get(ATTR_HS_COLOR) + if last_state.attributes.get(ATTR_COLOR_TEMP): + self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) + if last_state.attributes.get(ATTR_EFFECT): + self._effect = last_state.attributes.get(ATTR_EFFECT) + if last_state.attributes.get(ATTR_WHITE_VALUE): + self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -280,8 +294,7 @@ class MqttTemplate(MqttAvailability, Light): """Return the current effect.""" return self._effect - @asyncio.coroutine - def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Turn the entity on. This method is a coroutine. @@ -339,8 +352,7 @@ class MqttTemplate(MqttAvailability, Light): if self._optimistic: self.async_schedule_update_ha_state() - @asyncio.coroutine - def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Turn the entity off. This method is a coroutine. diff --git a/tests/components/light/test_mqtt.py b/tests/components/light/test_mqtt.py index 7f7841b1a69..8b51adb2187 100644 --- a/tests/components/light/test_mqtt.py +++ b/tests/components/light/test_mqtt.py @@ -140,14 +140,16 @@ light: """ import unittest from unittest import mock +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( assert_setup_component, get_test_home_assistant, mock_mqtt_component, - fire_mqtt_message) + fire_mqtt_message, mock_coro) class TestLightMQTT(unittest.TestCase): @@ -481,12 +483,23 @@ class TestLightMQTT(unittest.TestCase): 'payload_on': 'on', 'payload_off': 'off' }} - - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, config) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + with patch('homeassistant.components.light.mqtt.async_get_last_state', + return_value=mock_coro(fake_state)): + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, config) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) light.turn_on(self.hass, 'light.test') diff --git a/tests/components/light/test_mqtt_json.py b/tests/components/light/test_mqtt_json.py index 5bae1061b7f..275fb42ede9 100644 --- a/tests/components/light/test_mqtt_json.py +++ b/tests/components/light/test_mqtt_json.py @@ -90,15 +90,17 @@ light: import json import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) + assert_setup_component, mock_coro) class TestLightMQTTJSON(unittest.TestCase): @@ -284,22 +286,36 @@ class TestLightMQTTJSON(unittest.TestCase): def test_sending_mqtt_commands_and_optimistic(self): \ # pylint: disable=invalid-name """Test the sending of command in optimistic mode.""" - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_json', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'brightness': True, - 'color_temp': True, - 'effect': True, - 'rgb': True, - 'white_value': True, - 'qos': 2 - } - }) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + + with patch('homeassistant.components.light.mqtt_json' + '.async_get_last_state', + return_value=mock_coro(fake_state)): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_json', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'brightness': True, + 'color_temp': True, + 'effect': True, + 'rgb': True, + 'white_value': True, + 'qos': 2 + } + }) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertEqual(191, state.attributes.get(ATTR_SUPPORTED_FEATURES)) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) diff --git a/tests/components/light/test_mqtt_template.py b/tests/components/light/test_mqtt_template.py index 90d68dd10d2..1440a73f98e 100644 --- a/tests/components/light/test_mqtt_template.py +++ b/tests/components/light/test_mqtt_template.py @@ -27,14 +27,16 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ import unittest +from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ON, STATE_OFF, STATE_UNAVAILABLE, ATTR_ASSUMED_STATE) import homeassistant.components.light as light +import homeassistant.core as ha from tests.common import ( get_test_home_assistant, mock_mqtt_component, fire_mqtt_message, - assert_setup_component) + assert_setup_component, mock_coro) class TestLightMQTTTemplate(unittest.TestCase): @@ -207,26 +209,40 @@ class TestLightMQTTTemplate(unittest.TestCase): def test_optimistic(self): \ # pylint: disable=invalid-name """Test optimistic mode.""" - with assert_setup_component(1, light.DOMAIN): - assert setup_component(self.hass, light.DOMAIN, { - light.DOMAIN: { - 'platform': 'mqtt_template', - 'name': 'test', - 'command_topic': 'test_light_rgb/set', - 'command_on_template': 'on,' - '{{ brightness|d }},' - '{{ color_temp|d }},' - '{{ white_value|d }},' - '{{ red|d }}-' - '{{ green|d }}-' - '{{ blue|d }}', - 'command_off_template': 'off', - 'qos': 2 - } - }) + fake_state = ha.State('light.test', 'on', {'brightness': 95, + 'hs_color': [100, 100], + 'effect': 'random', + 'color_temp': 100, + 'white_value': 50}) + + with patch('homeassistant.components.light.mqtt_template' + '.async_get_last_state', + return_value=mock_coro(fake_state)): + with assert_setup_component(1, light.DOMAIN): + assert setup_component(self.hass, light.DOMAIN, { + light.DOMAIN: { + 'platform': 'mqtt_template', + 'name': 'test', + 'command_topic': 'test_light_rgb/set', + 'command_on_template': 'on,' + '{{ brightness|d }},' + '{{ color_temp|d }},' + '{{ white_value|d }},' + '{{ red|d }}-' + '{{ green|d }}-' + '{{ blue|d }}', + 'command_off_template': 'off', + 'qos': 2 + } + }) state = self.hass.states.get('light.test') - self.assertEqual(STATE_OFF, state.state) + self.assertEqual(STATE_ON, state.state) + self.assertEqual(95, state.attributes.get('brightness')) + self.assertEqual((100, 100), state.attributes.get('hs_color')) + self.assertEqual('random', state.attributes.get('effect')) + self.assertEqual(100, state.attributes.get('color_temp')) + self.assertEqual(50, state.attributes.get('white_value')) self.assertTrue(state.attributes.get(ATTR_ASSUMED_STATE)) # turn on the light From 612a37b2dd37f4856ac7103bb7bc6f7dc6d8b970 Mon Sep 17 00:00:00 2001 From: c727 Date: Tue, 15 May 2018 16:57:51 +0200 Subject: [PATCH 087/144] Remove simplepush.io (#14358) --- .coveragerc | 1 - homeassistant/components/notify/simplepush.py | 59 ------------------- requirements_all.txt | 3 - 3 files changed, 63 deletions(-) delete mode 100644 homeassistant/components/notify/simplepush.py diff --git a/.coveragerc b/.coveragerc index 431c2f6f976..d95bcb63b73 100644 --- a/.coveragerc +++ b/.coveragerc @@ -540,7 +540,6 @@ omit = homeassistant/components/notify/rest.py homeassistant/components/notify/rocketchat.py homeassistant/components/notify/sendgrid.py - homeassistant/components/notify/simplepush.py homeassistant/components/notify/slack.py homeassistant/components/notify/smtp.py homeassistant/components/notify/stride.py diff --git a/homeassistant/components/notify/simplepush.py b/homeassistant/components/notify/simplepush.py deleted file mode 100644 index 9d5c58fc5b1..00000000000 --- a/homeassistant/components/notify/simplepush.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -Simplepush notification service. - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/notify.simplepush/ -""" -import logging - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService) -from homeassistant.const import CONF_PASSWORD - -REQUIREMENTS = ['simplepush==1.1.4'] - -_LOGGER = logging.getLogger(__name__) - -ATTR_ENCRYPTED = 'encrypted' - -CONF_DEVICE_KEY = 'device_key' -CONF_EVENT = 'event' -CONF_SALT = 'salt' - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_DEVICE_KEY): cv.string, - vol.Optional(CONF_EVENT): cv.string, - vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, - vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, -}) - - -def get_service(hass, config, discovery_info=None): - """Get the Simplepush notification service.""" - return SimplePushNotificationService(config) - - -class SimplePushNotificationService(BaseNotificationService): - """Implementation of the notification service for Simplepush.""" - - def __init__(self, config): - """Initialize the Simplepush notification service.""" - self._device_key = config.get(CONF_DEVICE_KEY) - self._event = config.get(CONF_EVENT) - self._password = config.get(CONF_PASSWORD) - self._salt = config.get(CONF_SALT) - - def send_message(self, message='', **kwargs): - """Send a message to a Simplepush user.""" - from simplepush import send, send_encrypted - - title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - - if self._password: - send_encrypted(self._device_key, self._password, self._salt, title, - message, event=self._event) - else: - send(self._device_key, title, message, event=self._event) diff --git a/requirements_all.txt b/requirements_all.txt index d1da68360be..820a316a238 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1180,9 +1180,6 @@ sharp_aquos_rc==0.3.2 # homeassistant.components.sensor.shodan shodan==1.7.7 -# homeassistant.components.notify.simplepush -simplepush==1.1.4 - # homeassistant.components.alarm_control_panel.simplisafe simplisafe-python==1.0.5 From de50d5d9c1c0375c5e46042f3168d356c73f6aa1 Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Tue, 15 May 2018 13:58:00 -0400 Subject: [PATCH 088/144] Add Konnected component with support for discovery, binary sensor and switch (#13670) * Add Konnected component with support for discovery, binary sensor, and switch Co-authored-by: Eitan Mosenkis * Use more built-in constants from const.py * Fix switch actuation with low-level trigger * Quiet logging; Improve schema validation. * Execute sync request outside of event loop * Whitespace cleanup * Cleanup config validation; async device setup * Update API endpoint for Konnected 2.2.0 changes * Update async coroutines via @OttoWinter * Make backwards compatible with Konnected < 2.2.0 * Add constants suggested by @syssi * Add to CODEOWNERS * Remove TODO comment --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/binary_sensor/konnected.py | 74 ++++ homeassistant/components/discovery.py | 3 + homeassistant/components/konnected.py | 315 ++++++++++++++++++ homeassistant/components/switch/konnected.py | 87 +++++ requirements_all.txt | 3 + 7 files changed, 487 insertions(+) create mode 100644 homeassistant/components/binary_sensor/konnected.py create mode 100644 homeassistant/components/konnected.py create mode 100644 homeassistant/components/switch/konnected.py diff --git a/.coveragerc b/.coveragerc index d95bcb63b73..2eeb3a1530d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -153,6 +153,9 @@ omit = homeassistant/components/knx.py homeassistant/components/*/knx.py + homeassistant/components/konnected.py + homeassistant/components/*/konnected.py + homeassistant/components/lametric.py homeassistant/components/*/lametric.py diff --git a/CODEOWNERS b/CODEOWNERS index 33966d1badb..32639fed43c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -94,6 +94,8 @@ homeassistant/components/*/hive.py @Rendili @KJonline homeassistant/components/homekit/* @cdce8p homeassistant/components/knx.py @Julius2342 homeassistant/components/*/knx.py @Julius2342 +homeassistant/components/konnected.py @heythisisnate +homeassistant/components/*/konnected.py @heythisisnate homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py new file mode 100644 index 00000000000..c7e2b7c84fe --- /dev/null +++ b/homeassistant/components/binary_sensor/konnected.py @@ -0,0 +1,74 @@ +""" +Support for wired binary sensors attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.konnected/ +""" +import asyncio +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) +from homeassistant.const import ( + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_STATE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up binary sensors attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN] + device_id = discovery_info['device_id'] + sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_BINARY_SENSORS].items()] + async_add_devices(sensors) + + +class KonnectedBinarySensor(BinarySensorDevice): + """Representation of a Konnected binary sensor.""" + + def __init__(self, device_id, pin_num, data): + """Initialize the binary sensor.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get(ATTR_STATE) + self._device_class = self._data.get(CONF_TYPE) + self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._data['entity'] = self + _LOGGER.debug('Created new Konnected sensor: %s', self._name) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @asyncio.coroutine + def async_set_state(self, state): + """Update the sensor's state.""" + self._state = state + self._data[ATTR_STATE] = state + self.async_schedule_update_ha_state() diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 68cf293ce48..a24e82da106 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -37,6 +37,7 @@ SERVICE_WINK = 'wink' SERVICE_XIAOMI_GW = 'xiaomi_gw' SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' +SERVICE_KONNECTED = 'konnected' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' SERVICE_SABNZBD = 'sabnzbd' @@ -62,6 +63,7 @@ SERVICE_HANDLERS = { SERVICE_DAIKIN: ('daikin', None), SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), + SERVICE_KONNECTED: ('konnected', None), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), 'plex_mediaserver': ('media_player', 'plex'), @@ -191,6 +193,7 @@ def _discover(netdisco): for disc in netdisco.discover(): for service in netdisco.get_info(disc): results.append((disc, service)) + finally: netdisco.stop() diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py new file mode 100644 index 00000000000..8c5578f10e4 --- /dev/null +++ b/homeassistant/components/konnected.py @@ -0,0 +1,315 @@ +""" +Support for Konnected devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/konnected/ +""" +import logging +import hmac +import json +import voluptuous as vol + +from aiohttp.hdrs import AUTHORIZATION +from aiohttp.web import Request, Response # NOQA + +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA +from homeassistant.components.discovery import SERVICE_KONNECTED +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import ( + HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, + CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, + CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, + ATTR_STATE) +from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['konnected==0.1.2'] + +DOMAIN = 'konnected' + +CONF_ACTIVATION = 'activation' +STATE_LOW = 'low' +STATE_HIGH = 'high' + +PIN_TO_ZONE = {1: 1, 2: 2, 5: 3, 6: 4, 7: 5, 8: 'out', 9: 6} +ZONE_TO_PIN = {zone: pin for pin, zone in PIN_TO_ZONE.items()} + +_BINARY_SENSOR_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 's_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 's_pin'): vol.Any(*ZONE_TO_PIN), + vol.Required(CONF_TYPE): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +_SWITCH_SCHEMA = vol.All( + vol.Schema({ + vol.Exclusive(CONF_PIN, 'a_pin'): vol.Any(*PIN_TO_ZONE), + vol.Exclusive(CONF_ZONE, 'a_pin'): vol.Any(*ZONE_TO_PIN), + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): + vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)) + }), cv.has_at_least_one_key(CONF_PIN, CONF_ZONE) +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema({ + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Required(CONF_DEVICES): [{ + vol.Required(CONF_ID): cv.string, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [_BINARY_SENSOR_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [_SWITCH_SCHEMA]), + }], + }), + }, + extra=vol.ALLOW_EXTRA, +) + +DEPENDENCIES = ['http', 'discovery'] + +ENDPOINT_ROOT = '/api/konnected' +UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') + + +async def async_setup(hass, config): + """Set up the Konnected platform.""" + cfg = config.get(DOMAIN) + if cfg is None: + cfg = {} + + access_token = cfg.get(CONF_ACCESS_TOKEN) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token} + + async def async_device_discovered(service, info): + """Call when a Konnected device has been discovered.""" + _LOGGER.debug("Discovered a new Konnected device: %s", info) + host = info.get(CONF_HOST) + port = info.get(CONF_PORT) + + device = KonnectedDevice(hass, host, port, cfg) + await device.async_setup() + + discovery.async_listen( + hass, + SERVICE_KONNECTED, + async_device_discovered) + + hass.http.register_view(KonnectedView(access_token)) + + return True + + +class KonnectedDevice(object): + """A representation of a single Konnected device.""" + + def __init__(self, hass, host, port, config): + """Initialize the Konnected device.""" + self.hass = hass + self.host = host + self.port = port + self.user_config = config + + import konnected + self.client = konnected.Client(host, str(port)) + self.status = self.client.get_status() + _LOGGER.info('Initialized Konnected device %s', self.device_id) + + async def async_setup(self): + """Set up a newly discovered Konnected device.""" + user_config = self.config() + if user_config: + _LOGGER.debug('Configuring Konnected device %s', self.device_id) + self.save_data() + await self.async_sync_device_config() + await discovery.async_load_platform( + self.hass, 'binary_sensor', + DOMAIN, {'device_id': self.device_id}) + await discovery.async_load_platform( + self.hass, 'switch', DOMAIN, + {'device_id': self.device_id}) + + @property + def device_id(self): + """Device id is the MAC address as string with punctuation removed.""" + return self.status['mac'].replace(':', '') + + def config(self): + """Return an object representing the user defined configuration.""" + device_id = self.device_id + valid_keys = [device_id, device_id.upper(), + device_id[6:], device_id.upper()[6:]] + configured_devices = self.user_config[CONF_DEVICES] + return next((device for device in + configured_devices if device[CONF_ID] in valid_keys), + None) + + def save_data(self): + """Save the device configuration to `hass.data`.""" + sensors = {} + for entity in self.config().get(CONF_BINARY_SENSORS) or []: + if CONF_ZONE in entity: + pin = ZONE_TO_PIN[entity[CONF_ZONE]] + else: + pin = entity[CONF_PIN] + + sensor_status = next((sensor for sensor in + self.status.get('sensors') if + sensor.get(CONF_PIN) == pin), {}) + if sensor_status.get(ATTR_STATE): + initial_state = bool(int(sensor_status.get(ATTR_STATE))) + else: + initial_state = None + + sensors[pin] = { + CONF_TYPE: entity[CONF_TYPE], + CONF_NAME: entity.get(CONF_NAME, 'Konnected {} Zone {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state + } + _LOGGER.debug('Set up sensor %s (initial state: %s)', + sensors[pin].get('name'), + sensors[pin].get(ATTR_STATE)) + + actuators = {} + for entity in self.config().get(CONF_SWITCHES) or []: + if 'zone' in entity: + pin = ZONE_TO_PIN[entity['zone']] + else: + pin = entity['pin'] + + actuator_status = next((actuator for actuator in + self.status.get('actuators') if + actuator.get('pin') == pin), {}) + if actuator_status.get(ATTR_STATE): + initial_state = bool(int(actuator_status.get(ATTR_STATE))) + else: + initial_state = None + + actuators[pin] = { + CONF_NAME: entity.get( + CONF_NAME, 'Konnected {} Actuator {}'.format( + self.device_id[6:], PIN_TO_ZONE[pin])), + ATTR_STATE: initial_state, + CONF_ACTIVATION: entity[CONF_ACTIVATION], + } + _LOGGER.debug('Set up actuator %s (initial state: %s)', + actuators[pin].get(CONF_NAME), + actuators[pin].get(ATTR_STATE)) + + device_data = { + 'client': self.client, + CONF_BINARY_SENSORS: sensors, + CONF_SWITCHES: actuators, + CONF_HOST: self.host, + CONF_PORT: self.port, + } + + if CONF_DEVICES not in self.hass.data[DOMAIN]: + self.hass.data[DOMAIN][CONF_DEVICES] = {} + + _LOGGER.debug('Storing data in hass.data[konnected]: %s', device_data) + self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] = device_data + + @property + def stored_configuration(self): + """Return the configuration stored in `hass.data` for this device.""" + return self.hass.data[DOMAIN][CONF_DEVICES][self.device_id] + + def sensor_configuration(self): + """Return the configuration map for syncing sensors.""" + return [{'pin': p} for p in + self.stored_configuration[CONF_BINARY_SENSORS].keys()] + + def actuator_configuration(self): + """Return the configuration map for syncing actuators.""" + return [{'pin': p, + 'trigger': (0 if data.get(CONF_ACTIVATION) in [0, STATE_LOW] + else 1)} + for p, data in + self.stored_configuration[CONF_SWITCHES].items()] + + async def async_sync_device_config(self): + """Sync the new pin configuration to the Konnected device.""" + desired_sensor_configuration = self.sensor_configuration() + current_sensor_configuration = [ + {'pin': s[CONF_PIN]} for s in self.status.get('sensors')] + _LOGGER.debug('%s: desired sensor config: %s', self.device_id, + desired_sensor_configuration) + _LOGGER.debug('%s: current sensor config: %s', self.device_id, + current_sensor_configuration) + + desired_actuator_config = self.actuator_configuration() + current_actuator_config = self.status.get('actuators') + _LOGGER.debug('%s: desired actuator config: %s', self.device_id, + desired_actuator_config) + _LOGGER.debug('%s: current actuator config: %s', self.device_id, + current_actuator_config) + + if (desired_sensor_configuration != current_sensor_configuration) or \ + (current_actuator_config != desired_actuator_config): + _LOGGER.debug('pushing settings to device %s', self.device_id) + self.client.put_settings( + desired_sensor_configuration, + desired_actuator_config, + self.hass.data[DOMAIN].get(CONF_ACCESS_TOKEN), + self.hass.config.api.base_url + ENDPOINT_ROOT + ) + + +class KonnectedView(HomeAssistantView): + """View creates an endpoint to receive push updates from the device.""" + + url = UPDATE_ENDPOINT + extra_urls = [UPDATE_ENDPOINT + '/{pin_num}/{state}'] + name = 'api:konnected' + requires_auth = False # Uses access token from configuration + + def __init__(self, auth_token): + """Initialize the view.""" + self.auth_token = auth_token + + async def put(self, request: Request, device_id, + pin_num=None, state=None) -> Response: + """Receive a sensor update via PUT request and async set state.""" + hass = request.app['hass'] + data = hass.data[DOMAIN] + + try: # Konnected 2.2.0 and above supports JSON payloads + payload = await request.json() + pin_num = payload['pin'] + state = payload['state'] + except json.decoder.JSONDecodeError: + _LOGGER.warning(("Your Konnected device software may be out of " + "date. Visit https://help.konnected.io for " + "updating instructions.")) + + auth = request.headers.get(AUTHORIZATION, None) + if not hmac.compare_digest('Bearer {}'.format(self.auth_token), auth): + return self.json_message( + "unauthorized", status_code=HTTP_UNAUTHORIZED) + pin_num = int(pin_num) + state = bool(int(state)) + device = data[CONF_DEVICES].get(device_id) + if device is None: + return self.json_message('unregistered device', + status_code=HTTP_BAD_REQUEST) + pin_data = device[CONF_BINARY_SENSORS].get(pin_num) or \ + device[CONF_SWITCHES].get(pin_num) + + if pin_data is None: + return self.json_message('unregistered sensor/actuator', + status_code=HTTP_BAD_REQUEST) + entity = pin_data.get('entity') + if entity is None: + return self.json_message('uninitialized sensor/actuator', + status_code=HTTP_INTERNAL_SERVER_ERROR) + + await entity.async_set_state(state) + return self.json_message('ok') diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py new file mode 100644 index 00000000000..e88f9826678 --- /dev/null +++ b/homeassistant/components/switch/konnected.py @@ -0,0 +1,87 @@ +""" +Support for wired switches attached to a Konnected device. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.konnected/ +""" + +import asyncio +import logging + +from homeassistant.components.konnected import ( + DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, STATE_LOW, STATE_HIGH) +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import (CONF_DEVICES, CONF_SWITCHES, ATTR_STATE) + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['konnected'] + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set switches attached to a Konnected device.""" + if discovery_info is None: + return + + data = hass.data[DOMAIN] + device_id = discovery_info['device_id'] + client = data[CONF_DEVICES][device_id]['client'] + switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) + for pin_num, pin_data in + data[CONF_DEVICES][device_id][CONF_SWITCHES].items()] + async_add_devices(switches) + + +class KonnectedSwitch(ToggleEntity): + """Representation of a Konnected switch.""" + + def __init__(self, device_id, pin_num, data, client): + """Initialize the switch.""" + self._data = data + self._device_id = device_id + self._pin_num = pin_num + self._state = self._data.get(ATTR_STATE) + self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) + self._name = self._data.get( + 'name', 'Konnected {} Actuator {}'.format( + device_id, PIN_TO_ZONE[pin_num])) + self._data['entity'] = self + self._client = client + _LOGGER.debug('Created new switch: %s', self._name) + + @property + def name(self): + """Return the name of the switch.""" + return self._name + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + def turn_on(self, **kwargs): + """Send a command to turn on the switch.""" + self._client.put_device(self._pin_num, + int(self._activation == STATE_HIGH)) + self._set_state(True) + + def turn_off(self, **kwargs): + """Send a command to turn off the switch.""" + self._client.put_device(self._pin_num, + int(self._activation == STATE_LOW)) + self._set_state(False) + + def _set_state(self, state): + self._state = state + self._data[ATTR_STATE] = state + self.schedule_update_ha_state() + _LOGGER.debug('Setting status of %s actuator pin %s to %s', + self._device_id, self.name, state) + + @asyncio.coroutine + def async_set_state(self, state): + """Update the switch's state.""" + self._state = state + self._data[ATTR_STATE] = state + self.async_schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 820a316a238..a3f8aa13258 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -464,6 +464,9 @@ keyring==12.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1 +# homeassistant.components.konnected +konnected==0.1.2 + # homeassistant.components.eufy lakeside==0.6 From e49e0b5a13f72bf9413086dc9570bf3f224cf6f7 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 16 May 2018 04:43:27 +1000 Subject: [PATCH 089/144] Make Feedreader component more extendable (#14342) * moved regular updates definition to own method to be able to override behaviour in subclass * moved filter by max entries to own method to be able to override behaviour in subclass * event type used when firing events to the bus now based on variable to be able to override behaviour in subclass * feed id introduced instead of url for storing meta-data about the feed to be able to fetch the same feed from different configs with different filtering rules applied * keep the status of the last update; continue processing the entries retrieved even if a recoverable error was detected while fetching the feed * added test cases for feedreader component * better explanation around breaking change * fixing lint issues and hound violations * fixing lint issue * using assert_called_once_with instead of assert_called_once to make it compatible with python 3.5 --- .coveragerc | 1 - homeassistant/components/feedreader.py | 61 +++++++--- tests/components/test_feedreader.py | 149 +++++++++++++++++++++++++ tests/fixtures/feedreader.xml | 20 ++++ tests/fixtures/feedreader1.xml | 27 +++++ tests/fixtures/feedreader2.xml | 97 ++++++++++++++++ tests/fixtures/feedreader3.xml | 26 +++++ 7 files changed, 362 insertions(+), 19 deletions(-) create mode 100644 tests/components/test_feedreader.py create mode 100644 tests/fixtures/feedreader.xml create mode 100644 tests/fixtures/feedreader1.xml create mode 100644 tests/fixtures/feedreader2.xml create mode 100644 tests/fixtures/feedreader3.xml diff --git a/.coveragerc b/.coveragerc index 2eeb3a1530d..3ecf2411384 100644 --- a/.coveragerc +++ b/.coveragerc @@ -421,7 +421,6 @@ omit = homeassistant/components/emoncms_history.py homeassistant/components/emulated_hue/upnp.py homeassistant/components/fan/mqtt.py - homeassistant/components/feedreader.py homeassistant/components/folder_watcher.py homeassistant/components/foursquare.py homeassistant/components/goalfeed.py diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 2c0e146491a..61fbe9f3171 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -55,16 +55,28 @@ class FeedManager(object): self._firstrun = True self._storage = storage self._last_entry_timestamp = None + self._last_update_successful = False self._has_published_parsed = False + self._event_type = EVENT_FEEDREADER + self._feed_id = url hass.bus.listen_once( EVENT_HOMEASSISTANT_START, lambda _: self._update()) - track_utc_time_change( - hass, lambda now: self._update(), minute=0, second=0) + self._init_regular_updates(hass) def _log_no_entries(self): """Send no entries log at debug level.""" _LOGGER.debug("No new entries to be published in feed %s", self._url) + def _init_regular_updates(self, hass): + """Schedule regular updates at the top of the clock.""" + track_utc_time_change( + hass, lambda now: self._update(), minute=0, second=0) + + @property + def last_update_successful(self): + """Return True if the last feed update was successful.""" + return self._last_update_successful + def _update(self): """Update the feed and publish new entries to the event bus.""" import feedparser @@ -76,26 +88,39 @@ class FeedManager(object): else self._feed.get('modified')) if not self._feed: _LOGGER.error("Error fetching feed data from %s", self._url) + self._last_update_successful = False else: + # The 'bozo' flag really only indicates that there was an issue + # during the initial parsing of the XML, but it doesn't indicate + # whether this is an unrecoverable error. In this case the + # feedparser lib is trying a less strict parsing approach. + # If an error is detected here, log error message but continue + # processing the feed entries if present. if self._feed.bozo != 0: - _LOGGER.error("Error parsing feed %s", self._url) + _LOGGER.error("Error parsing feed %s: %s", self._url, + self._feed.bozo_exception) # Using etag and modified, if there's no new data available, # the entries list will be empty - elif self._feed.entries: + if self._feed.entries: _LOGGER.debug("%s entri(es) available in feed %s", len(self._feed.entries), self._url) - if len(self._feed.entries) > MAX_ENTRIES: - _LOGGER.debug("Processing only the first %s entries " - "in feed %s", MAX_ENTRIES, self._url) - self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + self._filter_entries() self._publish_new_entries() if self._has_published_parsed: self._storage.put_timestamp( - self._url, self._last_entry_timestamp) + self._feed_id, self._last_entry_timestamp) else: self._log_no_entries() + self._last_update_successful = True _LOGGER.info("Fetch from feed %s completed", self._url) + def _filter_entries(self): + """Filter the entries provided and return the ones to keep.""" + if len(self._feed.entries) > MAX_ENTRIES: + _LOGGER.debug("Processing only the first %s entries " + "in feed %s", MAX_ENTRIES, self._url) + self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" # We are lucky, `published_parsed` data available, let's make use of @@ -109,12 +134,12 @@ class FeedManager(object): _LOGGER.debug("No published_parsed info available for entry %s", entry.title) entry.update({'feed_url': self._url}) - self._hass.bus.fire(EVENT_FEEDREADER, entry) + self._hass.bus.fire(self._event_type, entry) def _publish_new_entries(self): """Publish new entries to the event bus.""" new_entries = False - self._last_entry_timestamp = self._storage.get_timestamp(self._url) + self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) if self._last_entry_timestamp: self._firstrun = False else: @@ -157,18 +182,18 @@ class StoredData(object): _LOGGER.error("Error loading data from pickled file %s", self._data_file) - def get_timestamp(self, url): - """Return stored timestamp for given url.""" + def get_timestamp(self, feed_id): + """Return stored timestamp for given feed id (usually the url).""" self._fetch_data() - return self._data.get(url) + return self._data.get(feed_id) - def put_timestamp(self, url, timestamp): - """Update timestamp for given URL.""" + def put_timestamp(self, feed_id, timestamp): + """Update timestamp for given feed id (usually the url).""" self._fetch_data() with self._lock, open(self._data_file, 'wb') as myfile: - self._data.update({url: timestamp}) + self._data.update({feed_id: timestamp}) _LOGGER.debug("Overwriting feed %s timestamp in storage file %s", - url, self._data_file) + feed_id, self._data_file) try: pickle.dump(self._data, myfile) except: # noqa: E722 # pylint: disable=bare-except diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py new file mode 100644 index 00000000000..2288e21e37a --- /dev/null +++ b/tests/components/test_feedreader.py @@ -0,0 +1,149 @@ +"""The tests for the feedreader component.""" +import time +from datetime import datetime + +import unittest +from genericpath import exists +from logging import getLogger +from os import remove +from unittest import mock +from unittest.mock import patch + +from homeassistant.components import feedreader +from homeassistant.components.feedreader import CONF_URLS, FeedManager, \ + StoredData, EVENT_FEEDREADER +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import callback +from homeassistant.setup import setup_component +from tests.common import get_test_home_assistant, assert_setup_component, \ + load_fixture + +_LOGGER = getLogger(__name__) + +URL = 'http://some.rss.local/rss_feed.xml' +VALID_CONFIG_1 = { + feedreader.DOMAIN: { + CONF_URLS: [URL] + } +} + + +class TestFeedreaderComponent(unittest.TestCase): + """Test the feedreader component.""" + + def setUp(self): + """Initialize values for this testcase class.""" + self.hass = get_test_home_assistant() + # Delete any previously stored data + data_file = self.hass.config.path("{}.pickle".format('feedreader')) + if exists(data_file): + remove(data_file) + + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_setup_one_feed(self): + """Test the general setup of this component.""" + with assert_setup_component(1, 'feedreader'): + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_1)) + + def setup_manager(self, feed_data): + """Generic test setup method.""" + events = [] + + @callback + def record_event(event): + """Add recorded event to set.""" + events.append(event) + + self.hass.bus.listen(EVENT_FEEDREADER, record_event) + + # Loading raw data from fixture and plug in to data object as URL + # works since the third-party feedparser library accepts a URL + # as well as the actual data. + data_file = self.hass.config.path("{}.pickle".format( + feedreader.DOMAIN)) + storage = StoredData(data_file) + with patch("homeassistant.components.feedreader." + "track_utc_time_change") as track_method: + manager = FeedManager(feed_data, self.hass, storage) + # Can't use 'assert_called_once' here because it's not available + # in Python 3.5 yet. + track_method.assert_called_once_with(self.hass, mock.ANY, minute=0, + second=0) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + return manager, events + + def test_feed(self): + """Test simple feed with valid data.""" + feed_data = load_fixture('feedreader.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + assert events[0].data.title == "Title 1" + assert events[0].data.description == "Description 1" + assert events[0].data.link == "http://www.example.com/link/1" + assert events[0].data.id == "GUID 1" + assert datetime.fromtimestamp( + time.mktime(events[0].data.published_parsed)) == \ + datetime(2018, 4, 30, 5, 10, 0) + assert manager.last_update_successful is True + + def test_feed_updates(self): + """Test feed updates.""" + # 1. Run + feed_data = load_fixture('feedreader.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 1 + # 2. Run + feed_data2 = load_fixture('feedreader1.xml') + # Must patch 'get_timestamp' method because the timestamp is stored + # with the URL which in these tests is the raw XML data. + with patch("homeassistant.components.feedreader.StoredData." + "get_timestamp", return_value=time.struct_time( + (2018, 4, 30, 5, 10, 0, 0, 120, 0))): + manager2, events2 = self.setup_manager(feed_data2) + assert len(events2) == 1 + # 3. Run + feed_data3 = load_fixture('feedreader1.xml') + with patch("homeassistant.components.feedreader.StoredData." + "get_timestamp", return_value=time.struct_time( + (2018, 4, 30, 5, 11, 0, 0, 120, 0))): + manager3, events3 = self.setup_manager(feed_data3) + assert len(events3) == 0 + + def test_feed_max_length(self): + """Test long feed beyond the 20 entry limit.""" + feed_data = load_fixture('feedreader2.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 20 + + def test_feed_without_publication_date(self): + """Test simple feed with entry without publication date.""" + feed_data = load_fixture('feedreader3.xml') + manager, events = self.setup_manager(feed_data) + assert len(events) == 2 + + def test_feed_invalid_data(self): + """Test feed with invalid data.""" + feed_data = "INVALID DATA" + manager, events = self.setup_manager(feed_data) + assert len(events) == 0 + assert manager.last_update_successful is True + + @mock.patch('feedparser.parse', return_value=None) + def test_feed_parsing_failed(self, mock_parse): + """Test feed where parsing fails.""" + data_file = self.hass.config.path("{}.pickle".format( + feedreader.DOMAIN)) + storage = StoredData(data_file) + manager = FeedManager("FEED DATA", self.hass, storage) + # Artificially trigger update. + self.hass.bus.fire(EVENT_HOMEASSISTANT_START) + # Collect events. + self.hass.block_till_done() + assert manager.last_update_successful is False diff --git a/tests/fixtures/feedreader.xml b/tests/fixtures/feedreader.xml new file mode 100644 index 00000000000..8c85a4975ee --- /dev/null +++ b/tests/fixtures/feedreader.xml @@ -0,0 +1,20 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + + diff --git a/tests/fixtures/feedreader1.xml b/tests/fixtures/feedreader1.xml new file mode 100644 index 00000000000..ff856125779 --- /dev/null +++ b/tests/fixtures/feedreader1.xml @@ -0,0 +1,27 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 2 + Description 2 + http://www.example.com/link/2 + GUID 2 + Mon, 30 Apr 2018 15:11:00 +1000 + + + + diff --git a/tests/fixtures/feedreader2.xml b/tests/fixtures/feedreader2.xml new file mode 100644 index 00000000000..653a16e4561 --- /dev/null +++ b/tests/fixtures/feedreader2.xml @@ -0,0 +1,97 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Mon, 30 Apr 2018 15:00:00 +1000 + + + Title 2 + Mon, 30 Apr 2018 15:01:00 +1000 + + + Title 3 + Mon, 30 Apr 2018 15:02:00 +1000 + + + Title 4 + Mon, 30 Apr 2018 15:03:00 +1000 + + + Title 5 + Mon, 30 Apr 2018 15:04:00 +1000 + + + Title 6 + Mon, 30 Apr 2018 15:05:00 +1000 + + + Title 7 + Mon, 30 Apr 2018 15:06:00 +1000 + + + Title 8 + Mon, 30 Apr 2018 15:07:00 +1000 + + + Title 9 + Mon, 30 Apr 2018 15:08:00 +1000 + + + Title 10 + Mon, 30 Apr 2018 15:09:00 +1000 + + + Title 11 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 12 + Mon, 30 Apr 2018 15:11:00 +1000 + + + Title 13 + Mon, 30 Apr 2018 15:12:00 +1000 + + + Title 14 + Mon, 30 Apr 2018 15:13:00 +1000 + + + Title 15 + Mon, 30 Apr 2018 15:14:00 +1000 + + + Title 16 + Mon, 30 Apr 2018 15:15:00 +1000 + + + Title 17 + Mon, 30 Apr 2018 15:16:00 +1000 + + + Title 18 + Mon, 30 Apr 2018 15:17:00 +1000 + + + Title 19 + Mon, 30 Apr 2018 15:18:00 +1000 + + + Title 20 + Mon, 30 Apr 2018 15:19:00 +1000 + + + Title 21 + Mon, 30 Apr 2018 15:20:00 +1000 + + + + diff --git a/tests/fixtures/feedreader3.xml b/tests/fixtures/feedreader3.xml new file mode 100644 index 00000000000..7b28e067cfe --- /dev/null +++ b/tests/fixtures/feedreader3.xml @@ -0,0 +1,26 @@ + + + + RSS Sample + This is an example of an RSS feed + http://www.example.com/main.html + Mon, 30 Apr 2018 12:00:00 +1000 + Mon, 30 Apr 2018 15:00:00 +1000 + 1800 + + + Title 1 + Description 1 + http://www.example.com/link/1 + GUID 1 + Mon, 30 Apr 2018 15:10:00 +1000 + + + Title 2 + Description 2 + http://www.example.com/link/2 + GUID 2 + + + + From 2e7b5dcd196782e2005577a6229ce4f4d168d814 Mon Sep 17 00:00:00 2001 From: Gerard Date: Tue, 15 May 2018 20:47:32 +0200 Subject: [PATCH 090/144] BMW code cleanup (#14424) * Some cleanup for BMW sensors * Changed dict sort * Updates based on review and Travis --- .../binary_sensor/bmw_connected_drive.py | 24 ++++++----- .../components/sensor/bmw_connected_drive.py | 40 ++++++++----------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/binary_sensor/bmw_connected_drive.py b/homeassistant/components/binary_sensor/bmw_connected_drive.py index af3ebd53b80..e214610f46d 100644 --- a/homeassistant/components/binary_sensor/bmw_connected_drive.py +++ b/homeassistant/components/binary_sensor/bmw_connected_drive.py @@ -115,14 +115,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['lights_parking'] = vehicle_state.parking_lights.value elif self._attribute == 'condition_based_services': for report in vehicle_state.condition_based_services: - service_type = report.service_type.lower().replace('_', ' ') - result['{} status'.format(service_type)] = report.state.value - if report.due_date is not None: - result['{} date'.format(service_type)] = \ - report.due_date.strftime('%Y-%m-%d') - if report.due_distance is not None: - result['{} distance'.format(service_type)] = \ - '{} km'.format(report.due_distance) + result.update(self._format_cbs_report(report)) elif self._attribute == 'check_control_messages': check_control_messages = vehicle_state.check_control_messages if not check_control_messages: @@ -139,7 +132,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): result['connection_status'] = \ vehicle_state._attributes['connectionStatus'] - return result + return sorted(result.items()) def update(self): """Read new state data from the library.""" @@ -177,6 +170,19 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._state = (vehicle_state._attributes['connectionStatus'] == 'CONNECTED') + @staticmethod + def _format_cbs_report(report): + result = {} + service_type = report.service_type.lower().replace('_', ' ') + result['{} status'.format(service_type)] = report.state.value + if report.due_date is not None: + result['{} date'.format(service_type)] = \ + report.due_date.strftime('%Y-%m-%d') + if report.due_distance is not None: + result['{} distance'.format(service_type)] = \ + '{} km'.format(report.due_distance) + return result + def update_callback(self): """Schedule a state update.""" self.schedule_update_ha_state(True) diff --git a/homeassistant/components/sensor/bmw_connected_drive.py b/homeassistant/components/sensor/bmw_connected_drive.py index 8e06836b102..e3331cdc763 100644 --- a/homeassistant/components/sensor/bmw_connected_drive.py +++ b/homeassistant/components/sensor/bmw_connected_drive.py @@ -15,6 +15,17 @@ DEPENDENCIES = ['bmw_connected_drive'] _LOGGER = logging.getLogger(__name__) +ATTR_TO_HA = { + 'mileage': ['mdi:speedometer', 'km'], + 'remaining_range_total': ['mdi:ruler', 'km'], + 'remaining_range_electric': ['mdi:ruler', 'km'], + 'remaining_range_fuel': ['mdi:ruler', 'km'], + 'max_range_electric': ['mdi:ruler', 'km'], + 'remaining_fuel': ['mdi:gas-station', 'l'], + 'charging_time_remaining': ['mdi:update', 'h'], + 'charging_status': ['mdi:battery-charging', None] +} + def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the BMW sensors.""" @@ -68,22 +79,12 @@ class BMWConnectedDriveSensor(Entity): charging_state = vehicle_state.charging_status in \ [ChargingState.CHARGING] - if self._attribute == 'mileage': - return 'mdi:speedometer' - elif self._attribute in ( - 'remaining_range_total', 'remaining_range_electric', - 'remaining_range_fuel', 'max_range_electric'): - return 'mdi:ruler' - elif self._attribute == 'remaining_fuel': - return 'mdi:gas-station' - elif self._attribute == 'charging_time_remaining': - return 'mdi:update' - elif self._attribute == 'charging_status': - return 'mdi:battery-charging' - elif self._attribute == 'charging_level_hv': + if self._attribute == 'charging_level_hv': return icon_for_battery_level( battery_level=vehicle_state.charging_level_hv, charging=charging_state) + icon, _ = ATTR_TO_HA.get(self._attribute, [None, None]) + return icon @property def state(self): @@ -97,17 +98,8 @@ class BMWConnectedDriveSensor(Entity): @property def unit_of_measurement(self) -> str: """Get the unit of measurement.""" - if self._attribute in ( - 'mileage', 'remaining_range_total', 'remaining_range_electric', - 'remaining_range_fuel', 'max_range_electric'): - return 'km' - elif self._attribute == 'remaining_fuel': - return 'l' - elif self._attribute == 'charging_time_remaining': - return 'h' - elif self._attribute == 'charging_level_hv': - return '%' - return None + _, unit = ATTR_TO_HA.get(self._attribute, [None, None]) + return unit @property def device_state_attributes(self): From df69680d2484c388b0a9ef1f722746afd7f86eb3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 May 2018 14:47:46 -0400 Subject: [PATCH 091/144] Don't add a url to built-in panels (#14456) * Don't add a url to built-in panels * Add url_path back * Lint * Frontend bump to 20180515.0 * Fix tests --- homeassistant/components/frontend/__init__.py | 71 ++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/test_frontend.py | 2 +- tests/components/test_panel_iframe.py | 7 -- 5 files changed, 31 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f60d095a682..68783a837cb 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180510.1'] +REQUIREMENTS = ['home-assistant-frontend==20180515.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -147,21 +147,6 @@ class AbstractPanel: 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path), index_view.get) - def to_response(self, hass, request): - """Panel as dictionary.""" - result = { - 'component_name': self.component_name, - 'icon': self.sidebar_icon, - 'title': self.sidebar_title, - 'url_path': self.frontend_url_path, - 'config': self.config, - } - if _is_latest(hass.data[DATA_JS_VERSION], request): - result['url'] = self.webcomponent_url_latest - else: - result['url'] = self.webcomponent_url_es5 - return result - class BuiltInPanel(AbstractPanel): """Panel that is part of hass_frontend.""" @@ -175,30 +160,15 @@ class BuiltInPanel(AbstractPanel): self.frontend_url_path = frontend_url_path or component_name self.config = config - @asyncio.coroutine - def async_finalize(self, hass, frontend_repository_path): - """Finalize this panel for usage. - - If frontend_repository_path is set, will be prepended to path of - built-in components. - """ - if frontend_repository_path is None: - import hass_frontend - import hass_frontend_es5 - - self.webcomponent_url_latest = \ - '/frontend_latest/panels/ha-panel-{}-{}.html'.format( - self.component_name, - hass_frontend.FINGERPRINTS[self.component_name]) - self.webcomponent_url_es5 = \ - '/frontend_es5/panels/ha-panel-{}-{}.html'.format( - self.component_name, - hass_frontend_es5.FINGERPRINTS[self.component_name]) - else: - # Dev mode - self.webcomponent_url_es5 = self.webcomponent_url_latest = \ - '/home-assistant-polymer/panels/{}/ha-panel-{}.html'.format( - self.component_name, self.component_name) + def to_response(self, hass, request): + """Panel as dictionary.""" + return { + 'component_name': self.component_name, + 'icon': self.sidebar_icon, + 'title': self.sidebar_title, + 'config': self.config, + 'url_path': self.frontend_url_path, + } class ExternalPanel(AbstractPanel): @@ -244,6 +214,21 @@ class ExternalPanel(AbstractPanel): frontend_repository_path is None) self.REGISTERED_COMPONENTS.add(self.component_name) + def to_response(self, hass, request): + """Panel as dictionary.""" + result = { + 'component_name': self.component_name, + 'icon': self.sidebar_icon, + 'title': self.sidebar_title, + 'url_path': self.frontend_url_path, + 'config': self.config, + } + if _is_latest(hass.data[DATA_JS_VERSION], request): + result['url'] = self.webcomponent_url_latest + else: + result['url'] = self.webcomponent_url_es5 + return result + @bind_hass @asyncio.coroutine @@ -365,10 +350,10 @@ def async_setup(hass, config): index_view = IndexView(repo_path, js_version, client) hass.http.register_view(index_view) - @asyncio.coroutine - def finalize_panel(panel): + async def finalize_panel(panel): """Finalize setup of a panel.""" - yield from panel.async_finalize(hass, repo_path) + if hasattr(panel, 'async_finalize'): + await panel.async_finalize(hass, repo_path) panel.async_register_index_routes(hass.http.app.router, index_view) yield from asyncio.wait([ diff --git a/requirements_all.txt b/requirements_all.txt index a3f8aa13258..2aad7805b02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180510.1 +home-assistant-frontend==20180515.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3e2e16dc57..55f62f062fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180510.1 +home-assistant-frontend==20180515.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb diff --git a/tests/components/test_frontend.py b/tests/components/test_frontend.py index 973544495d7..657497b868b 100644 --- a/tests/components/test_frontend.py +++ b/tests/components/test_frontend.py @@ -57,7 +57,7 @@ def test_frontend_and_static(mock_http_client): # Test we can retrieve frontend.js frontendjs = re.search( - r'(?P\/frontend_es5\/frontend-[A-Za-z0-9]{32}.html)', text) + r'(?P\/frontend_es5\/app-[A-Za-z0-9]{32}.js)', text) assert frontendjs is not None resp = yield from mock_http_client.get(frontendjs.groups(0)[0]) diff --git a/tests/components/test_panel_iframe.py b/tests/components/test_panel_iframe.py index 91a07511787..214eda04ad8 100644 --- a/tests/components/test_panel_iframe.py +++ b/tests/components/test_panel_iframe.py @@ -1,6 +1,5 @@ """The tests for the panel_iframe component.""" import unittest -from unittest.mock import patch from homeassistant import setup from homeassistant.components import frontend @@ -33,8 +32,6 @@ class TestPanelIframe(unittest.TestCase): 'panel_iframe': conf }) - @patch.dict('hass_frontend_es5.FINGERPRINTS', - {'iframe': 'md5md5'}) def test_correct_config(self): """Test correct config.""" assert setup.setup_component( @@ -70,7 +67,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'http://192.168.1.1'}, 'icon': 'mdi:network-wireless', 'title': 'Router', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'router' } @@ -79,7 +75,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'https://www.wunderground.com/us/ca/san-diego'}, 'icon': 'mdi:weather', 'title': 'Weather', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'weather', } @@ -88,7 +83,6 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': '/api'}, 'icon': 'mdi:weather', 'title': 'Api', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'api', } @@ -97,6 +91,5 @@ class TestPanelIframe(unittest.TestCase): 'config': {'url': 'ftp://some/ftp'}, 'icon': 'mdi:weather', 'title': 'FTP', - 'url': '/frontend_es5/panels/ha-panel-iframe-md5md5.html', 'url_path': 'ftp', } From 852ce9f990ca5c0cd16a8bd4abbfc3c88873e468 Mon Sep 17 00:00:00 2001 From: nordlead2005 Date: Tue, 15 May 2018 15:26:41 -0400 Subject: [PATCH 092/144] Added temperature (apparent) high/low, deprecated max/min (#12233) --- homeassistant/components/sensor/darksky.py | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/darksky.py b/homeassistant/components/sensor/darksky.py index ac09de9c699..e75f36d59f7 100644 --- a/homeassistant/components/sensor/darksky.py +++ b/homeassistant/components/sensor/darksky.py @@ -33,6 +33,11 @@ DEFAULT_LANGUAGE = 'en' DEFAULT_NAME = 'Dark Sky' +DEPRECATED_SENSOR_TYPES = {'apparent_temperature_max', + 'apparent_temperature_min', + 'temperature_max', + 'temperature_min'} + # Sensor types are defined like so: # Name, si unit, us unit, ca unit, uk unit, uk2 unit SENSOR_TYPES = { @@ -90,16 +95,28 @@ SENSOR_TYPES = { '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], + 'apparent_temperature_high': ["Daytime High Apparent Temperature", + '°C', '°F', '°C', '°C', '°C', + 'mdi:thermometer', ['daily']], 'apparent_temperature_min': ['Daily Low Apparent Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', ['currently', 'hourly', 'daily']], + 'apparent_temperature_low': ['Overnight Low Apparent Temperature', + '°C', '°F', '°C', '°C', '°C', + 'mdi:thermometer', ['daily']], 'temperature_max': ['Daily High Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['daily']], + 'temperature_high': ['Daytime High Temperature', + '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', + ['daily']], 'temperature_min': ['Daily Low Temperature', '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', - ['currently', 'hourly', 'daily']], + ['daily']], + 'temperature_low': ['Overnight Low Temperature', + '°C', '°F', '°C', '°C', '°C', 'mdi:thermometer', + ['daily']], 'precip_intensity_max': ['Daily Max Precip Intensity', 'mm/h', 'in', 'mm/h', 'mm/h', 'mm/h', 'mdi:thermometer', @@ -185,6 +202,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): forecast = config.get(CONF_FORECAST) sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: + if variable in DEPRECATED_SENSOR_TYPES: + _LOGGER.warning("Monitored condition %s is deprecated.", + variable) sensors.append(DarkSkySensor(forecast_data, variable, name)) if forecast is not None and 'daily' in SENSOR_TYPES[variable][7]: for forecast_day in forecast: @@ -288,9 +308,13 @@ class DarkSkySensor(Entity): elif self.forecast_day > 0 or ( self.type in ['daily_summary', 'temperature_min', + 'temperature_low', 'temperature_max', + 'temperature_high', 'apparent_temperature_min', + 'apparent_temperature_low', 'apparent_temperature_max', + 'apparent_temperature_high', 'precip_intensity_max', 'precip_accumulation']): self.forecast_data.update_daily() From 6ba49e12a261fcd02bcfd4eafaed7e5d2879d857 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Wed, 16 May 2018 07:35:43 +0200 Subject: [PATCH 093/144] Improve handling of offline Sonos devices (#14479) --- homeassistant/components/media_player/sonos.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index cc10355abe8..06e5f3befe4 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -682,11 +682,15 @@ class SonosDevice(MediaPlayerDevice): if group: # New group information is pushed coordinator_uid, *slave_uids = group.split(',') - else: + elif self.soco.group: # Use SoCo cache for existing topology coordinator_uid = self.soco.group.coordinator.uid slave_uids = [p.uid for p in self.soco.group.members if p.uid != coordinator_uid] + else: + # Not yet in the cache, this can happen when a speaker boots + coordinator_uid = self.unique_id + slave_uids = [] if self.unique_id == coordinator_uid: sonos_group = [] From 5ff5c73e2bf1918113197a2a7a9ed29fe69bc4d3 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Tue, 15 May 2018 23:00:57 -0700 Subject: [PATCH 094/144] "unavailable" Media players should be considered off in Universal player (#14466) The Universal media player inherits the states of the first child player that is not in some sort of "Off" state (including idle.) It was not considering the "unavailable" state to be off. Now it does. --- homeassistant/components/media_player/universal.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/universal.py b/homeassistant/components/media_player/universal.py index fa4f03f1179..03f847ae40c 100644 --- a/homeassistant/components/media_player/universal.py +++ b/homeassistant/components/media_player/universal.py @@ -30,7 +30,8 @@ from homeassistant.const import ( SERVICE_MEDIA_PLAY, SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_SEEK, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, - SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, SERVICE_MEDIA_STOP) + SERVICE_SHUFFLE_SET, STATE_IDLE, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, + SERVICE_MEDIA_STOP) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_call_from_config @@ -45,7 +46,7 @@ CONF_SERVICE_DATA = 'service_data' ATTR_DATA = 'data' CONF_STATE = 'state' -OFF_STATES = [STATE_IDLE, STATE_OFF] +OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE] REQUIREMENTS = [] _LOGGER = logging.getLogger(__name__) From 1533a68c064e6389a4bd9efe4b94b425004c7669 Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Wed, 16 May 2018 03:58:49 -0400 Subject: [PATCH 095/144] Added option to invert aREST pin switch logic for active low relays (#14467) * Added option to invert aREST pin switch logic for active low relays * Fixed line lengths * Changed naming and set optional invert default value. * Fixed line length * Removed default from get --- homeassistant/components/switch/arest.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/arest.py b/homeassistant/components/switch/arest.py index 6e31694fd2d..fd72d0728a0 100644 --- a/homeassistant/components/switch/arest.py +++ b/homeassistant/components/switch/arest.py @@ -18,11 +18,13 @@ _LOGGER = logging.getLogger(__name__) CONF_FUNCTIONS = 'functions' CONF_PINS = 'pins' +CONF_INVERT = 'invert' DEFAULT_NAME = 'aREST switch' PIN_FUNCTION_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INVERT, default=False): cv.boolean, }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -54,7 +56,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for pinnum, pin in pins.items(): dev.append(ArestSwitchPin( resource, config.get(CONF_NAME, response.json()[CONF_NAME]), - pin.get(CONF_NAME), pinnum)) + pin.get(CONF_NAME), pinnum, pin.get(CONF_INVERT))) functions = config.get(CONF_FUNCTIONS) for funcname, func in functions.items(): @@ -152,10 +154,11 @@ class ArestSwitchFunction(ArestSwitchBase): class ArestSwitchPin(ArestSwitchBase): """Representation of an aREST switch. Based on digital I/O.""" - def __init__(self, resource, location, name, pin): + def __init__(self, resource, location, name, pin, invert): """Initialize the switch.""" super().__init__(resource, location, name) self._pin = pin + self.invert = invert request = requests.get( '{}/mode/{}/o'.format(self._resource, self._pin), timeout=10) @@ -165,8 +168,11 @@ class ArestSwitchPin(ArestSwitchBase): def turn_on(self, **kwargs): """Turn the device on.""" + turn_on_payload = int(not self.invert) request = requests.get( - '{}/digital/{}/1'.format(self._resource, self._pin), timeout=10) + '{}/digital/{}/{}'.format(self._resource, self._pin, + turn_on_payload), + timeout=10) if request.status_code == 200: self._state = True else: @@ -175,8 +181,11 @@ class ArestSwitchPin(ArestSwitchBase): def turn_off(self, **kwargs): """Turn the device off.""" + turn_off_payload = int(self.invert) request = requests.get( - '{}/digital/{}/0'.format(self._resource, self._pin), timeout=10) + '{}/digital/{}/{}'.format(self._resource, self._pin, + turn_off_payload), + timeout=10) if request.status_code == 200: self._state = False else: @@ -188,7 +197,8 @@ class ArestSwitchPin(ArestSwitchBase): try: request = requests.get( '{}/digital/{}'.format(self._resource, self._pin), timeout=10) - self._state = request.json()['return_value'] != 0 + status_value = int(self.invert) + self._state = request.json()['return_value'] != status_value self._available = True except requests.exceptions.ConnectionError: _LOGGER.warning("No route to device %s", self._resource) From e20f88c143ecedd18cf502896282f3bd3fb4bd78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 16 May 2018 10:01:48 +0200 Subject: [PATCH 096/144] Use "Returns" consistently to avoid being treated as section (#14448) Otherwise, by side effect, results in error D413 by recent pydocstyle. --- tests/components/test_shell_command.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index 6f993732c38..c7cef78a127 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -19,8 +19,7 @@ def mock_process_creator(error: bool = False) -> asyncio.coroutine: def communicate() -> Tuple[bytes, bytes]: """Mock a coroutine that runs a process when yielded. - Returns: - a tuple of (stdout, stderr). + Returns a tuple of (stdout, stderr). """ return b"I am stdout", b"I am stderr" From 25dcddfeefc6ad98a93ea78d3ab37713a5c00051 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Wed, 16 May 2018 07:15:59 -0400 Subject: [PATCH 097/144] Add HomeKit support for fans (#14351) --- homeassistant/components/homekit/__init__.py | 8 +- homeassistant/components/homekit/const.py | 7 + homeassistant/components/homekit/type_fans.py | 116 ++++++++++++++ .../components/homekit/type_lights.py | 6 +- .../homekit/test_get_accessories.py | 1 + tests/components/homekit/test_type_covers.py | 10 +- tests/components/homekit/test_type_fans.py | 149 ++++++++++++++++++ tests/components/homekit/test_type_lights.py | 6 +- .../homekit/test_type_thermostats.py | 6 +- 9 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/homekit/type_fans.py create mode 100644 tests/components/homekit/test_type_fans.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 028155593fb..41b0791a352 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -115,6 +115,9 @@ def get_accessory(hass, state, aid, config): elif features & (SUPPORT_OPEN | SUPPORT_CLOSE): a_type = 'WindowCoveringBasic' + elif state.domain == 'fan': + a_type = 'Fan' + elif state.domain == 'light': a_type = 'Light' @@ -202,8 +205,9 @@ class HomeKit(): # pylint: disable=unused-variable from . import ( # noqa F401 - type_covers, type_lights, type_locks, type_security_systems, - type_sensors, type_switches, type_thermostats) + type_covers, type_fans, type_lights, type_locks, + type_security_systems, type_sensors, type_switches, + type_thermostats) for state in self.hass.states.all(): self.add_bridge_accessory(state) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ce46e84a2ef..adde13cc030 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -29,6 +29,7 @@ SERV_AIR_QUALITY_SENSOR = 'AirQualitySensor' SERV_CARBON_DIOXIDE_SENSOR = 'CarbonDioxideSensor' SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' +SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity SERV_LEAK_SENSOR = 'LeakSensor' @@ -46,6 +47,7 @@ SERV_WINDOW_COVERING = 'WindowCovering' # CurrentPosition, TargetPosition, PositionState # #### Characteristics #### +CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] @@ -77,9 +79,11 @@ CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' CHAR_ON = 'On' # boolean CHAR_POSITION_STATE = 'PositionState' +CHAR_ROTATION_DIRECTION = 'RotationDirection' CHAR_SATURATION = 'Saturation' # percent CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' +CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] @@ -88,6 +92,9 @@ CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### +PROP_MAX_VALUE = 'maxValue' +PROP_MIN_VALUE = 'minValue' + PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} # #### Device Class #### diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py new file mode 100644 index 00000000000..a3ea027c07e --- /dev/null +++ b/homeassistant/components/homekit/type_fans.py @@ -0,0 +1,116 @@ +"""Class to hold all light accessories.""" +import logging + +from pyhap.const import CATEGORY_FAN + +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_OSCILLATING, + DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, + SERVICE_TURN_OFF, SERVICE_TURN_ON) + +from . import TYPES +from .accessories import HomeAccessory +from .const import ( + CHAR_ACTIVE, CHAR_ROTATION_DIRECTION, CHAR_SWING_MODE, SERV_FANV2) + +_LOGGER = logging.getLogger(__name__) + + +@TYPES.register('Fan') +class Fan(HomeAccessory): + """Generate a Fan accessory for a fan entity. + + Currently supports: state, speed, oscillate, direction. + """ + + def __init__(self, *args): + """Initialize a new Light accessory object.""" + super().__init__(*args, category=CATEGORY_FAN) + self._flag = {CHAR_ACTIVE: False, + CHAR_ROTATION_DIRECTION: False, + CHAR_SWING_MODE: False} + self._state = 0 + + self.chars = [] + features = self.hass.states.get(self.entity_id) \ + .attributes.get(ATTR_SUPPORTED_FEATURES) + if features & SUPPORT_DIRECTION: + self.chars.append(CHAR_ROTATION_DIRECTION) + if features & SUPPORT_OSCILLATE: + self.chars.append(CHAR_SWING_MODE) + + serv_fan = self.add_preload_service(SERV_FANV2, self.chars) + self.char_active = serv_fan.configure_char( + CHAR_ACTIVE, value=0, setter_callback=self.set_state) + + if CHAR_ROTATION_DIRECTION in self.chars: + self.char_direction = serv_fan.configure_char( + CHAR_ROTATION_DIRECTION, value=0, + setter_callback=self.set_direction) + + if CHAR_SWING_MODE in self.chars: + self.char_swing = serv_fan.configure_char( + CHAR_SWING_MODE, value=0, setter_callback=self.set_oscillating) + + def set_state(self, value): + """Set state if call came from HomeKit.""" + if self._state == value: + return + + _LOGGER.debug('%s: Set state to %d', self.entity_id, value) + self._flag[CHAR_ACTIVE] = True + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) + + def set_direction(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) + self._flag[CHAR_ROTATION_DIRECTION] = True + direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_DIRECTION: direction} + self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) + + def set_oscillating(self, value): + """Set state if call came from HomeKit.""" + _LOGGER.debug('%s: Set oscillating to %d', self.entity_id, value) + self._flag[CHAR_SWING_MODE] = True + oscillating = True if value == 1 else False + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OSCILLATING: oscillating} + self.hass.services.call(DOMAIN, SERVICE_OSCILLATE, params) + + def update_state(self, new_state): + """Update fan after state change.""" + # Handle State + state = new_state.state + if state in (STATE_ON, STATE_OFF): + self._state = 1 if state == STATE_ON else 0 + if not self._flag[CHAR_ACTIVE] and \ + self.char_active.value != self._state: + self.char_active.set_value(self._state) + self._flag[CHAR_ACTIVE] = False + + # Handle Direction + if CHAR_ROTATION_DIRECTION in self.chars: + direction = new_state.attributes.get(ATTR_DIRECTION) + if not self._flag[CHAR_ROTATION_DIRECTION] and \ + direction in (DIRECTION_FORWARD, DIRECTION_REVERSE): + hk_direction = 1 if direction == DIRECTION_REVERSE else 0 + if self.char_direction.value != hk_direction: + self.char_direction.set_value(hk_direction) + self._flag[CHAR_ROTATION_DIRECTION] = False + + # Handle Oscillating + if CHAR_SWING_MODE in self.chars: + oscillating = new_state.attributes.get(ATTR_OSCILLATING) + if not self._flag[CHAR_SWING_MODE] and \ + oscillating in (True, False): + hk_oscillating = 1 if oscillating else 0 + if self.char_swing.value != hk_oscillating: + self.char_swing.set_value(hk_oscillating) + self._flag[CHAR_SWING_MODE] = False diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index d8a205d7026..dae3579a97a 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -12,7 +12,8 @@ from . import TYPES from .accessories import HomeAccessory, debounce from .const import ( SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION) + CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION, + PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -61,7 +62,8 @@ class Light(HomeAccessory): .attributes.get(ATTR_MAX_MIREDS, 500) self.char_color_temperature = serv_light.configure_char( CHAR_COLOR_TEMPERATURE, value=min_mireds, - properties={'minValue': min_mireds, 'maxValue': max_mireds}, + properties={PROP_MIN_VALUE: min_mireds, + PROP_MAX_VALUE: max_mireds}, setter_callback=self.set_color_temperature) if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index cdfb858b727..a72f50f6c6f 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -37,6 +37,7 @@ def test_customize_options(config, name): @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ + ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {}), diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index dc4caeb35a6..7260ae40c1a 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -14,18 +14,18 @@ from tests.components.homekit.test_accessories import patch_debounce @pytest.fixture(scope='module') -def cls(request): +def cls(): """Patch debounce decorator during import of type_covers.""" patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_covers', fromlist=['GarageDoorOpener', 'WindowCovering,', 'WindowCoveringBasic']) - request.addfinalizer(patcher.stop) patcher_tuple = namedtuple('Cls', ['window', 'window_basic', 'garage']) - return patcher_tuple(window=_import.WindowCovering, - window_basic=_import.WindowCoveringBasic, - garage=_import.GarageDoorOpener) + yield patcher_tuple(window=_import.WindowCovering, + window_basic=_import.WindowCoveringBasic, + garage=_import.GarageDoorOpener) + patcher.stop() async def test_garage_door_open_close(hass, cls): diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py new file mode 100644 index 00000000000..fc504cc6cbd --- /dev/null +++ b/tests/components/homekit/test_type_fans.py @@ -0,0 +1,149 @@ +"""Test different accessory types: Fans.""" +from collections import namedtuple + +import pytest + +from homeassistant.components.fan import ( + ATTR_DIRECTION, ATTR_OSCILLATING, + DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + +from tests.common import async_mock_service +from tests.components.homekit.test_accessories import patch_debounce + + +@pytest.fixture(scope='module') +def cls(): + """Patch debounce decorator during import of type_fans.""" + patcher = patch_debounce() + patcher.start() + _import = __import__('homeassistant.components.homekit.type_fans', + fromlist=['Fan']) + patcher_tuple = namedtuple('Cls', ['fan']) + yield patcher_tuple(fan=_import.Fan) + patcher.stop() + + +async def test_fan_basic(hass, cls): + """Test fan with char state.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.aid == 2 + assert acc.category == 3 # Fan + assert acc.char_active.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_active.value == 1 + + hass.states.async_set(entity_id, STATE_OFF, + {ATTR_SUPPORTED_FEATURES: 0}) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert acc.char_active.value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + + await hass.async_add_job(acc.char_active.client_update_value, 1) + await hass.async_block_till_done() + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + + await hass.async_add_job(acc.char_active.client_update_value, 0) + await hass.async_block_till_done() + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + + +async def test_fan_direction(hass, cls): + """Test fan with direction.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_DIRECTION, + ATTR_DIRECTION: DIRECTION_FORWARD}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.char_direction.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_direction.value == 0 + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_DIRECTION: DIRECTION_REVERSE}) + await hass.async_block_till_done() + assert acc.char_direction.value == 1 + + # Set from HomeKit + call_set_direction = async_mock_service(hass, DOMAIN, + SERVICE_SET_DIRECTION) + + await hass.async_add_job(acc.char_direction.client_update_value, 0) + await hass.async_block_till_done() + assert call_set_direction[0] + assert call_set_direction[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_direction[0].data[ATTR_DIRECTION] == DIRECTION_FORWARD + + await hass.async_add_job(acc.char_direction.client_update_value, 1) + await hass.async_block_till_done() + assert call_set_direction[1] + assert call_set_direction[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_direction[1].data[ATTR_DIRECTION] == DIRECTION_REVERSE + + +async def test_fan_oscillate(hass, cls): + """Test fan with oscillate.""" + entity_id = 'fan.demo' + + hass.states.async_set(entity_id, STATE_ON, { + ATTR_SUPPORTED_FEATURES: SUPPORT_OSCILLATE, ATTR_OSCILLATING: False}) + await hass.async_block_till_done() + acc = cls.fan(hass, 'Fan', entity_id, 2, None) + + assert acc.char_swing.value == 0 + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_swing.value == 0 + + hass.states.async_set(entity_id, STATE_ON, + {ATTR_OSCILLATING: True}) + await hass.async_block_till_done() + assert acc.char_swing.value == 1 + + # Set from HomeKit + call_oscillate = async_mock_service(hass, DOMAIN, SERVICE_OSCILLATE) + + await hass.async_add_job(acc.char_swing.client_update_value, 0) + await hass.async_block_till_done() + assert call_oscillate[0] + assert call_oscillate[0].data[ATTR_ENTITY_ID] == entity_id + assert call_oscillate[0].data[ATTR_OSCILLATING] is False + + await hass.async_add_job(acc.char_swing.client_update_value, 1) + await hass.async_block_till_done() + assert call_oscillate[1] + assert call_oscillate[1].data[ATTR_ENTITY_ID] == entity_id + assert call_oscillate[1].data[ATTR_OSCILLATING] is True diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index d9602a6e41f..65a526edcc3 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -15,15 +15,15 @@ from tests.components.homekit.test_accessories import patch_debounce @pytest.fixture(scope='module') -def cls(request): +def cls(): """Patch debounce decorator during import of type_lights.""" patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_lights', fromlist=['Light']) - request.addfinalizer(patcher.stop) patcher_tuple = namedtuple('Cls', ['light']) - return patcher_tuple(light=_import.Light) + yield patcher_tuple(light=_import.Light) + patcher.stop() async def test_light_basic(hass, cls): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index ea592bd63dd..bc5b3219cdf 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -16,15 +16,15 @@ from tests.components.homekit.test_accessories import patch_debounce @pytest.fixture(scope='module') -def cls(request): +def cls(): """Patch debounce decorator during import of type_thermostats.""" patcher = patch_debounce() patcher.start() _import = __import__('homeassistant.components.homekit.type_thermostats', fromlist=['Thermostat']) - request.addfinalizer(patcher.stop) patcher_tuple = namedtuple('Cls', ['thermostat']) - return patcher_tuple(thermostat=_import.Thermostat) + yield patcher_tuple(thermostat=_import.Thermostat) + patcher.stop() async def test_default_thermostat(hass, cls): From 105347311107788bec26e3737287fc52a2715fa0 Mon Sep 17 00:00:00 2001 From: Nathan Henrie Date: Wed, 16 May 2018 05:47:41 -0600 Subject: [PATCH 098/144] Add stdout and stderr to debug output for shell_command (#14465) --- homeassistant/components/shell_command.py | 18 ++++++++--- tests/components/test_shell_command.py | 38 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index ca33666d1f3..10a6c350b7c 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -68,8 +68,9 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: cmd, loop=hass.loop, stdin=None, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) else: # Template used. Break into list and use create_subprocess_exec # (which uses shell=False) for security @@ -80,12 +81,19 @@ def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: *shlexed_cmd, loop=hass.loop, stdin=None, - stdout=asyncio.subprocess.DEVNULL, - stderr=asyncio.subprocess.DEVNULL) + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) process = yield from create_process - yield from process.communicate() + stdout_data, stderr_data = yield from process.communicate() + if stdout_data: + _LOGGER.debug("Stdout of command: `%s`, return code: %s:\n%s", + cmd, process.returncode, stdout_data) + if stderr_data: + _LOGGER.debug("Stderr of command: `%s`, return code: %s:\n%s", + cmd, process.returncode, stderr_data) if process.returncode != 0: _LOGGER.exception("Error running command: `%s`, return code: %s", cmd, process.returncode) diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index c7cef78a127..a1acffd62e5 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -148,3 +148,41 @@ class TestShellCommand(unittest.TestCase): self.assertEqual(1, mock_call.call_count) self.assertEqual(1, mock_error.call_count) self.assertFalse(os.path.isfile(path)) + + @patch('homeassistant.components.shell_command._LOGGER.debug') + def test_stdout_captured(self, mock_output): + """Test subprocess that has stdout.""" + test_phrase = "I have output" + self.assertTrue( + setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': "echo {}".format(test_phrase) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.hass.block_till_done() + self.assertEqual(1, mock_output.call_count) + self.assertEqual(test_phrase.encode() + b'\n', + mock_output.call_args_list[0][0][-1]) + + @patch('homeassistant.components.shell_command._LOGGER.debug') + def test_stderr_captured(self, mock_output): + """Test subprocess that has stderr.""" + test_phrase = "I have error" + self.assertTrue( + setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': ">&2 echo {}".format(test_phrase) + } + })) + + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + self.hass.block_till_done() + self.assertEqual(1, mock_output.call_count) + self.assertEqual(test_phrase.encode() + b'\n', + mock_output.call_args_list[0][0][-1]) From 64223cea7216bf0c451e3d3ebc162d1a2a8886ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 May 2018 09:00:00 -0400 Subject: [PATCH 099/144] Update frontend to 20180516.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 68783a837cb..1818ecdd7bc 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180515.0'] +REQUIREMENTS = ['home-assistant-frontend==20180516.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 2aad7805b02..a997597cdea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180515.0 +home-assistant-frontend==20180516.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 55f62f062fe..5bbe3ff04a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180515.0 +home-assistant-frontend==20180516.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 3e7d4fc902d22bf10c21d8b6a54e57babc046261 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 16 May 2018 09:39:14 -0400 Subject: [PATCH 100/144] Bump frontend to 20180516.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1818ecdd7bc..3ea8594b2a0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180516.0'] +REQUIREMENTS = ['home-assistant-frontend==20180516.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index a997597cdea..1181228eb20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -386,7 +386,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.0 +home-assistant-frontend==20180516.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bbe3ff04a9..c49c7de7bd1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.0 +home-assistant-frontend==20180516.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 298d31e42b8aa27dcf4677e37a1b697d1baa0407 Mon Sep 17 00:00:00 2001 From: ChristianKuehnel Date: Thu, 17 May 2018 02:45:47 +0200 Subject: [PATCH 101/144] New Sensor FinTS (#14334) --- .coveragerc | 1 + homeassistant/components/sensor/fints.py | 285 +++++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 289 insertions(+) create mode 100644 homeassistant/components/sensor/fints.py diff --git a/.coveragerc b/.coveragerc index 3ecf2411384..d361cf2ddad 100644 --- a/.coveragerc +++ b/.coveragerc @@ -599,6 +599,7 @@ omit = homeassistant/components/sensor/fastdotcom.py homeassistant/components/sensor/fedex.py homeassistant/components/sensor/filesize.py + homeassistant/components/sensor/fints.py homeassistant/components/sensor/fitbit.py homeassistant/components/sensor/fixer.py homeassistant/components/sensor/folder.py diff --git a/homeassistant/components/sensor/fints.py b/homeassistant/components/sensor/fints.py new file mode 100644 index 00000000000..798f74bb654 --- /dev/null +++ b/homeassistant/components/sensor/fints.py @@ -0,0 +1,285 @@ +""" +Read the balance of your bank accounts via FinTS. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.fints/ +""" + +from collections import namedtuple +from datetime import timedelta +import logging +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_USERNAME, CONF_PIN, CONF_URL, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['fints==0.2.1'] + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(hours=4) + +ICON = 'mdi:currency-eur' + +BankCredentials = namedtuple('BankCredentials', 'blz login pin url') + +CONF_BIN = 'bank_identification_number' +CONF_ACCOUNTS = 'accounts' +CONF_HOLDINGS = 'holdings' +CONF_ACCOUNT = 'account' + +ATTR_ACCOUNT = CONF_ACCOUNT +ATTR_BANK = 'bank' +ATTR_ACCOUNT_TYPE = 'account_type' + +SCHEMA_ACCOUNTS = vol.Schema({ + vol.Required(CONF_ACCOUNT): cv.string, + vol.Optional(CONF_NAME, default=None): vol.Any(None, cv.string), +}) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_BIN): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PIN): cv.string, + vol.Required(CONF_URL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), + vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS), +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the sensors. + + Login to the bank and get a list of existing accounts. Create a + sensor for each account. + """ + credentials = BankCredentials(config[CONF_BIN], config[CONF_USERNAME], + config[CONF_PIN], config[CONF_URL]) + fints_name = config.get(CONF_NAME, config[CONF_BIN]) + + account_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] + for acc in config[CONF_ACCOUNTS]} + + holdings_config = {acc[CONF_ACCOUNT]: acc[CONF_NAME] + for acc in config[CONF_HOLDINGS]} + + client = FinTsClient(credentials, fints_name) + balance_accounts, holdings_accounts = client.detect_accounts() + accounts = [] + + for account in balance_accounts: + if config[CONF_ACCOUNTS] and account.iban not in account_config: + _LOGGER.info('skipping account %s for bank %s', + account.iban, fints_name) + continue + + account_name = account_config.get(account.iban) + if not account_name: + account_name = '{} - {}'.format(fints_name, account.iban) + accounts.append(FinTsAccount(client, account, account_name)) + _LOGGER.debug('Creating account %s for bank %s', + account.iban, fints_name) + + for account in holdings_accounts: + if config[CONF_HOLDINGS] and \ + account.accountnumber not in holdings_config: + _LOGGER.info('skipping holdings %s for bank %s', + account.accountnumber, fints_name) + continue + + account_name = holdings_config.get(account.accountnumber) + if not account_name: + account_name = '{} - {}'.format( + fints_name, account.accountnumber) + accounts.append(FinTsHoldingsAccount(client, account, account_name)) + _LOGGER.debug('Creating holdings %s for bank %s', + account.accountnumber, fints_name) + + add_devices(accounts, True) + + +class FinTsClient(object): + """Wrapper around the FinTS3PinTanClient. + + Use this class as Context Manager to get the FinTS3Client object. + """ + + def __init__(self, credentials: BankCredentials, name: str): + """Constructor for class FinTsClient.""" + self._credentials = credentials + self.name = name + + @property + def client(self): + """Get the client object. + + As the fints library is stateless, there is not benefit in caching + the client objects. If that ever changes, consider caching the client + object and also think about potential concurrency problems. + """ + from fints.client import FinTS3PinTanClient + return FinTS3PinTanClient( + self._credentials.blz, self._credentials.login, + self._credentials.pin, self._credentials.url) + + def detect_accounts(self): + """Identify the accounts of the bank.""" + from fints.dialog import FinTSDialogError + balance_accounts = [] + holdings_accounts = [] + for account in self.client.get_sepa_accounts(): + try: + self.client.get_balance(account) + balance_accounts.append(account) + except IndexError: + # account is not a balance account. + pass + except FinTSDialogError: + # account is not a balance account. + pass + try: + self.client.get_holdings(account) + holdings_accounts.append(account) + except FinTSDialogError: + # account is not a holdings account. + pass + + return balance_accounts, holdings_accounts + + +class FinTsAccount(Entity): + """Sensor for a FinTS balanc account. + + A balance account contains an amount of money (=balance). The amount may + also be negative. + """ + + def __init__(self, client: FinTsClient, account, name: str) -> None: + """Constructor for class FinTsAccount.""" + self._client = client # type: FinTsClient + self._account = account + self._name = name # type: str + self._balance = None # type: float + self._currency = None # type: str + + @property + def should_poll(self) -> bool: + """Data needs to be polled from the bank servers.""" + return True + + def update(self) -> None: + """Get the current balance and currency for the account.""" + bank = self._client.client + balance = bank.get_balance(self._account) + self._balance = balance.amount.amount + self._currency = balance.amount.currency + _LOGGER.debug('updated balance of account %s', self.name) + + @property + def name(self) -> str: + """Friendly name of the sensor.""" + return self._name + + @property + def state(self) -> float: + """Return the balance of the account as state.""" + return self._balance + + @property + def unit_of_measurement(self) -> str: + """Use the currency as unit of measurement.""" + return self._currency + + @property + def device_state_attributes(self) -> dict: + """Additional attributes of the sensor.""" + attributes = { + ATTR_ACCOUNT: self._account.iban, + ATTR_ACCOUNT_TYPE: 'balance', + } + if self._client.name: + attributes[ATTR_BANK] = self._client.name + return attributes + + @property + def icon(self) -> str: + """Set the icon for the sensor.""" + return ICON + + +class FinTsHoldingsAccount(Entity): + """Sensor for a FinTS holdings account. + + A holdings account does not contain money but rather some financial + instruments, e.g. stocks. + """ + + def __init__(self, client: FinTsClient, account, name: str) -> None: + """Constructor for class FinTsHoldingsAccount.""" + self._client = client # type: FinTsClient + self._name = name # type: str + self._account = account + self._holdings = [] + self._total = None # type: float + + @property + def should_poll(self) -> bool: + """Data needs to be polled from the bank servers.""" + return True + + def update(self) -> None: + """Get the current holdings for the account.""" + bank = self._client.client + self._holdings = bank.get_holdings(self._account) + self._total = sum(h.total_value for h in self._holdings) + + @property + def state(self) -> float: + """Return total market value as state.""" + return self._total + + @property + def icon(self) -> str: + """Set the icon for the sensor.""" + return ICON + + @property + def device_state_attributes(self) -> dict: + """Additional attributes of the sensor. + + Lists each holding of the account with the current value. + """ + attributes = { + ATTR_ACCOUNT: self._account.accountnumber, + ATTR_ACCOUNT_TYPE: 'holdings', + } + if self._client.name: + attributes[ATTR_BANK] = self._client.name + for holding in self._holdings: + total_name = '{} total'.format(holding.name) + attributes[total_name] = holding.total_value + pieces_name = '{} pieces'.format(holding.name) + attributes[pieces_name] = holding.pieces + price_name = '{} price'.format(holding.name) + attributes[price_name] = holding.market_value + + return attributes + + @property + def name(self) -> str: + """Friendly name of the sensor.""" + return self._name + + @property + def unit_of_measurement(self) -> str: + """Get the unit of measurement. + + Hardcoded to EUR, as the library does not provide the currency for the + holdings. And as FinTS is only used in Germany, most accounts will be + in EUR anyways. + """ + return "EUR" diff --git a/requirements_all.txt b/requirements_all.txt index 1181228eb20..63af0d7b94f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,6 +308,9 @@ fedexdeliverymanager==1.0.6 # homeassistant.components.sensor.geo_rss_events feedparser==5.2.1 +# homeassistant.components.sensor.fints +fints==0.2.1 + # homeassistant.components.sensor.fitbit fitbit==0.3.0 From 144524fbbb61c08474292cfffe0772f8b19e1d89 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 17 May 2018 11:44:01 -0600 Subject: [PATCH 102/144] Update hitron_coda.py (#14506) missed a typo that wasn't caught with testing since I don't have a Rogers router. --- homeassistant/components/device_tracker/hitron_coda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/hitron_coda.py b/homeassistant/components/device_tracker/hitron_coda.py index c9cd30cdb25..72817ca695c 100644 --- a/homeassistant/components/device_tracker/hitron_coda.py +++ b/homeassistant/components/device_tracker/hitron_coda.py @@ -55,7 +55,7 @@ class HitronCODADeviceScanner(DeviceScanner): if config.get(CONF_TYPE) == "shaw": self._type = 'pwd' else: - self.type = 'pws' + self._type = 'pws' self._userid = None From ed3efc871237f0749335e5f09689292513a5203d Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Thu, 17 May 2018 14:19:05 -0400 Subject: [PATCH 103/144] Konnected component follow up (#14491) * make device_discovered synchronous * small fixes from code review * use dispatcher to update sensor state * update switch state based on response from the device * interpolate entity_id into dispatcher signal * cleanup lint * change coroutine to callback --- .../components/binary_sensor/konnected.py | 22 +++++++--- homeassistant/components/konnected.py | 30 +++++++------ homeassistant/components/switch/konnected.py | 43 +++++++++++-------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/binary_sensor/konnected.py b/homeassistant/components/binary_sensor/konnected.py index c7e2b7c84fe..9a16ca5e1ab 100644 --- a/homeassistant/components/binary_sensor/konnected.py +++ b/homeassistant/components/binary_sensor/konnected.py @@ -4,13 +4,16 @@ Support for wired binary sensors attached to a Konnected device. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.konnected/ """ -import asyncio import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.konnected import (DOMAIN, PIN_TO_ZONE) +from homeassistant.components.konnected import ( + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE) from homeassistant.const import ( - CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_STATE) + CONF_DEVICES, CONF_TYPE, CONF_NAME, CONF_BINARY_SENSORS, ATTR_ENTITY_ID, + ATTR_STATE) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) @@ -23,7 +26,7 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return - data = hass.data[DOMAIN] + data = hass.data[KONNECTED_DOMAIN] device_id = discovery_info['device_id'] sensors = [KonnectedBinarySensor(device_id, pin_num, pin_data) for pin_num, pin_data in @@ -43,7 +46,6 @@ class KonnectedBinarySensor(BinarySensorDevice): self._device_class = self._data.get(CONF_TYPE) self._name = self._data.get(CONF_NAME, 'Konnected {} Zone {}'.format( device_id, PIN_TO_ZONE[pin_num])) - self._data['entity'] = self _LOGGER.debug('Created new Konnected sensor: %s', self._name) @property @@ -66,9 +68,15 @@ class KonnectedBinarySensor(BinarySensorDevice): """Return the device class.""" return self._device_class - @asyncio.coroutine + async def async_added_to_hass(self): + """Store entity_id and register state change callback.""" + self._data[ATTR_ENTITY_ID] = self.entity_id + async_dispatcher_connect( + self.hass, SIGNAL_SENSOR_UPDATE.format(self.entity_id), + self.async_set_state) + + @callback def async_set_state(self, state): """Update the sensor's state.""" self._state = state - self._data[ATTR_STATE] = state self.async_schedule_update_ha_state() diff --git a/homeassistant/components/konnected.py b/homeassistant/components/konnected.py index 8c5578f10e4..70b66f84ae9 100644 --- a/homeassistant/components/konnected.py +++ b/homeassistant/components/konnected.py @@ -19,7 +19,8 @@ from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, HTTP_UNAUTHORIZED, CONF_DEVICES, CONF_BINARY_SENSORS, CONF_SWITCHES, CONF_HOST, CONF_PORT, CONF_ID, CONF_NAME, CONF_TYPE, CONF_PIN, CONF_ZONE, CONF_ACCESS_TOKEN, - ATTR_STATE) + ATTR_ENTITY_ID, ATTR_STATE) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv @@ -75,6 +76,7 @@ DEPENDENCIES = ['http', 'discovery'] ENDPOINT_ROOT = '/api/konnected' UPDATE_ENDPOINT = (ENDPOINT_ROOT + r'/device/{device_id:[a-zA-Z0-9]+}') +SIGNAL_SENSOR_UPDATE = 'konnected.{}.update' async def async_setup(hass, config): @@ -87,19 +89,19 @@ async def async_setup(hass, config): if DOMAIN not in hass.data: hass.data[DOMAIN] = {CONF_ACCESS_TOKEN: access_token} - async def async_device_discovered(service, info): + def device_discovered(service, info): """Call when a Konnected device has been discovered.""" _LOGGER.debug("Discovered a new Konnected device: %s", info) host = info.get(CONF_HOST) port = info.get(CONF_PORT) device = KonnectedDevice(hass, host, port, cfg) - await device.async_setup() + device.setup() discovery.async_listen( hass, SERVICE_KONNECTED, - async_device_discovered) + device_discovered) hass.http.register_view(KonnectedView(access_token)) @@ -121,17 +123,17 @@ class KonnectedDevice(object): self.status = self.client.get_status() _LOGGER.info('Initialized Konnected device %s', self.device_id) - async def async_setup(self): + def setup(self): """Set up a newly discovered Konnected device.""" user_config = self.config() if user_config: _LOGGER.debug('Configuring Konnected device %s', self.device_id) self.save_data() - await self.async_sync_device_config() - await discovery.async_load_platform( + self.sync_device_config() + discovery.load_platform( self.hass, 'binary_sensor', DOMAIN, {'device_id': self.device_id}) - await discovery.async_load_platform( + discovery.load_platform( self.hass, 'switch', DOMAIN, {'device_id': self.device_id}) @@ -225,7 +227,7 @@ class KonnectedDevice(object): def sensor_configuration(self): """Return the configuration map for syncing sensors.""" return [{'pin': p} for p in - self.stored_configuration[CONF_BINARY_SENSORS].keys()] + self.stored_configuration[CONF_BINARY_SENSORS]] def actuator_configuration(self): """Return the configuration map for syncing actuators.""" @@ -235,7 +237,7 @@ class KonnectedDevice(object): for p, data in self.stored_configuration[CONF_SWITCHES].items()] - async def async_sync_device_config(self): + def sync_device_config(self): """Sync the new pin configuration to the Konnected device.""" desired_sensor_configuration = self.sensor_configuration() current_sensor_configuration = [ @@ -306,10 +308,12 @@ class KonnectedView(HomeAssistantView): if pin_data is None: return self.json_message('unregistered sensor/actuator', status_code=HTTP_BAD_REQUEST) - entity = pin_data.get('entity') - if entity is None: + + entity_id = pin_data.get(ATTR_ENTITY_ID) + if entity_id is None: return self.json_message('uninitialized sensor/actuator', status_code=HTTP_INTERNAL_SERVER_ERROR) - await entity.async_set_state(state) + async_dispatcher_send( + hass, SIGNAL_SENSOR_UPDATE.format(entity_id), state) return self.json_message('ok') diff --git a/homeassistant/components/switch/konnected.py b/homeassistant/components/switch/konnected.py index e88f9826678..53c6406b28a 100644 --- a/homeassistant/components/switch/konnected.py +++ b/homeassistant/components/switch/konnected.py @@ -5,11 +5,11 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.konnected/ """ -import asyncio import logging from homeassistant.components.konnected import ( - DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, STATE_LOW, STATE_HIGH) + DOMAIN as KONNECTED_DOMAIN, PIN_TO_ZONE, CONF_ACTIVATION, + STATE_LOW, STATE_HIGH) from homeassistant.helpers.entity import ToggleEntity from homeassistant.const import (CONF_DEVICES, CONF_SWITCHES, ATTR_STATE) @@ -24,7 +24,7 @@ async def async_setup_platform(hass, config, async_add_devices, if discovery_info is None: return - data = hass.data[DOMAIN] + data = hass.data[KONNECTED_DOMAIN] device_id = discovery_info['device_id'] client = data[CONF_DEVICES][device_id]['client'] switches = [KonnectedSwitch(device_id, pin_num, pin_data, client) @@ -41,12 +41,11 @@ class KonnectedSwitch(ToggleEntity): self._data = data self._device_id = device_id self._pin_num = pin_num - self._state = self._data.get(ATTR_STATE) self._activation = self._data.get(CONF_ACTIVATION, STATE_HIGH) + self._state = self._boolean_state(self._data.get(ATTR_STATE)) self._name = self._data.get( 'name', 'Konnected {} Actuator {}'.format( device_id, PIN_TO_ZONE[pin_num])) - self._data['entity'] = self self._client = client _LOGGER.debug('Created new switch: %s', self._name) @@ -62,26 +61,34 @@ class KonnectedSwitch(ToggleEntity): def turn_on(self, **kwargs): """Send a command to turn on the switch.""" - self._client.put_device(self._pin_num, - int(self._activation == STATE_HIGH)) - self._set_state(True) + resp = self._client.put_device( + self._pin_num, int(self._activation == STATE_HIGH)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) def turn_off(self, **kwargs): """Send a command to turn off the switch.""" - self._client.put_device(self._pin_num, - int(self._activation == STATE_LOW)) - self._set_state(False) + resp = self._client.put_device( + self._pin_num, int(self._activation == STATE_LOW)) + + if resp.get(ATTR_STATE) is not None: + self._set_state(self._boolean_state(resp.get(ATTR_STATE))) + + def _boolean_state(self, int_state): + if int_state is None: + return False + if int_state == 0: + return self._activation == STATE_LOW + if int_state == 1: + return self._activation == STATE_HIGH def _set_state(self, state): self._state = state - self._data[ATTR_STATE] = state self.schedule_update_ha_state() _LOGGER.debug('Setting status of %s actuator pin %s to %s', self._device_id, self.name, state) - @asyncio.coroutine - def async_set_state(self, state): - """Update the switch's state.""" - self._state = state - self._data[ATTR_STATE] = state - self.async_schedule_update_ha_state() + async def async_added_to_hass(self): + """Store entity_id.""" + self._data['entity_id'] = self.entity_id From 9afc2634c6cfe29c96d4c0303fa28dc5a8c50070 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Thu, 17 May 2018 20:54:25 +0200 Subject: [PATCH 104/144] Adjust LimitlessLED properties for running effects (#14481) --- .../components/light/limitlessled.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/light/limitlessled.py b/homeassistant/components/light/limitlessled.py index bb84b3a6fed..bd4fece89e3 100644 --- a/homeassistant/components/light/limitlessled.py +++ b/homeassistant/components/light/limitlessled.py @@ -142,10 +142,9 @@ def state(new_state): from limitlessled.pipeline import Pipeline pipeline = Pipeline() transition_time = DEFAULT_TRANSITION - # Stop any repeating pipeline. - if self.repeating: - self.repeating = False + if self._effect == EFFECT_COLORLOOP: self.group.stop() + self._effect = None # Set transition time. if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) @@ -183,11 +182,11 @@ class LimitlessLEDGroup(Light): self.group = group self.config = config - self.repeating = False self._is_on = False self._brightness = None self._temperature = None self._color = None + self._effect = None @asyncio.coroutine def async_added_to_hass(self): @@ -222,6 +221,9 @@ class LimitlessLEDGroup(Light): @property def brightness(self): """Return the brightness property.""" + if self._effect == EFFECT_NIGHT: + return 1 + return self._brightness @property @@ -242,6 +244,9 @@ class LimitlessLEDGroup(Light): @property def hs_color(self): """Return the color property.""" + if self._effect == EFFECT_NIGHT: + return None + return self._color @property @@ -249,6 +254,11 @@ class LimitlessLEDGroup(Light): """Flag supported features.""" return self._supported + @property + def effect(self): + """Return the current effect for this light.""" + return self._effect + @property def effect_list(self): """Return the list of supported effects for this light.""" @@ -270,6 +280,7 @@ class LimitlessLEDGroup(Light): if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT: if EFFECT_NIGHT in self._effect_list: pipeline.night_light() + self._effect = EFFECT_NIGHT return pipeline.on() @@ -314,7 +325,7 @@ class LimitlessLEDGroup(Light): if ATTR_EFFECT in kwargs and self._effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: from limitlessled.presets import COLORLOOP - self.repeating = True + self._effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: pipeline.white() From f06a0ba3738da05794a21d8e11f379c32f6ce7c0 Mon Sep 17 00:00:00 2001 From: thelittlefireman Date: Thu, 17 May 2018 23:06:39 +0200 Subject: [PATCH 105/144] Bump locationsharinglib to 2.0.2 (#14359) * Bump locationsharinglib to 2.0.2 * Bump locationsharinglib to 2.0.2 --- homeassistant/components/device_tracker/google_maps.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 1d0058ed229..7aaf02b0f4c 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -19,7 +19,7 @@ from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==1.2.2'] +REQUIREMENTS = ['locationsharinglib==2.0.2'] CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' diff --git a/requirements_all.txt b/requirements_all.txt index 63af0d7b94f..54cd304b7e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -509,7 +509,7 @@ liveboxplaytv==2.0.2 lmnotify==0.0.4 # homeassistant.components.device_tracker.google_maps -locationsharinglib==1.2.2 +locationsharinglib==2.0.2 # homeassistant.components.sensor.luftdaten luftdaten==0.1.3 From 1c3293ac855aa2193149d46c3ba32854d1172d01 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 May 2018 21:29:37 -0400 Subject: [PATCH 106/144] Update frontend to 20180518.0 (#14510) * Update frontend to 20180517.0 * Update requirements * Bump frontend to 20180518.0 --- homeassistant/components/frontend/__init__.py | 47 ++++++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3ea8594b2a0..13c8d826377 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180516.1'] +REQUIREMENTS = ['home-assistant-frontend==20180518.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] @@ -301,47 +301,28 @@ def async_setup(hass, config): hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION) if is_dev: - for subpath in ["src", "build-translations", "build-temp", "build", - "hass_frontend", "bower_components", "panels", - "hassio"]: - hass.http.register_static_path( - "/home-assistant-polymer/{}".format(subpath), - os.path.join(repo_path, subpath), - False) - - hass.http.register_static_path( - "/static/translations", - os.path.join(repo_path, "build-translations/output"), False) - sw_path_es5 = os.path.join(repo_path, "build-es5/service_worker.js") - sw_path_latest = os.path.join(repo_path, "build/service_worker.js") - static_path = os.path.join(repo_path, 'hass_frontend') - frontend_es5_path = os.path.join(repo_path, 'build-es5') - frontend_latest_path = os.path.join(repo_path, 'build') + hass_frontend_path = os.path.join(repo_path, 'hass_frontend') + hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5') else: import hass_frontend import hass_frontend_es5 - sw_path_es5 = os.path.join(hass_frontend_es5.where(), - "service_worker.js") - sw_path_latest = os.path.join(hass_frontend.where(), - "service_worker.js") - # /static points to dir with files that are JS-type agnostic. - # ES5 files are served from /frontend_es5. - # ES6 files are served from /frontend_latest. - static_path = hass_frontend.where() - frontend_es5_path = hass_frontend_es5.where() - frontend_latest_path = static_path + hass_frontend_path = hass_frontend.where() + hass_frontend_es5_path = hass_frontend_es5.where() hass.http.register_static_path( - "/service_worker_es5.js", sw_path_es5, False) + "/service_worker_es5.js", + os.path.join(hass_frontend_es5_path, "service_worker.js"), False) hass.http.register_static_path( - "/service_worker.js", sw_path_latest, False) + "/service_worker.js", + os.path.join(hass_frontend_path, "service_worker.js"), False) hass.http.register_static_path( - "/robots.txt", os.path.join(static_path, "robots.txt"), not is_dev) - hass.http.register_static_path("/static", static_path, not is_dev) + "/robots.txt", + os.path.join(hass_frontend_path, "robots.txt"), False) + hass.http.register_static_path("/static", hass_frontend_path, not is_dev) hass.http.register_static_path( - "/frontend_latest", frontend_latest_path, not is_dev) + "/frontend_latest", hass_frontend_path, not is_dev) hass.http.register_static_path( - "/frontend_es5", frontend_es5_path, not is_dev) + "/frontend_es5", hass_frontend_es5_path, not is_dev) local = hass.config.path('www') if os.path.isdir(local): diff --git a/requirements_all.txt b/requirements_all.txt index 54cd304b7e9..aaffe3068c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.1 +home-assistant-frontend==20180518.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c49c7de7bd1..f12fdd7ce77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180516.1 +home-assistant-frontend==20180518.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a3777c4ea85ccae7fb9071ec694a2d1eb4a5ec91 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 18 May 2018 15:25:08 +1000 Subject: [PATCH 107/144] Feedreader configurable update interval and max entries (#14487) --- homeassistant/components/feedreader.py | 37 +++++++++------ tests/components/test_feedreader.py | 65 ++++++++++++++++++++------ 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/feedreader.py b/homeassistant/components/feedreader.py index 61fbe9f3171..73ab9e8123c 100644 --- a/homeassistant/components/feedreader.py +++ b/homeassistant/components/feedreader.py @@ -4,7 +4,7 @@ Support for RSS/Atom feeds. For more details about this component, please refer to the documentation at https://home-assistant.io/components/feedreader/ """ -from datetime import datetime +from datetime import datetime, timedelta from logging import getLogger from os.path import exists from threading import Lock @@ -12,8 +12,8 @@ import pickle import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL +from homeassistant.helpers.event import track_time_interval import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['feedparser==5.2.1'] @@ -21,16 +21,22 @@ REQUIREMENTS = ['feedparser==5.2.1'] _LOGGER = getLogger(__name__) CONF_URLS = 'urls' +CONF_MAX_ENTRIES = 'max_entries' + +DEFAULT_MAX_ENTRIES = 20 +DEFAULT_SCAN_INTERVAL = timedelta(hours=1) DOMAIN = 'feedreader' EVENT_FEEDREADER = 'feedreader' -MAX_ENTRIES = 20 - CONFIG_SCHEMA = vol.Schema({ DOMAIN: { vol.Required(CONF_URLS): vol.All(cv.ensure_list, [cv.url]), + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + vol.Optional(CONF_MAX_ENTRIES, default=DEFAULT_MAX_ENTRIES): + cv.positive_int } }, extra=vol.ALLOW_EXTRA) @@ -38,18 +44,23 @@ CONFIG_SCHEMA = vol.Schema({ def setup(hass, config): """Set up the Feedreader component.""" urls = config.get(DOMAIN)[CONF_URLS] + scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) + max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) data_file = hass.config.path("{}.pickle".format(DOMAIN)) storage = StoredData(data_file) - feeds = [FeedManager(url, hass, storage) for url in urls] + feeds = [FeedManager(url, scan_interval, max_entries, hass, storage) for + url in urls] return len(feeds) > 0 class FeedManager(object): """Abstraction over Feedparser module.""" - def __init__(self, url, hass, storage): - """Initialize the FeedManager object, poll every hour.""" + def __init__(self, url, scan_interval, max_entries, hass, storage): + """Initialize the FeedManager object, poll as per scan interval.""" self._url = url + self._scan_interval = scan_interval + self._max_entries = max_entries self._feed = None self._hass = hass self._firstrun = True @@ -69,8 +80,8 @@ class FeedManager(object): def _init_regular_updates(self, hass): """Schedule regular updates at the top of the clock.""" - track_utc_time_change( - hass, lambda now: self._update(), minute=0, second=0) + track_time_interval(hass, lambda now: self._update(), + self._scan_interval) @property def last_update_successful(self): @@ -116,10 +127,10 @@ class FeedManager(object): def _filter_entries(self): """Filter the entries provided and return the ones to keep.""" - if len(self._feed.entries) > MAX_ENTRIES: + if len(self._feed.entries) > self._max_entries: _LOGGER.debug("Processing only the first %s entries " - "in feed %s", MAX_ENTRIES, self._url) - self._feed.entries = self._feed.entries[0:MAX_ENTRIES] + "in feed %s", self._max_entries, self._url) + self._feed.entries = self._feed.entries[0:self._max_entries] def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" diff --git a/tests/components/test_feedreader.py b/tests/components/test_feedreader.py index 2288e21e37a..c20b297017c 100644 --- a/tests/components/test_feedreader.py +++ b/tests/components/test_feedreader.py @@ -1,6 +1,6 @@ """The tests for the feedreader component.""" import time -from datetime import datetime +from datetime import datetime, timedelta import unittest from genericpath import exists @@ -11,12 +11,12 @@ from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import CONF_URLS, FeedManager, \ - StoredData, EVENT_FEEDREADER -from homeassistant.const import EVENT_HOMEASSISTANT_START + StoredData, EVENT_FEEDREADER, DEFAULT_SCAN_INTERVAL, CONF_MAX_ENTRIES, \ + DEFAULT_MAX_ENTRIES +from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, assert_setup_component, \ - load_fixture +from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) @@ -26,6 +26,18 @@ VALID_CONFIG_1 = { CONF_URLS: [URL] } } +VALID_CONFIG_2 = { + feedreader.DOMAIN: { + CONF_URLS: [URL], + CONF_SCAN_INTERVAL: 60 + } +} +VALID_CONFIG_3 = { + feedreader.DOMAIN: { + CONF_URLS: [URL], + CONF_MAX_ENTRIES: 100 + } +} class TestFeedreaderComponent(unittest.TestCase): @@ -45,11 +57,28 @@ class TestFeedreaderComponent(unittest.TestCase): def test_setup_one_feed(self): """Test the general setup of this component.""" - with assert_setup_component(1, 'feedreader'): + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, VALID_CONFIG_1)) + track_method.assert_called_once_with(self.hass, mock.ANY, + DEFAULT_SCAN_INTERVAL) - def setup_manager(self, feed_data): + def test_setup_scan_interval(self): + """Test the setup of this component with scan interval.""" + with patch("homeassistant.components.feedreader." + "track_time_interval") as track_method: + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_2)) + track_method.assert_called_once_with(self.hass, mock.ANY, + timedelta(seconds=60)) + + def test_setup_max_entries(self): + """Test the setup of this component with max entries.""" + self.assertTrue(setup_component(self.hass, feedreader.DOMAIN, + VALID_CONFIG_3)) + + def setup_manager(self, feed_data, max_entries=DEFAULT_MAX_ENTRIES): """Generic test setup method.""" events = [] @@ -67,12 +96,13 @@ class TestFeedreaderComponent(unittest.TestCase): feedreader.DOMAIN)) storage = StoredData(data_file) with patch("homeassistant.components.feedreader." - "track_utc_time_change") as track_method: - manager = FeedManager(feed_data, self.hass, storage) + "track_time_interval") as track_method: + manager = FeedManager(feed_data, DEFAULT_SCAN_INTERVAL, + max_entries, self.hass, storage) # Can't use 'assert_called_once' here because it's not available # in Python 3.5 yet. - track_method.assert_called_once_with(self.hass, mock.ANY, minute=0, - second=0) + track_method.assert_called_once_with(self.hass, mock.ANY, + DEFAULT_SCAN_INTERVAL) # Artificially trigger update. self.hass.bus.fire(EVENT_HOMEASSISTANT_START) # Collect events. @@ -116,12 +146,18 @@ class TestFeedreaderComponent(unittest.TestCase): manager3, events3 = self.setup_manager(feed_data3) assert len(events3) == 0 - def test_feed_max_length(self): - """Test long feed beyond the 20 entry limit.""" + def test_feed_default_max_length(self): + """Test long feed beyond the default 20 entry limit.""" feed_data = load_fixture('feedreader2.xml') manager, events = self.setup_manager(feed_data) assert len(events) == 20 + def test_feed_max_length(self): + """Test long feed beyond a configured 5 entry limit.""" + feed_data = load_fixture('feedreader2.xml') + manager, events = self.setup_manager(feed_data, max_entries=5) + assert len(events) == 5 + def test_feed_without_publication_date(self): """Test simple feed with entry without publication date.""" feed_data = load_fixture('feedreader3.xml') @@ -141,7 +177,8 @@ class TestFeedreaderComponent(unittest.TestCase): data_file = self.hass.config.path("{}.pickle".format( feedreader.DOMAIN)) storage = StoredData(data_file) - manager = FeedManager("FEED DATA", self.hass, storage) + manager = FeedManager("FEED DATA", DEFAULT_SCAN_INTERVAL, + DEFAULT_MAX_ENTRIES, self.hass, storage) # Artificially trigger update. self.hass.bus.fire(EVENT_HOMEASSISTANT_START) # Collect events. From 97076aa3fd9d72e526a0f651dd647e4a61eb551e Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 18 May 2018 06:48:16 +0100 Subject: [PATCH 108/144] Fix probability_threshold in binary_sensor.bayesian (#14512) (Closes: #14362) --- .../components/binary_sensor/bayesian.py | 2 +- .../components/binary_sensor/test_bayesian.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/binary_sensor/bayesian.py b/homeassistant/components/binary_sensor/bayesian.py index f3dbc912ade..72110eb50c9 100644 --- a/homeassistant/components/binary_sensor/bayesian.py +++ b/homeassistant/components/binary_sensor/bayesian.py @@ -217,4 +217,4 @@ class BayesianBinarySensor(BinarySensorDevice): @asyncio.coroutine def async_update(self): """Get the latest data and update the states.""" - self._deviation = bool(self.probability > self._probability_threshold) + self._deviation = bool(self.probability >= self._probability_threshold) diff --git a/tests/components/binary_sensor/test_bayesian.py b/tests/components/binary_sensor/test_bayesian.py index 3b403c3702f..c3242e09e78 100644 --- a/tests/components/binary_sensor/test_bayesian.py +++ b/tests/components/binary_sensor/test_bayesian.py @@ -154,6 +154,37 @@ class TestBayesianBinarySensor(unittest.TestCase): assert state.state == 'off' + def test_threshold(self): + """Test sensor on probabilty threshold limits.""" + config = { + 'binary_sensor': { + 'name': + 'Test_Binary', + 'platform': + 'bayesian', + 'observations': [{ + 'platform': 'state', + 'entity_id': 'sensor.test_monitored', + 'to_state': 'on', + 'prob_given_true': 1.0, + }], + 'prior': + 0.5, + 'probability_threshold': + 1.0, + } + } + + assert setup_component(self.hass, 'binary_sensor', config) + + self.hass.states.set('sensor.test_monitored', 'on') + self.hass.block_till_done() + + state = self.hass.states.get('binary_sensor.test_binary') + self.assertAlmostEqual(1.0, state.attributes.get('probability')) + + assert state.state == 'on' + def test_multiple_observations(self): """Test sensor with multiple observations of same entity.""" config = { From 909f2448cae6d6e3da7ac510b3088ad4b8039a1e Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Fri, 18 May 2018 01:50:57 -0500 Subject: [PATCH 109/144] Flux bug fix (#14476) * Simplify conditionals. * Send white_value on service call. * Remove extra blank line * Further simplification of conditionals * Requested changes * Do not call getRgb if not needed * Update log message --- homeassistant/components/light/flux_led.py | 33 +++++++++++++--------- homeassistant/components/switch/flux.py | 3 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 6c7f2e98e37..fc85e05238f 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -222,27 +222,34 @@ class FluxLight(Light): effect = kwargs.get(ATTR_EFFECT) white = kwargs.get(ATTR_WHITE_VALUE) - # color change only - if rgb is not None: - self._bulb.setRgb(*tuple(rgb), brightness=self.brightness) + # Show warning if effect set with rgb, brightness, or white level + if effect and (brightness or white or rgb): + _LOGGER.warning("RGB, brightness and white level are ignored when" + " an effect is specified for a flux bulb") - # brightness change only - elif brightness is not None: - (red, green, blue) = self._bulb.getRgb() - self._bulb.setRgb(red, green, blue, brightness=brightness) - - # random color effect - elif effect == EFFECT_RANDOM: + # Random color effect + if effect == EFFECT_RANDOM: self._bulb.setRgb(random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + return - # effect selection + # Effect selection elif effect in EFFECT_MAP: self._bulb.setPresetPattern(EFFECT_MAP[effect], 50) + return - # white change only - elif white is not None: + # Preserve current brightness on color/white level change + if brightness is None: + brightness = self.brightness + + # Preserve color on brightness/white level change + if rgb is None: + rgb = self._bulb.getRgb() + + self._bulb.setRgb(*tuple(rgb), brightness=brightness) + + if white is not None: self._bulb.setWarmWhite255(white) def turn_off(self, **kwargs): diff --git a/homeassistant/components/switch/flux.py b/homeassistant/components/switch/flux.py index e0bfdeee030..21689dcca0f 100644 --- a/homeassistant/components/switch/flux.py +++ b/homeassistant/components/switch/flux.py @@ -72,7 +72,8 @@ def set_lights_xy(hass, lights, x_val, y_val, brightness, transition): turn_on(hass, light, xy_color=[x_val, y_val], brightness=brightness, - transition=transition) + transition=transition, + white_value=brightness) def set_lights_temp(hass, lights, mired, brightness, transition): From cc5edf69e377ed3ccc48f5699089778a0d916f6f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 18 May 2018 09:04:47 +0200 Subject: [PATCH 110/144] Show warning if no locations are shared (fixes #14177) (#14511) --- .../components/device_tracker/google_maps.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_tracker/google_maps.py b/homeassistant/components/device_tracker/google_maps.py index 7aaf02b0f4c..3bf0cb0e126 100644 --- a/homeassistant/components/device_tracker/google_maps.py +++ b/homeassistant/components/device_tracker/google_maps.py @@ -12,14 +12,18 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, SOURCE_TYPE_GPS) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, ATTR_ID from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify + +REQUIREMENTS = ['locationsharinglib==2.0.2'] _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['locationsharinglib==2.0.2'] +ATTR_ADDRESS = 'address' +ATTR_FULL_NAME = 'full_name' +ATTR_LAST_SEEN = 'last_seen' +ATTR_NICKNAME = 'nickname' CREDENTIALS_FILE = '.google_maps_location_sharing.cookies' @@ -60,19 +64,23 @@ class GoogleMapsScanner(object): self.success_init = True except InvalidUser: - _LOGGER.error('You have specified invalid login credentials') + _LOGGER.error("You have specified invalid login credentials") self.success_init = False def _update_info(self, now=None): for person in self.service.get_all_people(): - dev_id = 'google_maps_{0}'.format(slugify(person.id)) + try: + dev_id = 'google_maps_{0}'.format(person.id) + except TypeError: + _LOGGER.warning("No location(s) shared with this account") + return attrs = { - 'id': person.id, - 'nickname': person.nickname, - 'full_name': person.full_name, - 'last_seen': person.datetime, - 'address': person.address + ATTR_ADDRESS: person.address, + ATTR_FULL_NAME: person.full_name, + ATTR_ID: person.id, + ATTR_LAST_SEEN: person.datetime, + ATTR_NICKNAME: person.nickname, } self.see( dev_id=dev_id, @@ -80,5 +88,5 @@ class GoogleMapsScanner(object): picture=person.picture_url, source_type=SOURCE_TYPE_GPS, gps_accuracy=person.accuracy, - attributes=attrs + attributes=attrs, ) From 4c328baaa6109a428c4dc0355ed5b77888649f55 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 May 2018 13:52:52 +0200 Subject: [PATCH 111/144] Add code to HomeKit lock (#14524) --- .../components/homekit/type_locks.py | 4 +++ homeassistant/components/homekit/util.py | 2 +- .../homekit/test_get_accessories.py | 2 +- tests/components/homekit/test_type_locks.py | 30 +++++++++++++++++-- tests/components/homekit/test_util.py | 7 +++++ 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index b08ac5930bd..309f3072768 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -5,6 +5,7 @@ from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) +from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory @@ -32,6 +33,7 @@ class Lock(HomeAccessory): def __init__(self, *args): """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) + self._code = self.config.get(ATTR_CODE) self.flag_target_state = False serv_lock_mechanism = self.add_preload_service(SERV_LOCK) @@ -51,6 +53,8 @@ class Lock(HomeAccessory): service = STATE_TO_SERVICE[hass_value] params = {ATTR_ENTITY_ID: self.entity_id} + if self._code: + params[ATTR_CODE] = self._code self.hass.services.call('lock', service, params) def update_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 5ddef534202..5d86dbc4612 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -30,7 +30,7 @@ def validate_entity_config(values): domain, _ = split_entity_id(entity) - if domain == 'alarm_control_panel': + if domain in ('alarm_control_panel', 'lock'): code = config.get(ATTR_CODE) params[ATTR_CODE] = cv.string(code) if code else None diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index a72f50f6c6f..0ffc1ae4767 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -39,7 +39,7 @@ def test_customize_options(config, name): @pytest.mark.parametrize('type_name, entity_id, state, attrs, config', [ ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), - ('Lock', 'lock.test', 'locked', {}, {}), + ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 984d032a1d9..3b8cde47fcb 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -1,19 +1,23 @@ """Test different accessory types: Locks.""" +import pytest + from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) from tests.common import async_mock_service async def test_lock_unlock(hass): """Test if accessory and HA are updated accordingly.""" + code = '1234' + config = {ATTR_CODE: code} entity_id = 'lock.kitchen_door' hass.states.async_set(entity_id, None) await hass.async_block_till_done() - acc = Lock(hass, 'Lock', entity_id, 2, None) + acc = Lock(hass, 'Lock', entity_id, 2, config) await hass.async_add_job(acc.run) assert acc.aid == 2 @@ -50,10 +54,32 @@ async def test_lock_unlock(hass): await hass.async_block_till_done() assert call_lock assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert call_lock[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 1 await hass.async_add_job(acc.char_target_state.client_update_value, 0) await hass.async_block_till_done() assert call_unlock assert call_unlock[0].data[ATTR_ENTITY_ID] == entity_id + assert call_unlock[0].data[ATTR_CODE] == code assert acc.char_target_state.value == 0 + + +@pytest.mark.parametrize('config', [{}, {ATTR_CODE: None}]) +async def test_no_code(hass, config): + """Test accessory if lock doesn't require a code.""" + entity_id = 'lock.kitchen_door' + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Lock(hass, 'Lock', entity_id, 2, config) + + # Set from HomeKit + call_lock = async_mock_service(hass, DOMAIN, 'lock') + + await hass.async_add_job(acc.char_target_state.client_update_value, 1) + await hass.async_block_till_done() + assert call_lock + assert call_lock[0].data[ATTR_ENTITY_ID] == entity_id + assert ATTR_CODE not in call_lock[0].data + assert acc.char_target_state.value == 1 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 0b3a5475f7e..f3ce35ee06b 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -30,9 +30,16 @@ def test_validate_entity_config(): assert vec({}) == {} assert vec({'demo.test': {CONF_NAME: 'Name'}}) == \ {'demo.test': {CONF_NAME: 'Name'}} + + assert vec({'alarm_control_panel.demo': {}}) == \ + {'alarm_control_panel.demo': {ATTR_CODE: None}} assert vec({'alarm_control_panel.demo': {ATTR_CODE: '1234'}}) == \ {'alarm_control_panel.demo': {ATTR_CODE: '1234'}} + assert vec({'lock.demo': {}}) == {'lock.demo': {ATTR_CODE: None}} + assert vec({'lock.demo': {ATTR_CODE: '1234'}}) == \ + {'lock.demo': {ATTR_CODE: '1234'}} + def test_convert_to_float(): """Test convert_to_float method.""" From e929f45ab88681cd3b0f9bcece84759c38fc0624 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 18 May 2018 15:42:41 +0200 Subject: [PATCH 112/144] Set pytz to >=2018.04 (#14520) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f6666c829e0..7bc4fe5761a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ requests==2.18.4 pyyaml>=3.11,<4 -pytz>=2017.02 +pytz>=2018.04 pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 diff --git a/requirements_all.txt b/requirements_all.txt index aaffe3068c2..6797be02b7f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,7 +1,7 @@ # Home Assistant core requests==2.18.4 pyyaml>=3.11,<4 -pytz>=2017.02 +pytz>=2018.04 pip>=8.0.3 jinja2>=2.10 voluptuous==0.11.1 diff --git a/setup.py b/setup.py index 8a68617afd9..6875230b7ab 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ PACKAGES = find_packages(exclude=['tests', 'tests.*']) REQUIRES = [ 'requests==2.18.4', 'pyyaml>=3.11,<4', - 'pytz>=2017.02', + 'pytz>=2018.04', 'pip>=8.0.3', 'jinja2>=2.10', 'voluptuous==0.11.1', From d36996c8f057b6267bbda6e0e9f14c5e27117b1a Mon Sep 17 00:00:00 2001 From: hanzoh Date: Fri, 18 May 2018 16:20:30 +0200 Subject: [PATCH 113/144] Add Homematic IP RotaryHandleSensor support (#14522) * Add Homematic IP RotaryHandleSensor support HmIP-SRH was in the RotaryHandleSensor class and threw errors that LOWBAT and ERROR could not be found (they are LOW_BAT and SABOTAGE). * Revert REQUIREMENTS change --- homeassistant/components/homematic/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index aa19875d43a..e0f0fafe5b5 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -71,7 +71,7 @@ HM_DEVICE_TYPES = { 'TemperatureSensor', 'CO2Sensor', 'IPSwitchPowermeter', 'HMWIOSwitch', 'FillingLevel', 'ValveDrive', 'EcoLogic', 'IPThermostatWall', 'IPSmoke', 'RFSiren', 'PresenceIP', 'IPAreaThermostat', - 'IPWeatherSensor'], + 'IPWeatherSensor', 'RotaryHandleSensorIP'], DISCOVER_CLIMATE: [ 'Thermostat', 'ThermostatWall', 'MAXThermostat', 'ThermostatWall2', 'MAXWallThermostat', 'IPThermostat', 'IPThermostatWall', @@ -98,6 +98,7 @@ HM_ATTRIBUTE_SUPPORT = { 'LOWBAT': ['battery', {0: 'High', 1: 'Low'}], 'LOW_BAT': ['battery', {0: 'High', 1: 'Low'}], 'ERROR': ['sabotage', {0: 'No', 1: 'Yes'}], + 'SABOTAGE': ['sabotage', {0: 'No', 1: 'Yes'}], 'RSSI_DEVICE': ['rssi', {}], 'VALVE_STATE': ['valve', {}], 'BATTERY_STATE': ['battery', {}], From 12e76ef7c159caca8356bfc62762b92d3f38d38f Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Fri, 18 May 2018 16:32:57 +0200 Subject: [PATCH 114/144] Update HAP-python to 2.1.0 (#14528) --- homeassistant/components/homekit/__init__.py | 9 ++-- .../components/homekit/accessories.py | 25 ++++++----- homeassistant/components/homekit/util.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit/test_accessories.py | 43 ++++++++++--------- tests/components/homekit/test_homekit.py | 11 +++-- tests/components/homekit/test_util.py | 8 ++-- 8 files changed, 56 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 41b0791a352..ce5f30d7bf2 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -29,7 +29,7 @@ from .util import show_setup_message, validate_entity_config TYPES = Registry() _LOGGER = logging.getLogger(__name__) -REQUIREMENTS = ['HAP-python==2.0.0'] +REQUIREMENTS = ['HAP-python==2.1.0'] # #### Driver Status #### STATUS_READY = 0 @@ -185,7 +185,8 @@ class HomeKit(): ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.bridge = HomeBridge(self.hass) - self.driver = HomeDriver(self.bridge, self._port, ip_addr, path) + self.driver = HomeDriver(self.hass, self.bridge, port=self._port, + address=ip_addr, persist_file=path) def add_bridge_accessory(self, state): """Try adding accessory to bridge if configured beforehand.""" @@ -213,8 +214,8 @@ class HomeKit(): self.add_bridge_accessory(state) self.bridge.set_driver(self.driver) - if not self.bridge.paired: - show_setup_message(self.hass, self.bridge) + if not self.driver.state.paired: + show_setup_message(self.hass, self.driver.state.pincode) _LOGGER.debug('Driver start') self.hass.add_job(self.driver.start) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 7ec1fb542c9..ff835659221 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -115,20 +115,23 @@ class HomeBridge(Bridge): """Prevent print of pyhap setup message to terminal.""" pass - def add_paired_client(self, client_uuid, client_public): - """Override super function to dismiss setup message if paired.""" - super().add_paired_client(client_uuid, client_public) - dismiss_setup_message(self.hass) - - def remove_paired_client(self, client_uuid): - """Override super function to show setup message if unpaired.""" - super().remove_paired_client(client_uuid) - show_setup_message(self.hass, self) - class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" - def __init__(self, *args, **kwargs): + def __init__(self, hass, *args, **kwargs): """Initialize a AccessoryDriver object.""" super().__init__(*args, **kwargs) + self.hass = hass + + def pair(self, client_uuid, client_public): + """Override super function to dismiss setup message if paired.""" + value = super().pair(client_uuid, client_public) + if value: + dismiss_setup_message(self.hass) + return value + + def unpair(self, client_uuid): + """Override super function to show setup message if unpaired.""" + super().unpair(client_uuid) + show_setup_message(self.hass, self.state.pincode) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 5d86dbc4612..447257f9e8f 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -38,9 +38,9 @@ def validate_entity_config(values): return entities -def show_setup_message(hass, bridge): +def show_setup_message(hass, pincode): """Display persistent notification with setup information.""" - pin = bridge.pincode.decode() + pin = pincode.decode() _LOGGER.info('Pincode: %s', pin) message = 'To setup Home Assistant in the Home App, enter the ' \ 'following code:\n### {}'.format(pin) diff --git a/requirements_all.txt b/requirements_all.txt index 6797be02b7f..ffcbc6001cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -28,7 +28,7 @@ Adafruit-SHT31==1.0.2 DoorBirdPy==0.1.3 # homeassistant.components.homekit -HAP-python==2.0.0 +HAP-python==2.1.0 # homeassistant.components.notify.mastodon Mastodon.py==1.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f12fdd7ce77..4baab1c79e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -19,7 +19,7 @@ requests_mock==1.5 # homeassistant.components.homekit -HAP-python==2.0.0 +HAP-python==2.1.0 # homeassistant.components.notify.html5 PyJWT==1.6.0 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index f12b80632b6..1b06e245734 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -116,26 +116,6 @@ def test_home_bridge(): # setup_message bridge.setup_message() - # add_paired_client - with patch('pyhap.accessory.Accessory.add_paired_client') \ - as mock_add_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'dismiss_setup_message') as mock_dissmiss_msg: - bridge.add_paired_client('client_uuid', 'client_public') - - mock_add_paired_client.assert_called_with('client_uuid', 'client_public') - mock_dissmiss_msg.assert_called_with('hass') - - # remove_paired_client - with patch('pyhap.accessory.Accessory.remove_paired_client') \ - as mock_remove_paired_client, \ - patch('homeassistant.components.homekit.accessories.' - 'show_setup_message') as mock_show_msg: - bridge.remove_paired_client('client_uuid') - - mock_remove_paired_client.assert_called_with('client_uuid') - mock_show_msg.assert_called_with('hass', bridge) - def test_home_driver(): """Test HomeDriver class.""" @@ -143,9 +123,30 @@ def test_home_driver(): ip_address = '127.0.0.1' port = 51826 path = '.homekit.state' + pin = b'123-45-678' with patch('pyhap.accessory_driver.AccessoryDriver.__init__') \ as mock_driver: - HomeDriver(bridge, ip_address, port, path) + driver = HomeDriver('hass', bridge, ip_address, port, path) mock_driver.assert_called_with(bridge, ip_address, port, path) + driver.state = Mock(pincode=pin) + + # pair + with patch('pyhap.accessory_driver.AccessoryDriver.pair') as mock_pair, \ + patch('homeassistant.components.homekit.accessories.' + 'dismiss_setup_message') as mock_dissmiss_msg: + driver.pair('client_uuid', 'client_public') + + mock_pair.assert_called_with('client_uuid', 'client_public') + mock_dissmiss_msg.assert_called_with('hass') + + # unpair + with patch('pyhap.accessory_driver.AccessoryDriver.unpair') \ + as mock_unpair, \ + patch('homeassistant.components.homekit.accessories.' + 'show_setup_message') as mock_show_msg: + driver.unpair('client_uuid') + + mock_unpair.assert_called_with('client_uuid') + mock_show_msg.assert_called_with('hass', pin) diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 23f117b15a0..b22a7f63cda 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -107,7 +107,8 @@ async def test_homekit_setup(hass): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - homekit.bridge, DEFAULT_PORT, IP_ADDRESS, path) + hass, homekit.bridge, port=DEFAULT_PORT, + address=IP_ADDRESS, persist_file=path) # Test if stop listener is setup assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 @@ -119,7 +120,8 @@ async def test_homekit_setup_ip_address(hass): with patch(PATH_HOMEKIT + '.accessories.HomeDriver') as mock_driver: await hass.async_add_job(homekit.setup) - mock_driver.assert_called_with(ANY, DEFAULT_PORT, '172.0.0.0', ANY) + mock_driver.assert_called_with( + hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) async def test_homekit_add_accessory(hass): @@ -167,9 +169,10 @@ async def test_homekit_entity_filter(hass): async def test_homekit_start(hass, debounce_patcher): """Test HomeKit start method.""" + pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) homekit.bridge = HomeBridge(hass) - homekit.driver = Mock() + homekit.driver = Mock(state=Mock(paired=False, pincode=pin)) hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] @@ -180,7 +183,7 @@ async def test_homekit_start(hass, debounce_patcher): await hass.async_add_job(homekit.start) mock_add_acc.assert_called_with(state) - mock_setup_msg.assert_called_with(hass, homekit.bridge) + mock_setup_msg.assert_called_with(hass, pin) assert homekit.driver.start.called is True assert homekit.status == STATUS_RUNNING diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f3ce35ee06b..42f81387960 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -2,7 +2,6 @@ import pytest import voluptuous as vol -from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( show_setup_message, dismiss_setup_message, convert_to_float, @@ -10,7 +9,7 @@ from homeassistant.components.homekit.util import ( from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - DOMAIN, ATTR_NOTIFICATION_ID) + DOMAIN, ATTR_MESSAGE, ATTR_NOTIFICATION_ID) from homeassistant.const import ( ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) @@ -74,16 +73,17 @@ def test_density_to_air_quality(): async def test_show_setup_msg(hass): """Test show setup message as persistence notification.""" - bridge = HomeBridge(hass) + pincode = b'123-45-678' call_create_notification = async_mock_service(hass, DOMAIN, 'create') - await hass.async_add_job(show_setup_message, hass, bridge) + await hass.async_add_job(show_setup_message, hass, pincode) await hass.async_block_till_done() assert call_create_notification assert call_create_notification[0].data[ATTR_NOTIFICATION_ID] == \ HOMEKIT_NOTIFY_ID + assert pincode.decode() in call_create_notification[0].data[ATTR_MESSAGE] async def test_dismiss_setup_msg(hass): From d7640e6ec3c8c35fba648e29c4895d065158b3c1 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Fri, 18 May 2018 08:42:09 -0700 Subject: [PATCH 115/144] Fix some ISY sensors not getting detected as binary sensors (#14497) Sensors that were defined via sensor_string were not getting properly identified as binary sensors when they had a uom defining them as binary (the other three methods of detecting binary sensors worked though.) --- homeassistant/components/isy994.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994.py b/homeassistant/components/isy994.py index 48a9499d1a9..ecabcd36a85 100644 --- a/homeassistant/components/isy994.py +++ b/homeassistant/components/isy994.py @@ -202,7 +202,7 @@ def _check_for_uom_id(hass: HomeAssistant, node, node_uom = set(map(str.lower, node.uom)) if uom_list: - if node_uom.intersection(NODE_FILTERS[single_domain]['uom']): + if node_uom.intersection(uom_list): hass.data[ISY994_NODES][single_domain].append(node) return True else: From 25970027c6c294978cd7b312d4140ed978d6e552 Mon Sep 17 00:00:00 2001 From: Sean Dague Date: Fri, 18 May 2018 13:37:43 -0400 Subject: [PATCH 116/144] Update mychevy to 0.4.0 (#14372) After 2 months of being offline, the my.chevy website seems to be working again. Some data structures changed in the mean time. The new library will handle multiple cars. This involves a breaking change in slug urls for devices where these now include the car make, model, and year in them. Discovery has to be delayed until after the initial site login to get the car metadata. --- .../components/binary_sensor/mychevy.py | 20 ++++++++---- homeassistant/components/mychevy.py | 25 +++++++++++---- homeassistant/components/sensor/mychevy.py | 32 ++++++++++++------- requirements_all.txt | 2 +- 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/binary_sensor/mychevy.py b/homeassistant/components/binary_sensor/mychevy.py index a89395ed86f..905e60c34d9 100644 --- a/homeassistant/components/binary_sensor/mychevy.py +++ b/homeassistant/components/binary_sensor/mychevy.py @@ -31,7 +31,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): sensors = [] hub = hass.data[MYCHEVY_DOMAIN] for sconfig in SENSORS: - sensors.append(EVBinarySensor(hub, sconfig)) + for car in hub.cars: + sensors.append(EVBinarySensor(hub, sconfig, car.vid)) async_add_devices(sensors) @@ -45,16 +46,18 @@ class EVBinarySensor(BinarySensorDevice): """ - def __init__(self, connection, config): + def __init__(self, connection, config, car_vid): """Initialize sensor with car connection.""" self._conn = connection self._name = config.name self._attr = config.attr self._type = config.device_class self._is_on = None - + self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + '{}_{}_{}'.format(MYCHEVY_DOMAIN, + slugify(self._car.name), + slugify(self._name))) @property def name(self): @@ -66,6 +69,11 @@ class EVBinarySensor(BinarySensorDevice): """Return if on.""" return self._is_on + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + @asyncio.coroutine def async_added_to_hass(self): """Register callbacks.""" @@ -75,8 +83,8 @@ class EVBinarySensor(BinarySensorDevice): @callback def async_update_callback(self): """Update state.""" - if self._conn.car is not None: - self._is_on = getattr(self._conn.car, self._attr, None) + if self._car is not None: + self._is_on = getattr(self._car, self._attr, None) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/mychevy.py b/homeassistant/components/mychevy.py index 678cdf10c56..3531c6b4919 100644 --- a/homeassistant/components/mychevy.py +++ b/homeassistant/components/mychevy.py @@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers import discovery from homeassistant.util import Throttle -REQUIREMENTS = ["mychevy==0.1.1"] +REQUIREMENTS = ["mychevy==0.4.0"] DOMAIN = 'mychevy' UPDATE_TOPIC = DOMAIN @@ -73,9 +73,6 @@ def setup(hass, base_config): hass.data[DOMAIN] = MyChevyHub(mc.MyChevy(email, password), hass) hass.data[DOMAIN].start() - discovery.load_platform(hass, 'sensor', DOMAIN, {}, config) - discovery.load_platform(hass, 'binary_sensor', DOMAIN, {}, config) - return True @@ -98,8 +95,9 @@ class MyChevyHub(threading.Thread): super().__init__() self._client = client self.hass = hass - self.car = None + self.cars = [] self.status = None + self.ready = False @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -109,7 +107,22 @@ class MyChevyHub(threading.Thread): (like 2 to 3 minutes long time) """ - self.car = self._client.data() + self._client.login() + self._client.get_cars() + self.cars = self._client.cars + if self.ready is not True: + discovery.load_platform(self.hass, 'sensor', DOMAIN, {}, {}) + discovery.load_platform(self.hass, 'binary_sensor', DOMAIN, {}, {}) + self.ready = True + self.cars = self._client.update_cars() + + def get_car(self, vid): + """Compatibility to work with one car.""" + if self.cars: + for car in self.cars: + if car.vid == vid: + return car + return None def run(self): """Thread run loop.""" diff --git a/homeassistant/components/sensor/mychevy.py b/homeassistant/components/sensor/mychevy.py index bdbffc46ca8..ef7c7ba8608 100644 --- a/homeassistant/components/sensor/mychevy.py +++ b/homeassistant/components/sensor/mychevy.py @@ -17,14 +17,15 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level from homeassistant.util import slugify -BATTERY_SENSOR = "percent" +BATTERY_SENSOR = "batteryLevel" SENSORS = [ - EVSensorConfig("Mileage", "mileage", "miles", "mdi:speedometer"), - EVSensorConfig("Range", "range", "miles", "mdi:speedometer"), - EVSensorConfig("Charging", "charging"), - EVSensorConfig("Charge Mode", "charge_mode"), - EVSensorConfig("EVCharge", BATTERY_SENSOR, "%", "mdi:battery") + EVSensorConfig("Mileage", "totalMiles", "miles", "mdi:speedometer"), + EVSensorConfig("Electric Range", "electricRange", "miles", + "mdi:speedometer"), + EVSensorConfig("Charged By", "estimatedFullChargeBy"), + EVSensorConfig("Charge Mode", "chargeMode"), + EVSensorConfig("Battery Level", BATTERY_SENSOR, "%", "mdi:battery") ] _LOGGER = logging.getLogger(__name__) @@ -38,7 +39,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): hub = hass.data[MYCHEVY_DOMAIN] sensors = [MyChevyStatus()] for sconfig in SENSORS: - sensors.append(EVSensor(hub, sconfig)) + for car in hub.cars: + sensors.append(EVSensor(hub, sconfig, car.vid)) add_devices(sensors) @@ -112,7 +114,7 @@ class EVSensor(Entity): """ - def __init__(self, connection, config): + def __init__(self, connection, config, car_vid): """Initialize sensor with car connection.""" self._conn = connection self._name = config.name @@ -120,9 +122,12 @@ class EVSensor(Entity): self._unit_of_measurement = config.unit_of_measurement self._icon = config.icon self._state = None + self._car_vid = car_vid self.entity_id = ENTITY_ID_FORMAT.format( - '{}_{}'.format(MYCHEVY_DOMAIN, slugify(self._name))) + '{}_{}_{}'.format(MYCHEVY_DOMAIN, + slugify(self._car.name), + slugify(self._name))) @asyncio.coroutine def async_added_to_hass(self): @@ -130,6 +135,11 @@ class EVSensor(Entity): self.hass.helpers.dispatcher.async_dispatcher_connect( UPDATE_TOPIC, self.async_update_callback) + @property + def _car(self): + """Return the car.""" + return self._conn.get_car(self._car_vid) + @property def icon(self): """Return the icon.""" @@ -145,8 +155,8 @@ class EVSensor(Entity): @callback def async_update_callback(self): """Update state.""" - if self._conn.car is not None: - self._state = getattr(self._conn.car, self._attr, None) + if self._car is not None: + self._state = getattr(self._car, self._attr, None) self.async_schedule_update_ha_state() @property diff --git a/requirements_all.txt b/requirements_all.txt index ffcbc6001cc..78efe76dce6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -543,7 +543,7 @@ motorparts==1.0.2 mutagen==1.40.0 # homeassistant.components.mychevy -mychevy==0.1.1 +mychevy==0.4.0 # homeassistant.components.mycroft mycroftapi==2.0 From c1127133eafd8e7439e4700d4881a9fd3ac3fbf4 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 19 May 2018 00:14:40 +0200 Subject: [PATCH 117/144] Set certifi to >=2018.04.16 (#14536) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7bc4fe5761a..4a7df44ee5e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6.1 -certifi>=2017.4.17 +certifi>=2018.04.16 attrs==18.1.0 # Breaks Python 3.6 and is not needed for our supported Python versions diff --git a/requirements_all.txt b/requirements_all.txt index 78efe76dce6..6cb1cdefe4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ typing>=3,<4 aiohttp==3.1.3 async_timeout==2.0.1 astral==1.6.1 -certifi>=2017.4.17 +certifi>=2018.04.16 attrs==18.1.0 # homeassistant.components.nuimo_controller diff --git a/setup.py b/setup.py index 6875230b7ab..2469f32d77e 100755 --- a/setup.py +++ b/setup.py @@ -52,7 +52,7 @@ REQUIRES = [ 'aiohttp==3.1.3', 'async_timeout==2.0.1', 'astral==1.6.1', - 'certifi>=2017.4.17', + 'certifi>=2018.04.16', 'attrs==18.1.0', ] From 8d06469efe21ad0c09d659f926fc7e96fcd5d10c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 May 2018 18:12:25 -0400 Subject: [PATCH 118/144] Bump frontend to 20180518.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 13c8d826377..8cc3c8ea473 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180518.0'] +REQUIREMENTS = ['home-assistant-frontend==20180518.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 6cb1cdefe4b..5d71464c08a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.0 +home-assistant-frontend==20180518.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4baab1c79e9..4a0db70f7d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.0 +home-assistant-frontend==20180518.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From f2dfc84d52c5308a597cb98efb861e63a194a060 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 May 2018 19:31:16 -0400 Subject: [PATCH 119/144] Version bump to 0.70.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acc30bcd57c..73c5ee00bc6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0.dev0' +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From b0e850ba5d55de5d646d286057aeba0d502d1307 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 May 2018 10:44:54 -0400 Subject: [PATCH 120/144] Bump frontend to 20180519.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8cc3c8ea473..d4700e5edd3 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180518.1'] +REQUIREMENTS = ['home-assistant-frontend==20180519.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 5d71464c08a..924f2297311 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a0db70f7d7..75a50d09a3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180518.1 +home-assistant-frontend==20180519.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 8854efd685950e22afc0030cefa5218554750434 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 May 2018 10:45:18 -0400 Subject: [PATCH 121/144] Version bump to 0.70.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73c5ee00bc6..68f4443d12e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From ba9bb90cf73d7c609510ddfd98dcfbab55170567 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 May 2018 11:01:35 -0400 Subject: [PATCH 122/144] Update frontend to 20180521.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d4700e5edd3..04e4e0dae48 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180519.0'] +REQUIREMENTS = ['home-assistant-frontend==20180521.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 924f2297311..b86398bb3e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75a50d09a3d..a4fe31b0639 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180519.0 +home-assistant-frontend==20180521.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From cfdea8d20f30b6494637ee65fa965f494b2b0022 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 20 May 2018 00:33:52 +0200 Subject: [PATCH 123/144] Wait for future mysensors gateway ready (#14398) * Wait for future mysensors gateway ready * Add an asyncio future that is done when the gateway reports the gateway ready message, I_GATEWAY_READY. * This will make sure that the gateway is ready before home assistant fires the home assistant start event. Automations can now send messages to the gateway when home assistant is started. * Use async timeout to wait max 15 seconds for ready gateway. * Address comments --- homeassistant/components/mysensors.py | 46 +++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mysensors.py b/homeassistant/components/mysensors.py index 6721669a026..1e7e252bd9d 100644 --- a/homeassistant/components/mysensors.py +++ b/homeassistant/components/mysensors.py @@ -12,13 +12,14 @@ import socket import sys from timeit import default_timer as timer +import async_timeout import voluptuous as vol from homeassistant.components.mqtt import ( valid_publish_topic, valid_subscribe_topic) from homeassistant.const import ( - ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON) + ATTR_BATTERY_LEVEL, CONF_NAME, CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP, + STATE_OFF, STATE_ON) from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -57,9 +58,11 @@ DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = '1.4' DOMAIN = 'mysensors' +GATEWAY_READY_TIMEOUT = 15.0 MQTT_COMPONENT = 'mqtt' MYSENSORS_GATEWAYS = 'mysensors_gateways' MYSENSORS_PLATFORM_DEVICES = 'mysensors_devices_{}' +MYSENSORS_GATEWAY_READY = 'mysensors_gateway_ready_{}' PLATFORM = 'platform' SCHEMA = 'schema' SIGNAL_CALLBACK = 'mysensors_callback_{}_{}_{}_{}' @@ -353,12 +356,12 @@ async def async_setup(hass, config): tcp_port = gway.get(CONF_TCP_PORT) in_prefix = gway.get(CONF_TOPIC_IN_PREFIX, '') out_prefix = gway.get(CONF_TOPIC_OUT_PREFIX, '') - ready_gateway = await setup_gateway( + gateway = await setup_gateway( device, persistence_file, baud_rate, tcp_port, in_prefix, out_prefix) - if ready_gateway is not None: - ready_gateway.nodes_config = gway.get(CONF_NODES) - gateways[id(ready_gateway)] = ready_gateway + if gateway is not None: + gateway.nodes_config = gway.get(CONF_NODES) + gateways[id(gateway)] = gateway if not gateways: _LOGGER.error( @@ -395,6 +398,35 @@ async def gw_start(hass, gateway): await gateway.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gw_stop) + if gateway.device == 'mqtt': + # Gatways connected via mqtt doesn't send gateway ready message. + return + gateway_ready = asyncio.Future() + gateway_ready_key = MYSENSORS_GATEWAY_READY.format(id(gateway)) + hass.data[gateway_ready_key] = gateway_ready + + try: + with async_timeout.timeout(GATEWAY_READY_TIMEOUT, loop=hass.loop): + await gateway_ready + except asyncio.TimeoutError: + _LOGGER.warning( + "Gateway %s not ready after %s secs so continuing with setup", + gateway.device, GATEWAY_READY_TIMEOUT) + finally: + hass.data.pop(gateway_ready_key, None) + + +@callback +def set_gateway_ready(hass, msg): + """Set asyncio future result if gateway is ready.""" + if (msg.type != msg.gateway.const.MessageType.internal or + msg.sub_type != msg.gateway.const.Internal.I_GATEWAY_READY): + return + gateway_ready = hass.data.get(MYSENSORS_GATEWAY_READY.format( + id(msg.gateway))) + if gateway_ready is None or gateway_ready.cancelled(): + return + gateway_ready.set_result(True) def validate_child(gateway, node_id, child): @@ -495,6 +527,8 @@ def gw_callback_factory(hass): _LOGGER.debug( "Node update: node %s child %s", msg.node_id, msg.child_id) + set_gateway_ready(hass, msg) + try: child = msg.gateway.sensors[msg.node_id].children[msg.child_id] except KeyError: From 2f8865d6cb64b252254874589a722cb886c405b8 Mon Sep 17 00:00:00 2001 From: cdce8p <30130371+cdce8p@users.noreply.github.com> Date: Mon, 21 May 2018 04:25:53 +0200 Subject: [PATCH 124/144] Homekit style cleanup (#14556) * Style cleanup * Sorted imports * Harmonized service calls * Test improvements * Small update --- homeassistant/components/homekit/__init__.py | 11 ++-- .../components/homekit/accessories.py | 5 +- homeassistant/components/homekit/const.py | 35 +++++------ .../components/homekit/type_covers.py | 25 ++++---- homeassistant/components/homekit/type_fans.py | 13 ++-- .../components/homekit/type_lights.py | 42 ++++++------- .../components/homekit/type_locks.py | 7 +-- .../homekit/type_security_systems.py | 12 ++-- .../components/homekit/type_sensors.py | 38 ++++++------ .../components/homekit/type_switches.py | 4 +- .../components/homekit/type_thermostats.py | 60 ++++++++++--------- tests/components/homekit/common.py | 8 +++ tests/components/homekit/test_accessories.py | 39 ++++++------ .../homekit/test_get_accessories.py | 15 ++--- tests/components/homekit/test_homekit.py | 49 +++++++-------- tests/components/homekit/test_type_covers.py | 4 +- tests/components/homekit/test_type_fans.py | 12 ++-- tests/components/homekit/test_type_lights.py | 6 +- tests/components/homekit/test_type_locks.py | 2 +- .../homekit/test_type_security_systems.py | 10 ++-- tests/components/homekit/test_type_sensors.py | 8 +-- .../components/homekit/test_type_switches.py | 2 +- .../homekit/test_type_thermostats.py | 8 +-- tests/components/homekit/test_util.py | 8 +-- 24 files changed, 209 insertions(+), 214 deletions(-) create mode 100644 tests/components/homekit/common.py diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index ce5f30d7bf2..202f9694689 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -13,17 +13,18 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, + CONF_IP_ADDRESS, CONF_NAME, CONF_PORT, + DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE) + TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import FILTER_SCHEMA from homeassistant.util import get_local_ip from homeassistant.util.decorator import Registry from .const import ( - CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_PORT, - DEFAULT_AUTO_START, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START, - DEVICE_CLASS_CO2, DEVICE_CLASS_PM25) + CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FILTER, DEFAULT_AUTO_START, + DEFAULT_PORT, DEVICE_CLASS_CO2, DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, + SERVICE_HOMEKIT_START) from .util import show_setup_message, validate_entity_config TYPES = Registry() diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ff835659221..ded4526b008 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -84,20 +84,21 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.entity_id, self.update_state_callback) + @ha_callback def update_state_callback(self, entity_id=None, old_state=None, new_state=None): """Callback from state change listener.""" _LOGGER.debug('New_state: %s', new_state) if new_state is None: return - self.update_state(new_state) + self.hass.async_add_job(self.update_state, new_state) def update_state(self, new_state): """Method called on state change to update HomeKit value. Overridden by accessory types. """ - pass + raise NotImplementedError() class HomeBridge(Bridge): diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index adde13cc030..21cad2d9cf7 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,23 +1,23 @@ """Constants used be the HomeKit component.""" -# #### MISC #### +# #### Misc #### DEBOUNCE_TIMEOUT = 0.5 DOMAIN = 'homekit' HOMEKIT_FILE = '.homekit.state' HOMEKIT_NOTIFY_ID = 4663548 -# #### CONFIG #### +# #### Config #### CONF_AUTO_START = 'auto_start' CONF_ENTITY_CONFIG = 'entity_config' CONF_FILTER = 'filter' -# #### CONFIG DEFAULTS #### +# #### Config Defaults #### DEFAULT_AUTO_START = True DEFAULT_PORT = 51827 -# #### HOMEKIT COMPONENT SERVICES #### +# #### HomeKit Component Services #### SERVICE_HOMEKIT_START = 'start' -# #### STRING CONSTANTS #### +# #### String Constants #### BRIDGE_MODEL = 'Bridge' BRIDGE_NAME = 'Home Assistant Bridge' BRIDGE_SERIAL_NUMBER = 'homekit.bridge' @@ -31,10 +31,10 @@ SERV_CARBON_MONOXIDE_SENSOR = 'CarbonMonoxideSensor' SERV_CONTACT_SENSOR = 'ContactSensor' SERV_FANV2 = 'Fanv2' SERV_GARAGE_DOOR_OPENER = 'GarageDoorOpener' -SERV_HUMIDITY_SENSOR = 'HumiditySensor' # CurrentRelativeHumidity +SERV_HUMIDITY_SENSOR = 'HumiditySensor' SERV_LEAK_SENSOR = 'LeakSensor' SERV_LIGHT_SENSOR = 'LightSensor' -SERV_LIGHTBULB = 'Lightbulb' # On | Brightness, Hue, Saturation, Name +SERV_LIGHTBULB = 'Lightbulb' SERV_LOCK = 'LockMechanism' SERV_MOTION_SENSOR = 'MotionSensor' SERV_OCCUPANCY_SENSOR = 'OccupancySensor' @@ -44,13 +44,12 @@ SERV_SWITCH = 'Switch' SERV_TEMPERATURE_SENSOR = 'TemperatureSensor' SERV_THERMOSTAT = 'Thermostat' SERV_WINDOW_COVERING = 'WindowCovering' -# CurrentPosition, TargetPosition, PositionState # #### Characteristics #### CHAR_ACTIVE = 'Active' CHAR_AIR_PARTICULATE_DENSITY = 'AirParticulateDensity' CHAR_AIR_QUALITY = 'AirQuality' -CHAR_BRIGHTNESS = 'Brightness' # Int | [0, 100] +CHAR_BRIGHTNESS = 'Brightness' CHAR_CARBON_DIOXIDE_DETECTED = 'CarbonDioxideDetected' CHAR_CARBON_DIOXIDE_LEVEL = 'CarbonDioxideLevel' CHAR_CARBON_DIOXIDE_PEAK_LEVEL = 'CarbonDioxidePeakLevel' @@ -61,13 +60,13 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = 'CoolingThresholdTemperature' CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = 'CurrentAmbientLightLevel' CHAR_CURRENT_DOOR_STATE = 'CurrentDoorState' CHAR_CURRENT_HEATING_COOLING = 'CurrentHeatingCoolingState' -CHAR_CURRENT_POSITION = 'CurrentPosition' # Int | [0, 100] -CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' # percent +CHAR_CURRENT_POSITION = 'CurrentPosition' +CHAR_CURRENT_HUMIDITY = 'CurrentRelativeHumidity' CHAR_CURRENT_SECURITY_STATE = 'SecuritySystemCurrentState' CHAR_CURRENT_TEMPERATURE = 'CurrentTemperature' CHAR_FIRMWARE_REVISION = 'FirmwareRevision' CHAR_HEATING_THRESHOLD_TEMPERATURE = 'HeatingThresholdTemperature' -CHAR_HUE = 'Hue' # arcdegress | [0, 360] +CHAR_HUE = 'Hue' CHAR_LEAK_DETECTED = 'LeakDetected' CHAR_LOCK_CURRENT_STATE = 'LockCurrentState' CHAR_LOCK_TARGET_STATE = 'LockTargetState' @@ -77,16 +76,16 @@ CHAR_MODEL = 'Model' CHAR_MOTION_DETECTED = 'MotionDetected' CHAR_NAME = 'Name' CHAR_OCCUPANCY_DETECTED = 'OccupancyDetected' -CHAR_ON = 'On' # boolean +CHAR_ON = 'On' CHAR_POSITION_STATE = 'PositionState' CHAR_ROTATION_DIRECTION = 'RotationDirection' -CHAR_SATURATION = 'Saturation' # percent +CHAR_SATURATION = 'Saturation' CHAR_SERIAL_NUMBER = 'SerialNumber' CHAR_SMOKE_DETECTED = 'SmokeDetected' CHAR_SWING_MODE = 'SwingMode' CHAR_TARGET_DOOR_STATE = 'TargetDoorState' CHAR_TARGET_HEATING_COOLING = 'TargetHeatingCoolingState' -CHAR_TARGET_POSITION = 'TargetPosition' # Int | [0, 100] +CHAR_TARGET_POSITION = 'TargetPosition' CHAR_TARGET_SECURITY_STATE = 'SecuritySystemTargetState' CHAR_TARGET_TEMPERATURE = 'TargetTemperature' CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' @@ -94,21 +93,17 @@ CHAR_TEMP_DISPLAY_UNITS = 'TemperatureDisplayUnits' # #### Properties #### PROP_MAX_VALUE = 'maxValue' PROP_MIN_VALUE = 'minValue' - PROP_CELSIUS = {'minValue': -273, 'maxValue': 999} -# #### Device Class #### +# #### Device Classes #### DEVICE_CLASS_CO2 = 'co2' DEVICE_CLASS_DOOR = 'door' DEVICE_CLASS_GARAGE_DOOR = 'garage_door' DEVICE_CLASS_GAS = 'gas' -DEVICE_CLASS_HUMIDITY = 'humidity' -DEVICE_CLASS_LIGHT = 'light' DEVICE_CLASS_MOISTURE = 'moisture' DEVICE_CLASS_MOTION = 'motion' DEVICE_CLASS_OCCUPANCY = 'occupancy' DEVICE_CLASS_OPENING = 'opening' DEVICE_CLASS_PM25 = 'pm25' DEVICE_CLASS_SMOKE = 'smoke' -DEVICE_CLASS_TEMPERATURE = 'temperature' DEVICE_CLASS_WINDOW = 'window' diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index a32ba0370ec..cf0620a4e30 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -1,21 +1,21 @@ """Class to hold all cover accessories.""" import logging -from pyhap.const import CATEGORY_WINDOW_COVERING, CATEGORY_GARAGE_DOOR_OPENER +from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( - ATTR_ENTITY_ID, SERVICE_SET_COVER_POSITION, STATE_OPEN, STATE_CLOSED, - SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER, SERVICE_STOP_COVER, - ATTR_SUPPORTED_FEATURES) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, + STATE_CLOSED, STATE_OPEN) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_WINDOW_COVERING, CHAR_CURRENT_POSITION, - CHAR_TARGET_POSITION, CHAR_POSITION_STATE, - SERV_GARAGE_DOOR_OPENER, CHAR_CURRENT_DOOR_STATE, CHAR_TARGET_DOOR_STATE) + CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, CHAR_POSITION_STATE, + CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, + SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING) _LOGGER = logging.getLogger(__name__) @@ -44,12 +44,13 @@ class GarageDoorOpener(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} if value == 0: self.char_current_state.set_value(3) - self.hass.components.cover.open_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_OPEN_COVER, params) elif value == 1: self.char_current_state.set_value(2) - self.hass.components.cover.close_cover(self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_CLOSE_COVER, params) def update_state(self, new_state): """Update cover state after state changed.""" @@ -141,8 +142,8 @@ class WindowCoveringBasic(HomeAccessory): else: service, position = (SERVICE_CLOSE_COVER, 0) - self.hass.services.call(DOMAIN, service, - {ATTR_ENTITY_ID: self.entity_id}) + params = {ATTR_ENTITY_ID: self.entity_id} + self.hass.services.call(DOMAIN, service, params) # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index a3ea027c07e..bf0d4da6a59 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -4,12 +4,12 @@ import logging from pyhap.const import CATEGORY_FAN from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, + SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, - SERVICE_TURN_OFF, SERVICE_TURN_ON) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_OFF, STATE_ON) from . import TYPES from .accessories import HomeAccessory @@ -71,8 +71,7 @@ class Fan(HomeAccessory): _LOGGER.debug('%s: Set direction to %d', self.entity_id, value) self._flag[CHAR_ROTATION_DIRECTION] = True direction = DIRECTION_REVERSE if value == 1 else DIRECTION_FORWARD - params = {ATTR_ENTITY_ID: self.entity_id, - ATTR_DIRECTION: direction} + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_DIRECTION: direction} self.hass.services.call(DOMAIN, SERVICE_SET_DIRECTION, params) def set_oscillating(self, value): diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index dae3579a97a..da012799602 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -4,16 +4,18 @@ import logging from pyhap.const import CATEGORY_LIGHTBULB from homeassistant.components.light import ( - ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_BRIGHTNESS, ATTR_MIN_MIREDS, - ATTR_MAX_MIREDS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_BRIGHTNESS) -from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, DOMAIN, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP) +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, SERVICE_TURN_ON, + SERVICE_TURN_OFF, STATE_OFF, STATE_ON) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_LIGHTBULB, CHAR_COLOR_TEMPERATURE, - CHAR_BRIGHTNESS, CHAR_HUE, CHAR_ON, CHAR_SATURATION, - PROP_MAX_VALUE, PROP_MIN_VALUE) + CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, CHAR_HUE, CHAR_ON, + CHAR_SATURATION, SERV_LIGHTBULB, PROP_MAX_VALUE, PROP_MIN_VALUE) _LOGGER = logging.getLogger(__name__) @@ -79,28 +81,27 @@ class Light(HomeAccessory): _LOGGER.debug('%s: Set state to %d', self.entity_id, value) self._flag[CHAR_ON] = True - - if value == 1: - self.hass.components.light.turn_on(self.entity_id) - elif value == 0: - self.hass.components.light.turn_off(self.entity_id) + params = {ATTR_ENTITY_ID: self.entity_id} + service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF + self.hass.services.call(DOMAIN, service, params) @debounce def set_brightness(self, value): """Set brightness if call came from HomeKit.""" _LOGGER.debug('%s: Set brightness to %d', self.entity_id, value) self._flag[CHAR_BRIGHTNESS] = True - if value != 0: - self.hass.components.light.turn_on( - self.entity_id, brightness_pct=value) - else: - self.hass.components.light.turn_off(self.entity_id) + if value == 0: + self.set_state(0) # Turn off light + return + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" _LOGGER.debug('%s: Set color temp to %s', self.entity_id, value) self._flag[CHAR_COLOR_TEMPERATURE] = True - self.hass.components.light.turn_on(self.entity_id, color_temp=value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def set_saturation(self, value): """Set saturation if call came from HomeKit.""" @@ -118,15 +119,14 @@ class Light(HomeAccessory): def set_color(self): """Set color if call came from HomeKit.""" - # Handle Color if self._features & SUPPORT_COLOR and self._flag[CHAR_HUE] and \ self._flag[CHAR_SATURATION]: color = (self._hue, self._saturation) _LOGGER.debug('%s: Set hs_color to %s', self.entity_id, color) self._flag.update({ CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True}) - self.hass.components.light.turn_on( - self.entity_id, hs_color=color) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 309f3072768..05ab6c6f822 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -4,13 +4,12 @@ import logging from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import ( - ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) + ATTR_ENTITY_ID, DOMAIN, STATE_LOCKED, STATE_UNLOCKED, STATE_UNKNOWN) from homeassistant.const import ATTR_CODE from . import TYPES from .accessories import HomeAccessory -from .const import ( - SERV_LOCK, CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE) +from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK _LOGGER = logging.getLogger(__name__) @@ -55,7 +54,7 @@ class Lock(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._code: params[ATTR_CODE] = self._code - self.hass.services.call('lock', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update lock after state changed.""" diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index bd29453e10a..bbf8b3f17cb 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -3,16 +3,16 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from homeassistant.components.alarm_control_panel import DOMAIN from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, ATTR_ENTITY_ID, ATTR_CODE) + ATTR_ENTITY_ID, ATTR_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_TRIGGERED, STATE_ALARM_DISARMED) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_SECURITY_SYSTEM, CHAR_CURRENT_SECURITY_STATE, - CHAR_TARGET_SECURITY_STATE) + CHAR_CURRENT_SECURITY_STATE, CHAR_TARGET_SECURITY_STATE, + SERV_SECURITY_SYSTEM) _LOGGER = logging.getLogger(__name__) @@ -56,7 +56,7 @@ class SecuritySystem(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} if self._alarm_code: params[ATTR_CODE] = self._alarm_code - self.hass.services.call('alarm_control_panel', service, params) + self.hass.services.call(DOMAIN, service, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 0005c6184ee..373c1188f2d 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -4,26 +4,26 @@ import logging from pyhap.const import CATEGORY_SENSOR from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, - ATTR_DEVICE_CLASS, STATE_ON, STATE_HOME) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_ON, STATE_HOME, + TEMP_CELSIUS) from . import TYPES from .accessories import HomeAccessory from .const import ( - SERV_HUMIDITY_SENSOR, SERV_TEMPERATURE_SENSOR, - CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, PROP_CELSIUS, - SERV_AIR_QUALITY_SENSOR, CHAR_AIR_QUALITY, CHAR_AIR_PARTICULATE_DENSITY, - CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL, - SERV_LIGHT_SENSOR, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, - DEVICE_CLASS_CO2, SERV_CARBON_DIOXIDE_SENSOR, CHAR_CARBON_DIOXIDE_DETECTED, - DEVICE_CLASS_GAS, SERV_CARBON_MONOXIDE_SENSOR, - CHAR_CARBON_MONOXIDE_DETECTED, - DEVICE_CLASS_MOISTURE, SERV_LEAK_SENSOR, CHAR_LEAK_DETECTED, - DEVICE_CLASS_MOTION, SERV_MOTION_SENSOR, CHAR_MOTION_DETECTED, - DEVICE_CLASS_OCCUPANCY, SERV_OCCUPANCY_SENSOR, CHAR_OCCUPANCY_DETECTED, - DEVICE_CLASS_OPENING, SERV_CONTACT_SENSOR, CHAR_CONTACT_SENSOR_STATE, - DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_WINDOW, - DEVICE_CLASS_SMOKE, SERV_SMOKE_SENSOR, CHAR_SMOKE_DETECTED) + CHAR_AIR_PARTICULATE_DENSITY, CHAR_AIR_QUALITY, + CHAR_CARBON_DIOXIDE_DETECTED, CHAR_CARBON_DIOXIDE_LEVEL, + CHAR_CARBON_DIOXIDE_PEAK_LEVEL, CHAR_CARBON_MONOXIDE_DETECTED, + CHAR_CONTACT_SENSOR_STATE, CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_LEAK_DETECTED, + CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, CHAR_SMOKE_DETECTED, + DEVICE_CLASS_CO2, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE_DOOR, + DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, DEVICE_CLASS_SMOKE, + DEVICE_CLASS_WINDOW, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, + SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR, + SERV_CONTACT_SENSOR, SERV_HUMIDITY_SENSOR, SERV_LEAK_SENSOR, + SERV_LIGHT_SENSOR, SERV_MOTION_SENSOR, SERV_OCCUPANCY_SENSOR, + SERV_SMOKE_SENSOR, SERV_TEMPERATURE_SENSOR) from .util import ( convert_to_float, temperature_to_homekit, density_to_air_quality) @@ -108,7 +108,7 @@ class AirQualitySensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) - if density is not None: + if density: self.char_density.set_value(density) self.char_quality.set_value(density_to_air_quality(density)) _LOGGER.debug('%s: Set to %d', self.entity_id, density) @@ -134,7 +134,7 @@ class CarbonDioxideSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" co2 = convert_to_float(new_state.state) - if co2 is not None: + if co2: self.char_co2.set_value(co2) if co2 > self.char_peak.value: self.char_peak.set_value(co2) @@ -157,7 +157,7 @@ class LightSensor(HomeAccessory): def update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) - if luminance is not None: + if luminance: self.char_light.set_value(luminance) _LOGGER.debug('%s: Set to %d', self.entity_id, luminance) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index ff4bf1611b8..5754266587c 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -33,9 +33,9 @@ class Switch(HomeAccessory): _LOGGER.debug('%s: Set switch state to %s', self.entity_id, value) self.flag_target_state = True + params = {ATTR_ENTITY_ID: self.entity_id} service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF - self.hass.services.call(self._domain, service, - {ATTR_ENTITY_ID: self.entity_id}) + self.hass.services.call(self._domain, service, params) def update_state(self, new_state): """Update switch state after state changed.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index ab4d7faf875..d6555d5056d 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,22 +4,23 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate import ( - ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, - STATE_HEAT, STATE_COOL, STATE_AUTO, SUPPORT_ON_OFF, - SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) + ATTR_CURRENT_TEMPERATURE, ATTR_OPERATION_LIST, ATTR_OPERATION_MODE, + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DOMAIN, SERVICE_SET_TEMPERATURE, SERVICE_SET_OPERATION_MODE, STATE_AUTO, + STATE_COOL, STATE_HEAT, SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE_HIGH, + SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, - STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, + TEMP_CELSIUS, TEMP_FAHRENHEIT) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import debounce, HomeAccessory from .const import ( - SERV_THERMOSTAT, CHAR_CURRENT_HEATING_COOLING, - CHAR_TARGET_HEATING_COOLING, CHAR_CURRENT_TEMPERATURE, - CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, - CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) + CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, + CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_TEMPERATURE, + CHAR_TEMP_DISPLAY_UNITS, SERV_THERMOSTAT) from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) @@ -99,12 +100,13 @@ class Thermostat(HomeAccessory): if self.support_power_state is True: params = {ATTR_ENTITY_ID: self.entity_id} if hass_value == STATE_OFF: - self.hass.services.call('climate', 'turn_off', params) + self.hass.services.call(DOMAIN, SERVICE_TURN_OFF, params) return else: - self.hass.services.call('climate', 'turn_on', params) - self.hass.components.climate.set_operation_mode( - operation_mode=hass_value, entity_id=self.entity_id) + self.hass.services.call(DOMAIN, SERVICE_TURN_ON, params) + params = {ATTR_ENTITY_ID: self.entity_id, + ATTR_OPERATION_MODE: hass_value} + self.hass.services.call(DOMAIN, SERVICE_SET_OPERATION_MODE, params) @debounce def set_cooling_threshold(self, value): @@ -113,11 +115,11 @@ class Thermostat(HomeAccessory): self.entity_id, value) self.coolingthresh_flag_target_state = True low = self.char_heating_thresh_temp.value - low = temperature_to_states(low, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=value, - target_temp_low=low) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(value, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(low, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_heating_threshold(self, value): @@ -125,13 +127,12 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set heating threshold temperature to %.2f°C', self.entity_id, value) self.heatingthresh_flag_target_state = True - # Home assistant always wants to set low and high at the same time high = self.char_cooling_thresh_temp.value - high = temperature_to_states(high, self._unit) - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - entity_id=self.entity_id, target_temp_high=high, - target_temp_low=value) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TARGET_TEMP_HIGH: temperature_to_states(high, self._unit), + ATTR_TARGET_TEMP_LOW: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) @debounce def set_target_temperature(self, value): @@ -139,9 +140,10 @@ class Thermostat(HomeAccessory): _LOGGER.debug('%s: Set target temperature to %.2f°C', self.entity_id, value) self.temperature_flag_target_state = True - value = temperature_to_states(value, self._unit) - self.hass.components.climate.set_temperature( - temperature=value, entity_id=self.entity_id) + params = { + ATTR_ENTITY_ID: self.entity_id, + ATTR_TEMPERATURE: temperature_to_states(value, self._unit)} + self.hass.services.call(DOMAIN, SERVICE_SET_TEMPERATURE, params) def update_state(self, new_state): """Update security state after state changed.""" diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py new file mode 100644 index 00000000000..915759f22d6 --- /dev/null +++ b/tests/components/homekit/common.py @@ -0,0 +1,8 @@ +"""Collection of fixtures and functions for the HomeKit tests.""" +from unittest.mock import patch + + +def patch_debounce(): + """Return patch for debounce method.""" + return patch('homeassistant.components.homekit.accessories.debounce', + lambda f: lambda *args, **kwargs: f(*args, **kwargs)) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 1b06e245734..3d1c335f8ae 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -5,22 +5,18 @@ This includes tests for all mock object types. from datetime import datetime, timedelta from unittest.mock import patch, Mock +import pytest + from homeassistant.components.homekit.accessories import ( debounce, HomeAccessory, HomeBridge, HomeDriver) from homeassistant.components.homekit.const import ( - BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, SERV_ACCESSORY_INFO, - CHAR_FIRMWARE_REVISION, CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, - CHAR_SERIAL_NUMBER, MANUFACTURER) + BRIDGE_MODEL, BRIDGE_NAME, BRIDGE_SERIAL_NUMBER, CHAR_FIRMWARE_REVISION, + CHAR_MANUFACTURER, CHAR_MODEL, CHAR_NAME, CHAR_SERIAL_NUMBER, + MANUFACTURER, SERV_ACCESSORY_INFO) from homeassistant.const import __version__, ATTR_NOW, EVENT_TIME_CHANGED import homeassistant.util.dt as dt_util -def patch_debounce(): - """Return patch for debounce method.""" - return patch('homeassistant.components.homekit.accessories.debounce', - lambda f: lambda *args, **kwargs: f(*args, **kwargs)) - - async def test_debounce(hass): """Test add_timeout decorator function.""" def demo_func(*args): @@ -74,20 +70,23 @@ async def test_home_accessory(hass): assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == \ 'homekit.accessory' - hass.states.async_set('homekit.accessory', 'on') - await hass.async_block_till_done() - await hass.async_add_job(acc.run) - hass.states.async_set('homekit.accessory', 'off') + hass.states.async_set(entity_id, 'on') await hass.async_block_till_done() + with patch('homeassistant.components.homekit.accessories.' + 'HomeAccessory.update_state') as mock_update_state: + await hass.async_add_job(acc.run) + state = hass.states.get(entity_id) + mock_update_state.assert_called_with(state) - entity_id = 'test_model.demo' - hass.states.async_set(entity_id, None) - await hass.async_block_till_done() + hass.states.async_remove(entity_id) + await hass.async_block_till_done() + assert mock_update_state.call_count == 1 - acc = HomeAccessory('hass', 'test_name', entity_id, 2, None) - assert acc.display_name == 'test_name' - assert acc.aid == 2 - assert len(acc.services) == 1 + with pytest.raises(NotImplementedError): + acc.update_state('new_state') + + # Test model name from domain + acc = HomeAccessory('hass', 'test_name', 'test_model.demo', 2, None) serv = acc.services[0] # SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_MODEL).value == 'Test Model' diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 0ffc1ae4767..25a0dd3f1cb 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -4,13 +4,13 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State -from homeassistant.components.cover import SUPPORT_OPEN, SUPPORT_CLOSE +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.climate import ( SUPPORT_TARGET_TEMPERATURE_HIGH, SUPPORT_TARGET_TEMPERATURE_LOW) from homeassistant.components.homekit import get_accessory, TYPES from homeassistant.const import ( ATTR_CODE, ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, - ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT) def test_not_supported(caplog): @@ -40,14 +40,12 @@ def test_customize_options(config, name): ('Fan', 'fan.test', 'on', {}, {}), ('Light', 'light.test', 'on', {}, {}), ('Lock', 'lock.test', 'locked', {}, {ATTR_CODE: '1234'}), - + ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, + {ATTR_CODE: '1234'}), ('Thermostat', 'climate.test', 'auto', {}, {}), ('Thermostat', 'climate.test', 'auto', {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_LOW | SUPPORT_TARGET_TEMPERATURE_HIGH}, {}), - - ('SecuritySystem', 'alarm_control_panel.test', 'armed', {}, - {ATTR_CODE: '1234'}), ]) def test_types(type_name, entity_id, state, attrs, config): """Test if types are associated correctly.""" @@ -83,22 +81,17 @@ def test_type_covers(type_name, entity_id, state, attrs): ('BinarySensor', 'binary_sensor.opening', 'on', {ATTR_DEVICE_CLASS: 'opening'}), ('BinarySensor', 'device_tracker.someone', 'not_home', {}), - ('AirQualitySensor', 'sensor.air_quality_pm25', '40', {}), ('AirQualitySensor', 'sensor.air_quality', '40', {ATTR_DEVICE_CLASS: 'pm25'}), - ('CarbonDioxideSensor', 'sensor.airmeter_co2', '500', {}), ('CarbonDioxideSensor', 'sensor.airmeter', '500', {ATTR_DEVICE_CLASS: 'co2'}), - ('HumiditySensor', 'sensor.humidity', '20', {ATTR_DEVICE_CLASS: 'humidity', ATTR_UNIT_OF_MEASUREMENT: '%'}), - ('LightSensor', 'sensor.light', '900', {ATTR_DEVICE_CLASS: 'illuminance'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lm'}), ('LightSensor', 'sensor.light', '900', {ATTR_UNIT_OF_MEASUREMENT: 'lx'}), - ('TemperatureSensor', 'sensor.temperature', '23', {ATTR_DEVICE_CLASS: 'temperature'}), ('TemperatureSensor', 'sensor.temperature', '23', diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index b22a7f63cda..31337088b33 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -6,29 +6,28 @@ import pytest from homeassistant import setup from homeassistant.core import State from homeassistant.components.homekit import ( - HomeKit, generate_aid, - STATUS_READY, STATUS_RUNNING, STATUS_STOPPED, STATUS_WAIT) + generate_aid, HomeKit, STATUS_READY, STATUS_RUNNING, + STATUS_STOPPED, STATUS_WAIT) from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( - DOMAIN, HOMEKIT_FILE, CONF_AUTO_START, - DEFAULT_PORT, SERVICE_HOMEKIT_START) + CONF_AUTO_START, DEFAULT_PORT, DOMAIN, HOMEKIT_FILE, SERVICE_HOMEKIT_START) from homeassistant.helpers.entityfilter import generate_filter from homeassistant.const import ( CONF_IP_ADDRESS, CONF_PORT, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP) -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce IP_ADDRESS = '127.0.0.1' PATH_HOMEKIT = 'homeassistant.components.homekit' -@pytest.fixture('module') -def debounce_patcher(request): +@pytest.fixture(scope='module') +def debounce_patcher(): """Patch debounce method.""" patcher = patch_debounce() - patcher.start() - request.addfinalizer(patcher.stop) + yield patcher.start() + patcher.stop() def test_generate_aid(): @@ -124,27 +123,25 @@ async def test_homekit_setup_ip_address(hass): hass, ANY, port=DEFAULT_PORT, address='172.0.0.0', persist_file=ANY) -async def test_homekit_add_accessory(hass): +async def test_homekit_add_accessory(): """Add accessory if config exists and get_acc returns an accessory.""" - homekit = HomeKit(hass, None, None, lambda entity_id: True, {}) - homekit.bridge = HomeBridge(hass) + homekit = HomeKit('hass', None, None, lambda entity_id: True, {}) + homekit.bridge = mock_bridge = Mock() - with patch(PATH_HOMEKIT + '.accessories.HomeBridge.add_accessory') \ - as mock_add_acc, \ - patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: + with patch(PATH_HOMEKIT + '.get_accessory') as mock_get_acc: mock_get_acc.side_effect = [None, 'acc', None] homekit.add_bridge_accessory(State('light.demo', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 363398124, {}) - assert mock_add_acc.called is False + mock_get_acc.assert_called_with('hass', ANY, 363398124, {}) + assert not mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 294192020, {}) - assert mock_add_acc.called is True + mock_get_acc.assert_called_with('hass', ANY, 294192020, {}) + assert mock_bridge.add_accessory.called homekit.add_bridge_accessory(State('demo.test_2', 'on')) - mock_get_acc.assert_called_with(hass, ANY, 429982757, {}) - mock_add_acc.assert_called_with('acc') + mock_get_acc.assert_called_with('hass', ANY, 429982757, {}) + mock_bridge.add_accessory.assert_called_with('acc') async def test_homekit_entity_filter(hass): @@ -171,8 +168,8 @@ async def test_homekit_start(hass, debounce_patcher): """Test HomeKit start method.""" pin = b'123-45-678' homekit = HomeKit(hass, None, None, {}, {'cover.demo': {}}) - homekit.bridge = HomeBridge(hass) - homekit.driver = Mock(state=Mock(paired=False, pincode=pin)) + homekit.bridge = Mock() + homekit.driver = mock_driver = Mock(state=Mock(paired=False, pincode=pin)) hass.states.async_set('light.demo', 'on') state = hass.states.async_all()[0] @@ -184,13 +181,13 @@ async def test_homekit_start(hass, debounce_patcher): mock_add_acc.assert_called_with(state) mock_setup_msg.assert_called_with(hass, pin) - assert homekit.driver.start.called is True + assert mock_driver.start.called is True assert homekit.status == STATUS_RUNNING # Test start() if already started - homekit.driver.reset_mock() + mock_driver.reset_mock() await hass.async_add_job(homekit.start) - assert homekit.driver.start.called is False + assert mock_driver.start.called is False async def test_homekit_stop(hass): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 7260ae40c1a..8138d1c506b 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -4,13 +4,13 @@ from collections import namedtuple import pytest from homeassistant.components.cover import ( - DOMAIN, ATTR_CURRENT_POSITION, ATTR_POSITION, SUPPORT_STOP) + ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN, SUPPORT_STOP) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index fc504cc6cbd..f96fe19d603 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.fan import ( - ATTR_DIRECTION, ATTR_OSCILLATING, - DIRECTION_FORWARD, DIRECTION_REVERSE, DOMAIN, SERVICE_OSCILLATE, - SERVICE_SET_DIRECTION, SUPPORT_DIRECTION, SUPPORT_OSCILLATE) + ATTR_DIRECTION, ATTR_OSCILLATING, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE) from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - STATE_ON, STATE_OFF, STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, + STATE_UNKNOWN, SERVICE_TURN_ON, SERVICE_TURN_OFF) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 65a526edcc3..7a1db7b3f71 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -4,14 +4,14 @@ from collections import namedtuple import pytest from homeassistant.components.light import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) + ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + DOMAIN, SUPPORT_BRIGHTNESS, SUPPORT_COLOR_TEMP, SUPPORT_COLOR) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_OFF, STATE_UNKNOWN) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_type_locks.py b/tests/components/homekit/test_type_locks.py index 3b8cde47fcb..f4698b1380b 100644 --- a/tests/components/homekit/test_type_locks.py +++ b/tests/components/homekit/test_type_locks.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components.homekit.type_locks import Lock from homeassistant.components.lock import DOMAIN from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_UNLOCKED, STATE_LOCKED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index 577d2f2175d..7b72404cdaa 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -2,12 +2,12 @@ import pytest from homeassistant.components.alarm_control_panel import DOMAIN -from homeassistant.components.homekit.type_security_systems import ( - SecuritySystem) +from homeassistant.components.homekit.type_security_systems import \ + SecuritySystem from homeassistant.const import ( - ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED) + ATTR_CODE, ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, + STATE_UNKNOWN) from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 56742bada92..e36ae67da12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -1,11 +1,11 @@ """Test different accessory types: Sensors.""" from homeassistant.components.homekit.const import PROP_CELSIUS from homeassistant.components.homekit.type_sensors import ( - TemperatureSensor, HumiditySensor, AirQualitySensor, CarbonDioxideSensor, - LightSensor, BinarySensor, BINARY_SENSOR_SERVICE_MAP) + AirQualitySensor, BinarySensor, CarbonDioxideSensor, HumiditySensor, + LightSensor, TemperatureSensor, BINARY_SENSOR_SERVICE_MAP) from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_DEVICE_CLASS, STATE_UNKNOWN, STATE_ON, - STATE_OFF, STATE_HOME, STATE_NOT_HOME, TEMP_CELSIUS, TEMP_FAHRENHEIT) + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_HOME, STATE_NOT_HOME, + STATE_OFF, STATE_ON, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) async def test_temperature(hass): diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 399a8bd84c8..5fc0b6ce1b9 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -3,7 +3,7 @@ import pytest from homeassistant.core import split_entity_id from homeassistant.components.homekit.type_switches import Switch -from homeassistant.const import ATTR_ENTITY_ID, STATE_ON, STATE_OFF +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from tests.common import async_mock_service diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index bc5b3219cdf..337ad23ad05 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -4,15 +4,15 @@ from collections import namedtuple import pytest from homeassistant.components.climate import ( - DOMAIN, ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, - ATTR_OPERATION_LIST, STATE_COOL, STATE_HEAT, STATE_AUTO) + ATTR_CURRENT_TEMPERATURE, ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, + ATTR_TARGET_TEMP_HIGH, ATTR_OPERATION_MODE, ATTR_OPERATION_LIST, + DOMAIN, STATE_AUTO, STATE_COOL, STATE_HEAT) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service -from tests.components.homekit.test_accessories import patch_debounce +from tests.components.homekit.common import patch_debounce @pytest.fixture(scope='module') diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 42f81387960..0755e8f54d4 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -4,14 +4,14 @@ import voluptuous as vol from homeassistant.components.homekit.const import HOMEKIT_NOTIFY_ID from homeassistant.components.homekit.util import ( - show_setup_message, dismiss_setup_message, convert_to_float, - temperature_to_homekit, temperature_to_states, density_to_air_quality) + convert_to_float, density_to_air_quality, dismiss_setup_message, + show_setup_message, temperature_to_homekit, temperature_to_states) from homeassistant.components.homekit.util import validate_entity_config \ as vec from homeassistant.components.persistent_notification import ( - DOMAIN, ATTR_MESSAGE, ATTR_NOTIFICATION_ID) + ATTR_MESSAGE, ATTR_NOTIFICATION_ID, DOMAIN) from homeassistant.const import ( - ATTR_CODE, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, CONF_NAME) + ATTR_CODE, CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT) from tests.common import async_mock_service From 1bc916927ca7138ac136e722a41a6ac2f65a9c25 Mon Sep 17 00:00:00 2001 From: Marco Orovecchia Date: Mon, 21 May 2018 17:02:50 +0200 Subject: [PATCH 125/144] fix nanoleaf aurora lights min and max temperature (#14571) * fixed nanoleaf aurora lights min and max temperature * review changes --- homeassistant/components/light/nanoleaf_aurora.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/light/nanoleaf_aurora.py b/homeassistant/components/light/nanoleaf_aurora.py index 99c07166037..c26766d8deb 100644 --- a/homeassistant/components/light/nanoleaf_aurora.py +++ b/homeassistant/components/light/nanoleaf_aurora.py @@ -92,6 +92,16 @@ class AuroraLight(Light): """Return the list of supported effects.""" return self._effects_list + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 154 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 833 + @property def name(self): """Return the display name of this light.""" From 4671bd95c612cc55c05ef3284f697b8fbeb76f27 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 May 2018 11:10:53 -0400 Subject: [PATCH 126/144] Version bump to 0.70.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 68f4443d12e..73dc0db7aac 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 45d1d30a8b698a5c8b7495845512c1a345432216 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 13:08:12 -0400 Subject: [PATCH 127/144] Update frontend to 20180524.0 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 04e4e0dae48..8ee6ce549a4 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180521.0'] +REQUIREMENTS = ['home-assistant-frontend==20180524.0'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index b86398bb3e8..94f49fb2454 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4fe31b0639..57224aa4233 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180521.0 +home-assistant-frontend==20180524.0 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a4d45c46e8859268b185a311c64282fe824e5348 Mon Sep 17 00:00:00 2001 From: Greg Laabs Date: Mon, 21 May 2018 12:00:01 -0700 Subject: [PATCH 128/144] Fix ISY moisure sensors showing unknown until a leak is detected (#14496) * Fix ISY leak sensors always showing UNKNOWN until a leak is detected Added some logic that handles both moisture sensors and door/window sensors * Handle edge case of leak sensor status update after ISY reboot If a leak sensor is unknown, due to a recent reboot of the ISY, the status will get updated to dry upon the first heartbeat. This status update is the only way that a leak sensor's status changes without an accompanying Control event, so we need to watch for it. * Fixes from overnight testing State was checking the incorrect parameter, and wasn't calling schedule update * Remove leftover debug log line * Remove unnecessary pylint instruction * Remove access of protected property We can't cast _.status directly to a bool for some unknown reason (possibly with the VarEvents library), but casting to an int then bool does work. --- .../components/binary_sensor/isy994.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/binary_sensor/isy994.py b/homeassistant/components/binary_sensor/isy994.py index fb86244acf3..09f1739cba7 100644 --- a/homeassistant/components/binary_sensor/isy994.py +++ b/homeassistant/components/binary_sensor/isy994.py @@ -117,8 +117,10 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if _is_val_unknown(self._node.status._val): self._computed_state = None + self._status_was_unknown = True else: self._computed_state = bool(self._node.status._val) + self._status_was_unknown = False @asyncio.coroutine def async_added_to_hass(self) -> None: @@ -156,9 +158,13 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): # pylint: disable=protected-access if not _is_val_unknown(self._negative_node.status._val): # If the negative node has a value, it means the negative node is - # in use for this device. Therefore, we cannot determine the state - # of the sensor until we receive our first ON event. - self._computed_state = None + # in use for this device. Next we need to check to see if the + # negative and positive nodes disagree on the state (both ON or + # both OFF). + if self._negative_node.status._val == self._node.status._val: + # The states disagree, therefore we cannot determine the state + # of the sensor until we receive our first ON event. + self._computed_state = None def _negative_node_control_handler(self, event: object) -> None: """Handle an "On" control event from the "negative" node.""" @@ -189,14 +195,21 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self.schedule_update_ha_state() self._heartbeat() - # pylint: disable=unused-argument def on_update(self, event: object) -> None: - """Ignore primary node status updates. + """Primary node status updates. - We listen directly to the Control events on all nodes for this - device. + We MOSTLY ignore these updates, as we listen directly to the Control + events on all nodes for this device. However, there is one edge case: + If a leak sensor is unknown, due to a recent reboot of the ISY, the + status will get updated to dry upon the first heartbeat. This status + update is the only way that a leak sensor's status changes without + an accompanying Control event, so we need to watch for it. """ - pass + if self._status_was_unknown and self._computed_state is None: + self._computed_state = bool(int(self._node.status)) + self._status_was_unknown = False + self.schedule_update_ha_state() + self._heartbeat() @property def value(self) -> object: From 69e86c29a6ede65f7baa61846435bf18bca24682 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 21 May 2018 19:46:20 -0400 Subject: [PATCH 129/144] Bump insteonplm version to fix device hanging (#14582) * Update inteonplm to 0.9.2 * Change to force Travis CI * Change to force Travis CI --- homeassistant/components/insteon_plm/__init__.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon_plm/__init__.py b/homeassistant/components/insteon_plm/__init__.py index 246e84ec71f..b86f80cbee7 100644 --- a/homeassistant/components/insteon_plm/__init__.py +++ b/homeassistant/components/insteon_plm/__init__.py @@ -17,7 +17,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['insteonplm==0.9.1'] +REQUIREMENTS = ['insteonplm==0.9.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 94f49fb2454..0bd40c03fae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -449,7 +449,7 @@ influxdb==5.0.0 insteonlocal==0.53 # homeassistant.components.insteon_plm -insteonplm==0.9.1 +insteonplm==0.9.2 # homeassistant.components.verisure jsonpath==0.75 From ef35b8d42879d8209f7e56abc660353a0df1a810 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 14:24:14 -0400 Subject: [PATCH 130/144] Fix hue discovery popping up (#14614) * Fix hue discovery popping up * Fix result * Fix tests --- homeassistant/auth.py | 3 +++ homeassistant/config_entries.py | 15 +++++++++------ homeassistant/data_entry_flow.py | 8 ++++---- tests/test_config_entries.py | 20 ++++++++++++++++++++ tests/test_data_entry_flow.py | 3 ++- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/homeassistant/auth.py b/homeassistant/auth.py index 7c01776b7b1..5e434b74ca8 100644 --- a/homeassistant/auth.py +++ b/homeassistant/auth.py @@ -347,6 +347,9 @@ class AuthManager: async def _async_finish_login_flow(self, result): """Result of a credential login flow.""" + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + auth_provider = self._providers[result['handler']] return await auth_provider.async_get_or_create_credentials( result['data']) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1350cd7d76a..8a73e424fb5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -347,6 +347,15 @@ class ConfigEntries: async def _async_finish_flow(self, result): """Finish a config flow and add an entry.""" + # If no discovery config entries in progress, remove notification. + if not any(ent['source'] in DISCOVERY_SOURCES for ent + in self.hass.config_entries.flow.async_progress()): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID) + + if result['type'] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return None + entry = ConfigEntry( version=result['version'], domain=result['handler'], @@ -370,12 +379,6 @@ class ConfigEntries: if result['source'] not in DISCOVERY_SOURCES: return entry - # If no discovery config entries in progress, remove notification. - if not any(ent['source'] in DISCOVERY_SOURCES for ent - in self.hass.config_entries.flow.async_progress()): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID) - return entry async def _async_create_flow(self, handler, *, source, data): diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index e9580aba273..5095297e795 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -110,11 +110,11 @@ class FlowManager: # Abort and Success results both finish the flow self._progress.pop(flow.flow_id) - if result['type'] == RESULT_TYPE_ABORT: - return result - # We pass a copy of the result because we're mutating our version - result['result'] = await self._async_finish_flow(dict(result)) + entry = await self._async_finish_flow(dict(result)) + + if result['type'] == RESULT_TYPE_CREATE_ENTRY: + result['result'] = entry return result diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1518706db55..84bd0771542 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -284,3 +284,23 @@ async def test_discovery_notification(hass): await hass.async_block_till_done() state = hass.states.get('persistent_notification.config_entry_discovery') assert state is None + + +async def test_discovery_notification_not_created(hass): + """Test that we not create a notification when discovery is aborted.""" + loader.set_component(hass, 'test', MockModule('test')) + await async_setup_component(hass, 'persistent_notification', {}) + + class TestFlow(data_entry_flow.FlowHandler): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + return self.async_abort(reason='test') + + with patch.dict(config_entries.HANDLERS, {'test': TestFlow}): + await hass.config_entries.flow.async_init( + 'test', source=data_entry_flow.SOURCE_DISCOVERY) + + await hass.async_block_till_done() + state = hass.states.get('persistent_notification.config_entry_discovery') + assert state is None diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 6d3e41436c5..894fd4d7194 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -21,7 +21,8 @@ def manager(): return handler() async def async_add_entry(result): - entries.append(result) + if (result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY): + entries.append(result) manager = data_entry_flow.FlowManager( None, async_create_flow, async_add_entry) From 43d2e436b9750fcaf5392062b252d686a6ca30a3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 May 2018 14:25:15 -0400 Subject: [PATCH 131/144] Version bump to 0.70.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 73dc0db7aac..dafe7e90db5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From bfc16428dad23ea72c836955a43cf5f289b5b836 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 08:33:22 -0400 Subject: [PATCH 132/144] Bump frontend to 20180526.1 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 8ee6ce549a4..f6b8bc9cb7a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180524.0'] +REQUIREMENTS = ['home-assistant-frontend==20180526.1'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0bd40c03fae..1386c490695 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57224aa4233..9e2ea04fb8c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180524.0 +home-assistant-frontend==20180526.1 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 19351fc429389a948297e4eb31e24c0ceb825058 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 11:32:45 -0400 Subject: [PATCH 133/144] Use libsodium18 (#14624) --- virtualization/Docker/setup_docker_prereqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 302dfba2e1d..23f55eea13f 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -22,7 +22,7 @@ PACKAGES=( # homeassistant.components.device_tracker.bluetooth_tracker bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks - libsodium13 + libsodium18 # homeassistant.components.zwave libudev-dev # homeassistant.components.homekit_controller From 2f0435ebd81bc7b3a9a0b474b831b33720218c9d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 May 2018 13:49:45 -0400 Subject: [PATCH 134/144] No longer use backports for ffmpeg (#14626) --- virtualization/Docker/scripts/ffmpeg | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/virtualization/Docker/scripts/ffmpeg b/virtualization/Docker/scripts/ffmpeg index 81b9ce694f9..914c2648e56 100755 --- a/virtualization/Docker/scripts/ffmpeg +++ b/virtualization/Docker/scripts/ffmpeg @@ -8,9 +8,4 @@ PACKAGES=( ffmpeg ) -# Add jessie-backports -echo "Adding jessie-backports" -echo "deb http://deb.debian.org/debian jessie-backports main" >> /etc/apt/sources.list -apt-get update - -apt-get install -y --no-install-recommends -t jessie-backports ${PACKAGES[@]} \ No newline at end of file +apt-get install -y --no-install-recommends ${PACKAGES[@]} From c9498d9f0941575bbf15e19b602f5dda9352b55c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 08:35:18 -0400 Subject: [PATCH 135/144] Version bump to 0.70.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dafe7e90db5..86653d4c909 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 52c21a53b37bdfa37f2466ecfd301a1ccae4511c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:53:24 -0400 Subject: [PATCH 136/144] Bump frontend to 20180526.2 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index f6b8bc9cb7a..7d888a2b082 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.1'] +REQUIREMENTS = ['home-assistant-frontend==20180526.2'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 1386c490695..0dbd5f74d3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e2ea04fb8c..405511c6d50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.1 +home-assistant-frontend==20180526.2 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From 6c62f7231b07db9f058908799955418642829fa6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:53:57 -0400 Subject: [PATCH 137/144] Version bump to 0.70.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 86653d4c909..f2d95bc2ac4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0b5' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 6b9addfeeab05a8de0d2a2cb0df99b1c6e73c085 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 11:54:50 -0400 Subject: [PATCH 138/144] Update release script --- script/release | 1 + 1 file changed, 1 insertion(+) diff --git a/script/release b/script/release index dc3e208bc1a..cf4f808377e 100755 --- a/script/release +++ b/script/release @@ -27,5 +27,6 @@ then exit 1 fi +rm -rf dist python3 setup.py sdist bdist_wheel python3 -m twine upload dist/* --skip-existing From fb447cab82dae8a2332f82d4f9db4fc11d7e804c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 14:29:26 -0400 Subject: [PATCH 139/144] Bump frontend to 20180526.3 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7d888a2b082..654afd67f42 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.2'] +REQUIREMENTS = ['home-assistant-frontend==20180526.3'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index 0dbd5f74d3d..d58c58e38b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 405511c6d50..9622fe9674a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.2 +home-assistant-frontend==20180526.3 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a5b9e59ceeee8cdf3d4629b44abd9485927b382b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 14:30:03 -0400 Subject: [PATCH 140/144] Version bump to 0.70.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f2d95bc2ac4..37c583e3b7e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b5' +PATCH_VERSION = '0b6' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 07b27283805dea27ae9163c49db1ac4d23147e52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 20:02:16 -0400 Subject: [PATCH 141/144] Bump frontend to 20180526.4 --- homeassistant/components/frontend/__init__.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 654afd67f42..2bd7283e38e 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -25,7 +25,7 @@ from homeassistant.core import callback from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass -REQUIREMENTS = ['home-assistant-frontend==20180526.3'] +REQUIREMENTS = ['home-assistant-frontend==20180526.4'] DOMAIN = 'frontend' DEPENDENCIES = ['api', 'websocket_api', 'http', 'system_log'] diff --git a/requirements_all.txt b/requirements_all.txt index d58c58e38b2..ae16651d8e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -389,7 +389,7 @@ hipnotify==1.0.8 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.homekit_controller # homekit==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9622fe9674a..ce458995d2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ hbmqtt==0.9.2 holidays==0.9.5 # homeassistant.components.frontend -home-assistant-frontend==20180526.3 +home-assistant-frontend==20180526.4 # homeassistant.components.influxdb # homeassistant.components.sensor.influxdb From a9b0f92afefb6a5bef1abd865808ac0845e691ed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 26 May 2018 20:02:54 -0400 Subject: [PATCH 142/144] Version bump to 0.70.0b7 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 37c583e3b7e..0ff3f0738a1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b6' +PATCH_VERSION = '0b7' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 94a82ab7dc1b504ee89bd93f75fc40503b023bf1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 May 2018 17:17:19 -0400 Subject: [PATCH 143/144] Allow Hass.io panel dir (#14655) --- homeassistant/components/hassio/http.py | 2 +- tests/components/hassio/test_http.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 9dd6427ec38..bb4f8219a33 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -36,7 +36,7 @@ NO_TIMEOUT = { } NO_AUTH = { - re.compile(r'^app-(es5|latest)/(index|hassio-app).html$'), + re.compile(r'^app-(es5|latest)/.+$'), re.compile(r'^addons/[^/]*/logo$') } diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ed425ad8cca..ac90deb9f73 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -48,7 +48,7 @@ def test_auth_required_forward_request(hassio_client): @pytest.mark.parametrize( 'build_type', [ 'es5/index.html', 'es5/hassio-app.html', 'latest/index.html', - 'latest/hassio-app.html' + 'latest/hassio-app.html', 'es5/some-chunk.js', 'es5/app.js', ]) def test_forward_request_no_auth_for_panel(hassio_client, build_type): """Test no auth needed for .""" From cd0e321668706c767d4be49dd66002743dac9d7a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 27 May 2018 17:18:53 -0400 Subject: [PATCH 144/144] Version bump to 0.70.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0ff3f0738a1..84088c4511c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 70 -PATCH_VERSION = '0b7' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3)